Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Verified against Minecraft 26.3

Java Minecraft is one codebase that runs as two programs. The server is the world: a loop that ticks twenty times a second, owns every chunk, entity and block, and decides what they are — save a few things it takes from your client once it has checked them, chief among them where the player you control, and anything it steers, stands. The client is a window, a loop that draws frames as fast as vsync or the frame-rate limit allow, and a copy of the world it is told about — which it will happily change on its own and be overruled about. It ticks too, nought to ten times inside each frame, catching its own copy up to the same twenty-a-second clock and dropping any tick owed beyond ten. They talk over a real Netty connection even when both run in the same process — in singleplayer the connection never touches a socket, but the packets are real, and an IntegratedServer is a MinecraftServer with the client half attached rather than absent. The split is behind some of the first things a player notices: the client predicts and the server overrules; a chunk exists on the server long before the client is sent it; a sword swing is a packet, a hit is a reply.

flowchart TD
    subgraph Client["Render thread"]
        MC["Minecraft: a frame, 0 to 10 ticks in it"]
        CL["ClientLevel: the copy it is told about"]
    end
    subgraph Netty["Netty event loop"]
        Conn["Connection: socket or in-process"]
    end
    subgraph Server["Server thread"]
        MS["MinecraftServer: a tick every 50 ms"]
        SL["ServerLevel: the world, one per dimension"]
    end
    subgraph Pool["Worker-Main-n"]
        W["generation, lighting, meshing"]
    end
    MC --> CL
    MS --> SL
    MC -- "what the player did" --> Conn
    Conn -- "what the world became" --> CL
    Conn <--> SL
    W -. "chunks" .-> SL
    W -. "meshes" .-> MC

The two programs and the four threads they run on: the client and the server each own their copy of the world and talk through the connection, and the worker pool hands its results back, dotted, to whichever of them asked.

The whole thing is 7,301 source files and about 740,000 lines of Java 25. Just under a third of those lines is client-only; the rest ships in both jars, and the picture below is the split — orange is the client’s, blue is everything the dedicated server also ships, and the hatched boxes are the packages Part I’s what this book skips tours, most of them what this book leaves out.

The 26.3 jar as a treemap of packages: area is lines of decompiled source, colour is which jar ships the package, hatching marks the packages what this book skips tours net/minecraft/world — 2715 classes, 313,098 lines, shared net/minecraft/world/level — 1418 classes, 151,146 lines, shared level 151,146 net/minecraft/world/entity — 727 classes, 110,760 lines, shared entity 110,760 net/minecraft/world/item — 323 classes, 30,661 lines, shared item 30,661 net/minecraft/world/inventory — 64 classes, 7,152 lines, shared inventory 7,152 net/minecraft/world/phys — 28 classes, 3,036 lines, shared phys net/minecraft/world/attribute — 30 classes, 1,936 lines, shared net/minecraft/world — 25 classes, 1,686 lines, shared net/minecraft/world/scores — 16 classes, 1,442 lines, shared net/minecraft/world/effect — 20 classes, 1,306 lines, shared net/minecraft/world/damagesource — 12 classes, 936 lines, shared net/minecraft/world/ticks — 14 classes, 869 lines, shared net/minecraft/world/waypoints — 9 classes, 677 lines, shared net/minecraft/world/timeline — 5 classes, 446 lines, shared net/minecraft/world/clock — 11 classes, 390 lines, shared net/minecraft/world/flag — 7 classes, 356 lines, shared net/minecraft/world/food — 6 classes, 299 lines, shared world 42% net/minecraft/client — 1891 classes, 176,818 lines, client-only net/minecraft/client/gui — 445 classes, 59,594 lines, client-only gui 59,594 net/minecraft/client/renderer — 715 classes, 50,649 lines, client-only renderer 50,649 net/minecraft/client/model — 268 classes, 12,712 lines, client-only model 12,712 net/minecraft/client/multiplayer — 63 classes, 11,303 lines, client-only multiplayer 11,303 net/minecraft/client — 41 classes, 10,821 lines, client-only (itself) 10,821 net/minecraft/client/resources — 110 classes, 8,326 lines, client-only resources 8,326 net/minecraft/client/particle — 88 classes, 6,839 lines, client-only particle 6,839 net/minecraft/client/data — 28 classes, 6,189 lines, client-only — toured in what this book skips data 6,189 net/minecraft/client/sounds — 17 classes, 2,174 lines, client-only sounds net/minecraft/client/player — 11 classes, 2,072 lines, client-only player net/minecraft/client/telemetry — 18 classes, 1,221 lines, client-only — toured in what this book skips net/minecraft/client/server — 6 classes, 885 lines, client-only net/minecraft/client/color — 17 classes, 879 lines, client-only net/minecraft/client/tutorial — 10 classes, 671 lines, client-only net/minecraft/client/main — 4 classes, 523 lines, client-only net/minecraft/client/animation — 23 classes, 509 lines, client-only net/minecraft/client/searchtree — 8 classes, 505 lines, client-only net/minecraft/client/input — 8 classes, 304 lines, client-only net/minecraft/client/quickplay — 3 classes, 284 lines, client-only net/minecraft/client/entity — 4 classes, 228 lines, client-only net/minecraft/client/profiling — 2 classes, 81 lines, client-only net/minecraft/client/waypoints — 2 classes, 49 lines, client-only client 24% net/minecraft/util — 735 classes, 54,700 lines, shared net/minecraft/util/datafix — 411 classes, 27,048 lines, shared — toured in what this book skips datafix 27,048 net/minecraft/util — 93 classes, 12,767 lines, shared (itself) 12,767 net/minecraft/util/profiling — 70 classes, 4,265 lines, shared — toured in what this book skips profiling 4,265 net/minecraft/util/filefix — 57 classes, 3,553 lines, shared — toured in what this book skips filefix net/minecraft/util/parsing — 29 classes, 1,879 lines, shared net/minecraft/util/debug — 19 classes, 1,348 lines, shared net/minecraft/util/worldupdate — 6 classes, 958 lines, shared net/minecraft/util/thread — 9 classes, 842 lines, shared net/minecraft/util/valueproviders — 19 classes, 707 lines, shared net/minecraft/util/eventlog — 4 classes, 459 lines, shared net/minecraft/util/random — 4 classes, 374 lines, shared net/minecraft/util/debugchart — 8 classes, 199 lines, shared net/minecraft/util/context — 4 classes, 181 lines, shared net/minecraft/util/monitoring — 2 classes, 120 lines, shared util 7% net/minecraft/server — 432 classes, 53,789 lines, shared net/minecraft/server/commands — 111 classes, 13,357 lines, shared commands 13,357 net/minecraft/server/level — 42 classes, 12,166 lines, shared level 12,166 net/minecraft/server/network — 29 classes, 5,566 lines, shared network 5,566 net/minecraft/server — 27 classes, 5,153 lines, shared (itself) 5,153 net/minecraft/server/packs — 57 classes, 5,122 lines, shared packs 5,122 net/minecraft/server/jsonrpc — 65 classes, 4,118 lines, shared — toured in what this book skips jsonrpc 4,118 net/minecraft/server/players — 19 classes, 2,770 lines, shared players net/minecraft/server/dedicated — 7 classes, 1,811 lines, shared net/minecraft/server/dialog — 35 classes, 885 lines, shared net/minecraft/server/rcon — 9 classes, 837 lines, shared — toured in what this book skips net/minecraft/server/permissions — 12 classes, 402 lines, shared net/minecraft/server/chase — 3 classes, 354 lines, shared net/minecraft/server/notifications — 5 classes, 343 lines, shared net/minecraft/server/bossevents — 3 classes, 328 lines, shared net/minecraft/server/gui — 4 classes, 310 lines, shared net/minecraft/server/waypoints — 2 classes, 180 lines, shared net/minecraft/server/advancements — 2 classes, 87 lines, shared server 7% net/minecraft/network — 416 classes, 23,319 lines, shared net/minecraft/network/protocol — 298 classes, 12,872 lines, shared protocol 12,872 net/minecraft/network/chat — 63 classes, 4,799 lines, shared chat 4,799 net/minecraft/network — 42 classes, 3,590 lines, shared net/minecraft/network/codec — 7 classes, 1,533 lines, shared net/minecraft/network/syncher — 6 classes, 525 lines, shared network 3% com/mojang/renderpearl — 146 classes, 18,659 lines, client-only com/mojang/renderpearl/backend — 76 classes, 13,866 lines, client-only backend 13,866 com/mojang/renderpearl/frontend — 12 classes, 2,876 lines, client-only frontend com/mojang/renderpearl/api — 53 classes, 1,879 lines, client-only api com/mojang/renderpearl/util — 4 classes, 34 lines, client-only com/mojang/renderpearl — 1 classes, 4 lines, client-only renderpearl net/minecraft/data — 174 classes, 16,849 lines, shared net/minecraft/data/worldgen — 64 classes, 6,016 lines, shared — toured in what this book skips worldgen 6,016 net/minecraft/data/loot — 21 classes, 3,023 lines, shared — toured in what this book skips loot 3,023 net/minecraft/data/recipes — 20 classes, 2,507 lines, shared — toured in what this book skips net/minecraft/data/tags — 29 classes, 1,756 lines, shared — toured in what this book skips tags net/minecraft/data — 10 classes, 1,272 lines, shared — toured in what this book skips net/minecraft/data/advancements — 10 classes, 896 lines, shared — toured in what this book skips net/minecraft/data/info — 8 classes, 518 lines, shared — toured in what this book skips net/minecraft/data/structures — 5 classes, 473 lines, shared — toured in what this book skips net/minecraft/data/registries — 5 classes, 325 lines, shared — toured in what this book skips net/minecraft/data/metadata — 2 classes, 63 lines, shared — toured in what this book skips data 2% com/mojang/realmsclient — 137 classes, 14,701 lines, client-only com/mojang/realmsclient/gui — 46 classes, 7,091 lines, client-only — toured in what this book skips gui 7,091 com/mojang/realmsclient/client — 19 classes, 2,643 lines, client-only — toured in what this book skips client com/mojang/realmsclient/dto — 43 classes, 1,986 lines, client-only — toured in what this book skips dto com/mojang/realmsclient — 4 classes, 1,492 lines, client-only — toured in what this book skips com/mojang/realmsclient/util — 20 classes, 1,425 lines, client-only — toured in what this book skips util com/mojang/realmsclient/exception — 5 classes, 64 lines, client-only — toured in what this book skips realmsclient net/minecraft/commands — 124 classes, 13,229 lines, shared net/minecraft/commands/arguments — 73 classes, 9,017 lines, shared arguments 9,017 net/minecraft/commands — 14 classes, 1,921 lines, shared net/minecraft/commands/synchronization — 12 classes, 905 lines, shared net/minecraft/commands/execution — 18 classes, 756 lines, shared net/minecraft/commands/functions — 7 classes, 630 lines, shared commands 2% net/minecraft/core — 123 classes, 12,359 lines, shared net/minecraft/core — 39 classes, 5,499 lines, shared (itself) 5,499 net/minecraft/core/component — 32 classes, 3,067 lines, shared net/minecraft/core/registries — 14 classes, 1,525 lines, shared net/minecraft/core/dispenser — 14 classes, 1,098 lines, shared net/minecraft/core/particles — 21 classes, 863 lines, shared net/minecraft/core/cauldron — 3 classes, 307 lines, shared core 2% com/mojang/blaze3d — 86 classes, 10,437 lines, client-only com/mojang/blaze3d/platform — 26 classes, 4,382 lines, client-only com/mojang/blaze3d/vertex — 14 classes, 2,322 lines, client-only vertex com/mojang/blaze3d/audio — 12 classes, 1,013 lines, client-only — toured in what this book skips com/mojang/blaze3d/systems — 5 classes, 773 lines, client-only com/mojang/blaze3d/framegraph — 3 classes, 437 lines, client-only com/mojang/blaze3d/font — 7 classes, 432 lines, client-only com/mojang/blaze3d/pipeline — 5 classes, 366 lines, client-only com/mojang/blaze3d — 5 classes, 289 lines, client-only com/mojang/blaze3d/buffers — 3 classes, 228 lines, client-only com/mojang/blaze3d/resource — 6 classes, 195 lines, client-only blaze3d 1% net/minecraft/nbt — 43 classes, 7,791 lines, shared net/minecraft/nbt — 36 classes, 7,279 lines, shared (itself) 7,279 net/minecraft/nbt/visitors — 7 classes, 512 lines, shared nbt 1% net/minecraft/advancements — 115 classes, 7,647 lines, shared net/minecraft/advancements/predicates — 53 classes, 3,450 lines, shared net/minecraft/advancements/triggers — 49 classes, 2,816 lines, shared net/minecraft/advancements — 13 classes, 1,381 lines, shared net/minecraft/gametest — 48 classes, 5,756 lines, shared net/minecraft/gametest/framework — 46 classes, 5,737 lines, shared net/minecraft/gametest — 2 classes, 19 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared sounds net/minecraft/tags — 29 classes, 1,874 lines, shared net/minecraft/tags — 29 classes, 1,874 lines, shared tags net/minecraft — 19 classes, 1,665 lines, shared net/minecraft — 19 classes, 1,665 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared — toured in what this book skips net/minecraft/resources — 12 classes, 1,473 lines, shared net/minecraft/resources — 12 classes, 1,473 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared math net/minecraft/stats — 10 classes, 874 lines, shared net/minecraft/stats — 10 classes, 874 lines, shared — toured in what this book skips net/minecraft/gizmos — 15 classes, 569 lines, shared net/minecraft/gizmos — 15 classes, 569 lines, shared — toured in what this book skips net/minecraft/recipebook — 3 classes, 351 lines, shared net/minecraft/locale — 3 classes, 247 lines, shared net/minecraft/realms — 4 classes, 203 lines, client-only in both jars client only hatched: toured in what this book skips 741,069 lines, 7301 classes — area is lines of decompiled source
The two jars. Every box is a package, its area is lines of decompiled source; the atlas walks through it. Click to enlarge.

The four boxes of the first picture are the four threads that carry nearly all of it — the Render thread, which is also the client’s game thread; the Server thread; the Netty event loop; and a shared worker pool, whose threads are named Worker-Main-n — and the first lecture of the book, Anatomy, is those four threads and the two loops.

How the book is read

This site is the notes for a video lecture series, and that is why it keeps saying watched where a book would say read. One system page is one lecture; the page stands alone and nothing on it leans on the video to make sense, but the order, the scenarios and the figures are all built for someone who will eventually be shown them. The site is that series in three tiers, and the sidebar is its table of contents.

Parts are watched in order. Thirteen of them, I to XIII, each a system: the anatomy of the two programs, the foundations they are both built out of, the server, the world, blocks, entities, items, the player, networking, the client, rendering, world generation, and commands. Each opens on a landing page that argues what its part claims about its system, says what shape the part is, names the parts it assumes and what it uses each of them for, lists its pages in the order to watch them, says where the part stops, and says which Reference pages the part reads. A page is one lecture’s notes: it follows one scenario through the system (a player walks east across a chunk boundary; a server is clicked in the list) and its figure is the artefact — a sequence diagram whose lanes are class names, a state machine, a flowchart of a decision. Every diagram enlarges on click. Each part only assumes the ones before it, and the lecture map says where that is not quite true. The picture below is the whole of that, drawn as the line it nearly is: the parts in watch order, and a solid arc from a part to each part that assumes it. The two dependencies the whole book leans on, whether or not a landing page lists them — Part I for the threads, Part II for codecs and registries — keep their boxes and the one arc each along the spine but have the rest of theirs left off, because those would reach almost every box, and the two dashed arcs are the only places a part assumes a later one, each cut on purpose rather than solved by reordering.

The thirteen parts in watch order; an arc on the right is a part another part assumes, an arc on the left is one of the two places a part assumes a later one Part I · AnatomyI · Anatomy Part II · FoundationsII · Foundations Part III · The serverIII · The server Part IV · The worldIV · The world Part V · BlocksV · Blocks Part VI · EntitiesVI · Entities Part VII · Items and inventoriesVII · Items and inventories Part VIII · The playerVIII · The player Part IX · NetworkingIX · Networking Part X · The clientX · The client Part XI · RenderingXI · Rendering Part XII · World generationXII · World generation Part XIII · Commands and data packsXIII · Commands and data packs I → II: Part II assumes Part I II → III: Part III assumes Part II III → IV: Part IV assumes Part III IV → V: Part V assumes Part IV V → VI: Part VI assumes Part V VII → VIII: Part VIII assumes Part VII IX → X: Part X assumes Part IX X → XI: Part XI assumes Part X III → V: Part V assumes Part III IV → VI: Part VI assumes Part IV V → VII: Part VII assumes Part V VI → VIII: Part VIII assumes Part VI III → VI: Part VI assumes Part III VI → IX: Part IX assumes Part VI VII → X: Part X assumes Part VII III → VII: Part VII assumes Part III VI → X: Part X assumes Part VI IX → XIII: Part XIII assumes Part IX III → VIII: Part VIII assumes Part III V → X: Part X assumes Part V III → IX: Part IX assumes Part III VII → XIII: Part XIII assumes Part VII IV → XI: Part XI assumes Part IV IV → XII: Part XII assumes Part IV III → XIII: Part XIII assumes Part III IV ⇢ III: Part III assumes Tickets and loading, Environment attributes and timelines, cut on purpose Tickets and loading Environment attributes and timelines X ⇢ V: Part V assumes Prediction and acknowledgement, cut on purpose Prediction and acknowledgement

The thirteen parts top to bottom in watch order; an arc on the right runs from a part to one that assumes it, always downward, and the two dashed arcs on the left are the only places a part assumes a later one, each labelled with the pages it needs.

Maps are looked at once. The atlas is four views of the decompile — where the code is, where the mass is, what everything imports and what extends what. Each view’s figure and its table are regenerated on every deploy, so their numbers are re-derived each time; the prose around them is written by hand from those tables, or from the tree where no table holds a number, and is re-read when the version moves. It is the “where is everything” answer a newcomer wants before any system page makes sense.

Reference is looked up. Twenty-three pages, of which these are the ones a reader reaches for first: the glossary, the naming drift table, a class index that answers “which page talks about ChunkMap”, the diagram lanes and the threads. Thirteen of the twenty-three are rewritten from the decompile or from the corpus on every deploy, so their rows are re-derived each time; the other ten are hand-kept, and the shelf’s own front page says which is which. The rule for what belongs here is would a viewer pause the video to read this.

For agents, the whole site is one file: llms-full.txt, regenerated on every deploy.

The rules the book keeps

Names, never code. A page names classes, methods, fields and packages so that anyone with the decompiled source can find them in a minute, and explains what they own, when they run and how they interact. It never reproduces the source — not a method body, not a snippet. Anyone who needs the code decompiles the game themselves, which is also the line Mojang’s mappings licence draws.

Mojang’s names. Every game identifier is Mojang’s official mapping, which the decompile uses. Fabric’s Yarn names differ; where a modder would not recognise a class under its official name, the Yarn name is noted once. Names have moved since 1.21 — even since 1.21.11, Lightmap was LightTexture and RedstoneWireBlock was RedStoneWireBlock — and the naming drift table is the translation.

Newest version only. Every page that describes the game says in its header which release it was verified against, or for a generated view which release it was generated from, and the book’s is 26.3. There are no version-difference sections, and the only “in 1.x this was” on a system page is the For a 1.21-era reader note at its foot. When a release lands, every page is re-read against it, and until a page has been, its header still names the release it was checked against.

Verified means tested. Every backticked name on every written page, and every class name of more than one word and every member named with its class or its lane inside a diagram, is checked against the decompile of the release the page’s header names before the site publishes, every diagram is parsed by the same mermaid the site ships, every lane in every diagram is checked against the one key the whole book uses, and every link and anchor between pages is checked to land. A page that fails any of those does not go up, and neither does a change that puts the landing pages, the sidebar, the lecture map, the dependency figure and the Reference shelf’s own index of who uses it out of step with each other. That is a narrow guarantee, and it is worth stating narrowly: it proves the names are real in that release, not that the sentence around them is true. The sentences are what the passes are for. The book has been fact-checked against the decompile three times — as drafted, after the pages were restructured into the book you are reading, and again for this release — and a page written after the first check has had every check since. The first two times every page checked had something wrong on it, and the third found errors again in every part. What is left is a correction waiting to be filed, and the repository is where to file it.

How the system works, not how the code reads. Object-level: this class owns that state, this call happens on that thread, this packet is sent then. Never line-level. Code makes boring video and dates fast.

What this book skips

Most of what the treemap hatches is left out, and it falls into three kinds. Code that is version differences by definition — save migration, the util/datafix and util/filefix trees — which rule three excludes on sight. Code that is not the game — Realms, telemetry, the profiler, the management server, RCON, the data generators. And code that is the game’s bookkeeping rather than its behaviour — statistics, player reporting, and a package of id constants nobody will recognise. What this book skips, the second lecture of Part I, draws that boundary honestly — what each thing is, how big, whether the dedicated server ships it, and the two or three class names to start at if you need it anyway — so a viewer knows the edge of the map before investing in thirteen parts. It also tours three things that are not skipped and are only surprising for where they live: the OpenAL backend, which sits in Blaze3D beside the window and RenderSystem rather than in the client’s sound package; the recipe book, which sits in the statistics package and belongs to Part VII; and the debug-drawing API every debug renderer in the game is now written against.

Unofficial, and free to reuse

This is an independent description of how the game works. It is not endorsed by, sponsored by or associated with Mojang Studios or Microsoft, and Minecraft is a trademark of Mojang Synergies AB.

The book is CC BY-SA 4.0: take it, adapt it, teach from it — credit minecraftdocs.dev and keep what you build under the same licence. That covers the writing and the figures, which are the only things here that are anyone’s to license. It does not cover the game, its source, its assets or Mojang’s mappings; none of those are in this book, which is why it names identifiers and never reproduces code. Corrections, and the repository the book is written in, are on GitHub.

The atlas

Verified against Minecraft 26.3 · Maps · Four views of the whole decompile, drawn by tools/map_source.py from the source tree on every deploy.

Before any system page makes sense you want the answer to a newcomer’s question: where is everything? The atlas is that answer, looked at once. Each map is one or more figures drawn from the decompile, prose saying what they show, and the tables they were drawn from — four figures and two tables on what extends what, one and one on the two bar charts, one and three on where the code is. Take them in any order; none of the four needs another.

No number in the atlas’s figures and tables is counted by hand — they come from tools/map_source.py, which reads all 7,301 files — but the four pages are not all one thing, and the distinction matters as much here as on the Reference shelf. The figures and the tables are generated: they live in src/generated/, are rewritten on every deploy, and cannot drift from the source they describe. The prose is typed, and every number quoted in a sentence was typed by a session, read off one of those tables or counted from the tree where no table holds it. So a version bump re-derives the tables by itself and leaves the sentences around them to be re-read, which is what the version pass is for.

flowchart LR
    D[("the 26.3 decompile")] --> T["map_source.py, on every deploy"] --> G["seven figures, seven tables"]
    G -- "included as they are" --> P["the four map pages"]
    G -- "read and typed by a session" --> S["the sentences"] --> P
    D -. "or counted from it where no table holds the number" .-> S

The two routes into an atlas page: the figures and tables arrive by the upper one and cannot drift, and the sentences arrive by the lower one, typed by a session, which is the route a version bump has to walk again by hand.

The four maps

mapthe question it answersthe figure
Where the code ishow big is each package, which jar ships it, and which parts of the book cover itthe jar as a treemap of packages, area by lines
Where the mass iswhich classes are the largest, and what kind of thing gets that bigthe thirty largest classes as bars
What everything importswhich Mojang classes the rest of the code cannot be written without, and where the book teaches eachthe thirty most-imported Mojang classes as bars
What extends whatwhich inheritance trees are widest, and what shape they arefour trees with the descendant count on every branch

The treemap is also the book’s picture of the two jars: the introduction uses it to show how much of the code the dedicated server ships, and Part I’s what this book skips uses its hatching to draw the boundary of the book.

How the numbers are counted

The decompile is the client jar, which is a strict superset of the server jar; beside it sits server-classes.txt, the list of classes the dedicated server also ships. From those two things every number on these pages follows.

numberhow it is counted
a classone .java file in the decompile — nested types are not counted as classes, and the 565 four-line package-info.java markers are counted like any other file
a lineone line of the decompiled file, blank lines included, so counts are comparable with each other and not with Mojang’s own source
client-onlythe file is not listed in server-classes.txt; shared means it is
fan-inhow many files have a net.minecraft or com.mojang import statement naming the class — the JDK is not counted, and same-package use needs no import, which is what what everything imports then has to allow for
descendantsevery type reachable from a root through extends and implements, nested types included; a parent written as outer dot inner resolves inside the outer class’s own file, so two types with the same simple name stay two types

The line counts include what the decompiler adds — braces on their own lines, expanded switches — and a table of constants written a line apiece can be longer than a class that does something. Read length as where the reading is, not where the difficulty is.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Where the code is

Verified against Minecraft 26.3 · Maps · The jar as a treemap of packages: area is lines of decompiled source, colour is which jar ships the package, hatching is the packages Part I’s what this book skips tours.

Java Minecraft is 7,301 classes and 741,069 lines of decompiled Java 25, and the first surprise in it is how little of that the client owns alone. Everything a player sees — every screen, the HUD, the whole renderer, the entity models, both GPU back-ends, the sound engine, Realms — is the orange near-third of this picture. The blue seven tenths ship in both jars, and the biggest inner box, world/level, is a fifth of the game by itself: blocks, block states, chunks, lighting and world generation, all of it code the dedicated server runs with no window attached.

The 26.3 jar as a treemap of packages: area is lines of decompiled source, colour is which jar ships the package, hatching marks the packages what this book skips tours net/minecraft/world — 2715 classes, 313,098 lines, shared net/minecraft/world/level — 1418 classes, 151,146 lines, shared level 151,146 net/minecraft/world/entity — 727 classes, 110,760 lines, shared entity 110,760 net/minecraft/world/item — 323 classes, 30,661 lines, shared item 30,661 net/minecraft/world/inventory — 64 classes, 7,152 lines, shared inventory 7,152 net/minecraft/world/phys — 28 classes, 3,036 lines, shared phys net/minecraft/world/attribute — 30 classes, 1,936 lines, shared net/minecraft/world — 25 classes, 1,686 lines, shared net/minecraft/world/scores — 16 classes, 1,442 lines, shared net/minecraft/world/effect — 20 classes, 1,306 lines, shared net/minecraft/world/damagesource — 12 classes, 936 lines, shared net/minecraft/world/ticks — 14 classes, 869 lines, shared net/minecraft/world/waypoints — 9 classes, 677 lines, shared net/minecraft/world/timeline — 5 classes, 446 lines, shared net/minecraft/world/clock — 11 classes, 390 lines, shared net/minecraft/world/flag — 7 classes, 356 lines, shared net/minecraft/world/food — 6 classes, 299 lines, shared world 42% net/minecraft/client — 1891 classes, 176,818 lines, client-only net/minecraft/client/gui — 445 classes, 59,594 lines, client-only gui 59,594 net/minecraft/client/renderer — 715 classes, 50,649 lines, client-only renderer 50,649 net/minecraft/client/model — 268 classes, 12,712 lines, client-only model 12,712 net/minecraft/client/multiplayer — 63 classes, 11,303 lines, client-only multiplayer 11,303 net/minecraft/client — 41 classes, 10,821 lines, client-only (itself) 10,821 net/minecraft/client/resources — 110 classes, 8,326 lines, client-only resources 8,326 net/minecraft/client/particle — 88 classes, 6,839 lines, client-only particle 6,839 net/minecraft/client/data — 28 classes, 6,189 lines, client-only — toured in what this book skips data 6,189 net/minecraft/client/sounds — 17 classes, 2,174 lines, client-only sounds net/minecraft/client/player — 11 classes, 2,072 lines, client-only player net/minecraft/client/telemetry — 18 classes, 1,221 lines, client-only — toured in what this book skips net/minecraft/client/server — 6 classes, 885 lines, client-only net/minecraft/client/color — 17 classes, 879 lines, client-only net/minecraft/client/tutorial — 10 classes, 671 lines, client-only net/minecraft/client/main — 4 classes, 523 lines, client-only net/minecraft/client/animation — 23 classes, 509 lines, client-only net/minecraft/client/searchtree — 8 classes, 505 lines, client-only net/minecraft/client/input — 8 classes, 304 lines, client-only net/minecraft/client/quickplay — 3 classes, 284 lines, client-only net/minecraft/client/entity — 4 classes, 228 lines, client-only net/minecraft/client/profiling — 2 classes, 81 lines, client-only net/minecraft/client/waypoints — 2 classes, 49 lines, client-only client 24% net/minecraft/util — 735 classes, 54,700 lines, shared net/minecraft/util/datafix — 411 classes, 27,048 lines, shared — toured in what this book skips datafix 27,048 net/minecraft/util — 93 classes, 12,767 lines, shared (itself) 12,767 net/minecraft/util/profiling — 70 classes, 4,265 lines, shared — toured in what this book skips profiling 4,265 net/minecraft/util/filefix — 57 classes, 3,553 lines, shared — toured in what this book skips filefix net/minecraft/util/parsing — 29 classes, 1,879 lines, shared net/minecraft/util/debug — 19 classes, 1,348 lines, shared net/minecraft/util/worldupdate — 6 classes, 958 lines, shared net/minecraft/util/thread — 9 classes, 842 lines, shared net/minecraft/util/valueproviders — 19 classes, 707 lines, shared net/minecraft/util/eventlog — 4 classes, 459 lines, shared net/minecraft/util/random — 4 classes, 374 lines, shared net/minecraft/util/debugchart — 8 classes, 199 lines, shared net/minecraft/util/context — 4 classes, 181 lines, shared net/minecraft/util/monitoring — 2 classes, 120 lines, shared util 7% net/minecraft/server — 432 classes, 53,789 lines, shared net/minecraft/server/commands — 111 classes, 13,357 lines, shared commands 13,357 net/minecraft/server/level — 42 classes, 12,166 lines, shared level 12,166 net/minecraft/server/network — 29 classes, 5,566 lines, shared network 5,566 net/minecraft/server — 27 classes, 5,153 lines, shared (itself) 5,153 net/minecraft/server/packs — 57 classes, 5,122 lines, shared packs 5,122 net/minecraft/server/jsonrpc — 65 classes, 4,118 lines, shared — toured in what this book skips jsonrpc 4,118 net/minecraft/server/players — 19 classes, 2,770 lines, shared players net/minecraft/server/dedicated — 7 classes, 1,811 lines, shared net/minecraft/server/dialog — 35 classes, 885 lines, shared net/minecraft/server/rcon — 9 classes, 837 lines, shared — toured in what this book skips net/minecraft/server/permissions — 12 classes, 402 lines, shared net/minecraft/server/chase — 3 classes, 354 lines, shared net/minecraft/server/notifications — 5 classes, 343 lines, shared net/minecraft/server/bossevents — 3 classes, 328 lines, shared net/minecraft/server/gui — 4 classes, 310 lines, shared net/minecraft/server/waypoints — 2 classes, 180 lines, shared net/minecraft/server/advancements — 2 classes, 87 lines, shared server 7% net/minecraft/network — 416 classes, 23,319 lines, shared net/minecraft/network/protocol — 298 classes, 12,872 lines, shared protocol 12,872 net/minecraft/network/chat — 63 classes, 4,799 lines, shared chat 4,799 net/minecraft/network — 42 classes, 3,590 lines, shared net/minecraft/network/codec — 7 classes, 1,533 lines, shared net/minecraft/network/syncher — 6 classes, 525 lines, shared network 3% com/mojang/renderpearl — 146 classes, 18,659 lines, client-only com/mojang/renderpearl/backend — 76 classes, 13,866 lines, client-only backend 13,866 com/mojang/renderpearl/frontend — 12 classes, 2,876 lines, client-only frontend com/mojang/renderpearl/api — 53 classes, 1,879 lines, client-only api com/mojang/renderpearl/util — 4 classes, 34 lines, client-only com/mojang/renderpearl — 1 classes, 4 lines, client-only renderpearl net/minecraft/data — 174 classes, 16,849 lines, shared net/minecraft/data/worldgen — 64 classes, 6,016 lines, shared — toured in what this book skips worldgen 6,016 net/minecraft/data/loot — 21 classes, 3,023 lines, shared — toured in what this book skips loot 3,023 net/minecraft/data/recipes — 20 classes, 2,507 lines, shared — toured in what this book skips net/minecraft/data/tags — 29 classes, 1,756 lines, shared — toured in what this book skips tags net/minecraft/data — 10 classes, 1,272 lines, shared — toured in what this book skips net/minecraft/data/advancements — 10 classes, 896 lines, shared — toured in what this book skips net/minecraft/data/info — 8 classes, 518 lines, shared — toured in what this book skips net/minecraft/data/structures — 5 classes, 473 lines, shared — toured in what this book skips net/minecraft/data/registries — 5 classes, 325 lines, shared — toured in what this book skips net/minecraft/data/metadata — 2 classes, 63 lines, shared — toured in what this book skips data 2% com/mojang/realmsclient — 137 classes, 14,701 lines, client-only com/mojang/realmsclient/gui — 46 classes, 7,091 lines, client-only — toured in what this book skips gui 7,091 com/mojang/realmsclient/client — 19 classes, 2,643 lines, client-only — toured in what this book skips client com/mojang/realmsclient/dto — 43 classes, 1,986 lines, client-only — toured in what this book skips dto com/mojang/realmsclient — 4 classes, 1,492 lines, client-only — toured in what this book skips com/mojang/realmsclient/util — 20 classes, 1,425 lines, client-only — toured in what this book skips util com/mojang/realmsclient/exception — 5 classes, 64 lines, client-only — toured in what this book skips realmsclient net/minecraft/commands — 124 classes, 13,229 lines, shared net/minecraft/commands/arguments — 73 classes, 9,017 lines, shared arguments 9,017 net/minecraft/commands — 14 classes, 1,921 lines, shared net/minecraft/commands/synchronization — 12 classes, 905 lines, shared net/minecraft/commands/execution — 18 classes, 756 lines, shared net/minecraft/commands/functions — 7 classes, 630 lines, shared commands 2% net/minecraft/core — 123 classes, 12,359 lines, shared net/minecraft/core — 39 classes, 5,499 lines, shared (itself) 5,499 net/minecraft/core/component — 32 classes, 3,067 lines, shared net/minecraft/core/registries — 14 classes, 1,525 lines, shared net/minecraft/core/dispenser — 14 classes, 1,098 lines, shared net/minecraft/core/particles — 21 classes, 863 lines, shared net/minecraft/core/cauldron — 3 classes, 307 lines, shared core 2% com/mojang/blaze3d — 86 classes, 10,437 lines, client-only com/mojang/blaze3d/platform — 26 classes, 4,382 lines, client-only com/mojang/blaze3d/vertex — 14 classes, 2,322 lines, client-only vertex com/mojang/blaze3d/audio — 12 classes, 1,013 lines, client-only — toured in what this book skips com/mojang/blaze3d/systems — 5 classes, 773 lines, client-only com/mojang/blaze3d/framegraph — 3 classes, 437 lines, client-only com/mojang/blaze3d/font — 7 classes, 432 lines, client-only com/mojang/blaze3d/pipeline — 5 classes, 366 lines, client-only com/mojang/blaze3d — 5 classes, 289 lines, client-only com/mojang/blaze3d/buffers — 3 classes, 228 lines, client-only com/mojang/blaze3d/resource — 6 classes, 195 lines, client-only blaze3d 1% net/minecraft/nbt — 43 classes, 7,791 lines, shared net/minecraft/nbt — 36 classes, 7,279 lines, shared (itself) 7,279 net/minecraft/nbt/visitors — 7 classes, 512 lines, shared nbt 1% net/minecraft/advancements — 115 classes, 7,647 lines, shared net/minecraft/advancements/predicates — 53 classes, 3,450 lines, shared net/minecraft/advancements/triggers — 49 classes, 2,816 lines, shared net/minecraft/advancements — 13 classes, 1,381 lines, shared net/minecraft/gametest — 48 classes, 5,756 lines, shared net/minecraft/gametest/framework — 46 classes, 5,737 lines, shared net/minecraft/gametest — 2 classes, 19 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared sounds net/minecraft/tags — 29 classes, 1,874 lines, shared net/minecraft/tags — 29 classes, 1,874 lines, shared tags net/minecraft — 19 classes, 1,665 lines, shared net/minecraft — 19 classes, 1,665 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared — toured in what this book skips net/minecraft/resources — 12 classes, 1,473 lines, shared net/minecraft/resources — 12 classes, 1,473 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared math net/minecraft/stats — 10 classes, 874 lines, shared net/minecraft/stats — 10 classes, 874 lines, shared — toured in what this book skips net/minecraft/gizmos — 15 classes, 569 lines, shared net/minecraft/gizmos — 15 classes, 569 lines, shared — toured in what this book skips net/minecraft/recipebook — 3 classes, 351 lines, shared net/minecraft/locale — 3 classes, 247 lines, shared net/minecraft/realms — 4 classes, 203 lines, client-only in both jars client only hatched: toured in what this book skips 741,069 lines, 7301 classes — area is lines of decompiled source
The 26.3 decompile as a treemap. Each outer box is a package directly under net/minecraft or com/mojang, labelled with its share of the lines where the box has room for it; the boxes inside are its sub-packages. Hover a box for its counts; click the figure to enlarge it.

Two jars, one tree

The client jar is a strict superset of the server jar, and the split is clean: the 2,264 client-only classes live in exactly five packages — net/minecraft/client (1,891 classes), com/mojang/renderpearl (146), com/mojang/realmsclient (137), com/mojang/blaze3d (86) and net/minecraft/realms (4). Every other package in the table below ships in both jars, class for class; no sub-package at this depth is mixed. The client-only side is 220,818 lines, 29.8% of the total, which is the “just under a third” the introduction quotes.

The consequence for reading this book: when a page says a class is client-only it is saying which package the class lives in, and every package Parts IV and V own ships in both jars. The oracle for which side a class is on is server-classes.txt.

What the big boxes are

world — 42%. world/level (151,146 lines, 1,418 classes) is Parts IV, V and XII: chunks, block states, lighting and, under world/level/levelgen, most of world generation — world/level/biome is its sibling rather than part of it. world/entity (110,760 lines, 727 classes) is Parts VI and VIII: the entity hierarchy, its AI and the player. world/item and world/inventory (37,813 lines between them) are Part VII.

client — 24%. client/gui (59,594 lines) is the screens and the HUD, Part X; client/renderer and client/model (63,361 lines together) are the frame, section meshing and the entity models, Part XI; client/multiplayer (11,303) holds ClientPacketListener and ClientLevel, the client’s copy of the world. The 41 classes directly in net/minecraft/client — Minecraft, Options, KeyMapping, MouseHandler — are 10,821 lines on their own. (That 41 is the itself only count defined below; the 1,891 above is the package with every sub-package under it.)

util — 7%, two thirds of it skipped. util/datafix, util/filefix and util/profiling are 34,866 of the package’s 54,700 lines and are all outside this book; the hatched boxes are those three, and much the largest of them is the save-migration history. What is left is the toolbox every part uses — Mth, RandomSource, Util — and the parsing and debug packages.

server — 7%. server/commands (13,357) is Part XIII’s command implementations; server/level is 42 classes and 12,166 lines — 290 lines a class, nearly three times the map’s average, in a package small enough to list (ServerLevel, ChunkMap, ServerPlayer), Parts III and IV; server/network is the serverbound packet handlers, Part IX; server/packs is the pack system, Part II; server/jsonrpc, the management server, is skipped.

renderpearl and blaze3d — 4%. The GPU abstraction, com/mojang/renderpearl, has two back-ends behind GpuDevice, and the Vulkan one (renderpearl/backend/vulkan, 7,387 lines) is larger than the OpenGL one (renderpearl/backend/opengl, 5,815); com/mojang/blaze3d keeps the window, the frame graph, the vertex formats and RenderSystem.

network — 3%, in 416 classes. network/protocol is 298 classes in 12,872 lines: the packet catalogue is many tiny classes, one per packet. network/chat (4,799) is Component and chat signing.

Below 3% the other boxes include Part II’s foundations — net/minecraft/core, net/minecraft/nbt, net/minecraft/tags, net/minecraft/resources — and Part XIII’s net/minecraft/commands (the command source, the argument types and the execution engine) and net/minecraft/advancements, plus com/mojang/realmsclient and the skipped net/minecraft/data. That last one is the program that writes the vanilla data pack — and it is not build-time only: the dedicated server ships all 174 classes, and Blocks and MinecraftServer both read data/worldgen constants at run time (what this book skips sorts every such reader into three kinds).

Where each part lives

The parts follow the tree, but not one box each. This table is the map from the book’s order to the jar’s, and it is the one table on this page that is a decision rather than a measurement: the packages are assigned by hand, in the tool that draws the atlas, and the counts follow. A package under net/minecraft is written without that prefix; itself only means the files directly in the package and not its sub-packages; a package two parts share is counted in both and a file in no part’s packages in none, so the total row is not the jar’s. A landing page that states its part’s size quotes its own row of this table, and the hatched packages and player reporting are left out of every row.

partpackagesclassesclient-onlylines
I · Anatomyclient/main, Minecraft, Main, MinecraftServer756,817
II · Foundationscore, resources, tags, nbt, server/packs, util, world/flag468048,809
III · The serverserver (itself only), server/level, server/players, server/dedicated95021,900
IV · The worldworld/level/chunk, world/level/lighting, world/ticks, world/level/gameevent, world/level/entity, world/level/material, world/attribute, world/timeline, world/clock, world/level/border, server/level, world/level/blockscan229031,913
V · Blocksworld/level/block, world/level/redstone484058,507
VI · Entitiesworld/entity, network/syncher, world/level/pathfinder, world/damagesource, world/effect, minus world/entity/player7660112,091
VII · Items and inventoriesworld/item, world/inventory, world/level/storage/loot581049,245
VIII · The playerworld/entity/player, world/food, ServerPlayer, client/player32118,460
IX · Networkingnetwork, server/network, client/multiplayer, minus network/syncher4905138,711
X · The clientclient (itself only), client/gui, client/multiplayer, client/sounds, client/resources, client/player, client/input, client/server68968994,527
XI · Renderingclient/renderer, client/model, client/particle, com/mojang/blaze3d, com/mojang/renderpearl1,2911,29198,283
XII · World generationworld/level/levelgen, world/level/biome477049,303
XIII · Commands and data packscommands, server/commands, server/dialog, server/permissions, server/bossevents, advancements, gametest, world/scores, client/gui/screens/dialog4811744,083
the thirteen parts, with the shared packages counted twice6,0902,064662,649

The table

Depth three is the outer boxes of the treemap; depth four is the inner ones. client-only counts the classes not in server-classes.txt.

package (depth 3)classesclient-onlylines
net/minecraft/world27150313,098
net/minecraft/client18911891176,818
net/minecraft/util735054,700
net/minecraft/server432053,789
net/minecraft/network416023,319
com/mojang/renderpearl14614618,659
net/minecraft/data174016,849
com/mojang/realmsclient13713714,701
net/minecraft/commands124013,229
net/minecraft/core123012,359
com/mojang/blaze3d868610,437
net/minecraft/nbt4307,791
net/minecraft/advancements11507,647
net/minecraft/gametest4805,756
net/minecraft/sounds602,124
net/minecraft/tags2901,874
net/minecraft1901,665
net/minecraft/references501,483
net/minecraft/resources1201,473
com/mojang/math1001,054
net/minecraft/stats100874
net/minecraft/gizmos150569
net/minecraft/recipebook30351
net/minecraft/locale30247
net/minecraft/realms44203
total73012264741,069
package (depth 4)classesclient-onlylines
net/minecraft/world/level14180151,146
net/minecraft/world/entity7270110,760
net/minecraft/client/gui44544559,594
net/minecraft/client/renderer71571550,649
net/minecraft/world/item323030,661
net/minecraft/util/datafix411027,048
com/mojang/renderpearl/backend767613,866
net/minecraft/server/commands111013,357
net/minecraft/network/protocol298012,872
net/minecraft/util93012,767
net/minecraft/client/model26826812,712
net/minecraft/server/level42012,166
net/minecraft/client/multiplayer636311,303
net/minecraft/client414110,821
net/minecraft/commands/arguments7309,017
net/minecraft/client/resources1101108,326
net/minecraft/nbt3607,279
net/minecraft/world/inventory6407,152
com/mojang/realmsclient/gui46467,091
net/minecraft/client/particle88886,839
net/minecraft/client/data28286,189
net/minecraft/data/worldgen6406,016
net/minecraft/gametest/framework4605,737
net/minecraft/server/network2905,566
net/minecraft/core3905,499
net/minecraft/server2705,153
net/minecraft/server/packs5705,122
net/minecraft/network/chat6304,799
com/mojang/blaze3d/platform26264,382
net/minecraft/util/profiling7004,265
net/minecraft/server/jsonrpc6504,118
net/minecraft/network4203,590
net/minecraft/util/filefix5703,553
net/minecraft/advancements/predicates5303,450
net/minecraft/core/component3203,067
net/minecraft/world/phys2803,036
net/minecraft/data/loot2103,023
com/mojang/renderpearl/frontend12122,876
net/minecraft/advancements/triggers4902,816
net/minecraft/server/players1902,770
com/mojang/realmsclient/client19192,643
net/minecraft/data/recipes2002,507
com/mojang/blaze3d/vertex14142,322
net/minecraft/client/sounds17172,174
net/minecraft/sounds602,124
net/minecraft/client/player11112,072
com/mojang/realmsclient/dto43431,986
net/minecraft/world/attribute3001,936
net/minecraft/commands1401,921
com/mojang/renderpearl/api53531,879
net/minecraft/util/parsing2901,879
net/minecraft/tags2901,874
net/minecraft/server/dedicated701,811
net/minecraft/data/tags2901,756
net/minecraft/world2501,686
net/minecraft1901,665
net/minecraft/network/codec701,533
net/minecraft/core/registries1401,525
com/mojang/realmsclient441,492
net/minecraft/references501,483
net/minecraft/resources1201,473
net/minecraft/world/scores1601,442
com/mojang/realmsclient/util20201,425
net/minecraft/advancements1301,381
net/minecraft/util/debug1901,348
net/minecraft/world/effect2001,306
net/minecraft/data1001,272
net/minecraft/client/telemetry18181,221
net/minecraft/core/dispenser1401,098
com/mojang/math1001,054
com/mojang/blaze3d/audio12121,013
net/minecraft/util/worldupdate60958
net/minecraft/world/damagesource120936
net/minecraft/commands/synchronization120905
net/minecraft/data/advancements100896
net/minecraft/client/server66885
net/minecraft/server/dialog350885
net/minecraft/client/color1717879
net/minecraft/stats100874
net/minecraft/world/ticks140869
net/minecraft/core/particles210863
net/minecraft/util/thread90842
net/minecraft/server/rcon90837
com/mojang/blaze3d/systems55773
net/minecraft/commands/execution180756
net/minecraft/util/valueproviders190707
net/minecraft/world/waypoints90677
net/minecraft/client/tutorial1010671
net/minecraft/commands/functions70630
net/minecraft/gizmos150569
net/minecraft/network/syncher60525
net/minecraft/client/main44523
net/minecraft/data/info80518
net/minecraft/nbt/visitors70512
net/minecraft/client/animation2323509
net/minecraft/client/searchtree88505
net/minecraft/data/structures50473
net/minecraft/util/eventlog40459
net/minecraft/world/timeline50446
com/mojang/blaze3d/framegraph33437
com/mojang/blaze3d/font77432
net/minecraft/server/permissions120402
net/minecraft/world/clock110390
net/minecraft/util/random40374
com/mojang/blaze3d/pipeline55366
net/minecraft/world/flag70356
net/minecraft/server/chase30354
net/minecraft/recipebook30351
net/minecraft/server/notifications50343
net/minecraft/server/bossevents30328
net/minecraft/data/registries50325
net/minecraft/server/gui40310
net/minecraft/core/cauldron30307
net/minecraft/client/input88304
net/minecraft/world/food60299
com/mojang/blaze3d55289
net/minecraft/client/quickplay33284
net/minecraft/locale30247
com/mojang/blaze3d/buffers33228
net/minecraft/client/entity44228
net/minecraft/realms44203
net/minecraft/util/debugchart80199
com/mojang/blaze3d/resource66195
net/minecraft/util/context40181
net/minecraft/server/waypoints20180
net/minecraft/util/monitoring20120
net/minecraft/server/advancements2087
net/minecraft/client/profiling2281
com/mojang/realmsclient/exception5564
net/minecraft/data/metadata2063
net/minecraft/client/waypoints2249
com/mojang/renderpearl/util4434
net/minecraft/gametest2019
com/mojang/renderpearl114
total73012264741,069

Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Where the mass is

Verified against Minecraft 26.3 · Maps · The forty largest classes by lines of decompiled source, the top thirty of them drawn, coloured by which jar ships them.

The two largest classes in the game are the top of one hierarchy: Entity and LivingEntity, 9,078 lines between them, the thing every mob and player is before it is anything else. Third is Minecraft, the client itself. And then the list turns odd: two of the top ten are not game logic at all. BlockModelGenerators runs only inside the data generator — its single caller is ModelProvider — and writes the block model JSON that ships in the jar; BlockStateData is a table that the save-migration fixes consult when data a much older version saved is read, and nothing outside util/datafix reads it. Size is where the reading is, not where the game is.

The 30 largest classes of 26.3 by lines of decompiled source Entity (world/entity): 4,617 lines Entity world/entity 4,617 LivingEntity (world/entity): 4,461 lines LivingEntity world/entity 4,461 Minecraft (client): 3,345 lines Minecraft client 3,345 BlockModelGenerators (client/data/models): 3,136 lines BlockModelGenerators client/data/models 3,136 ClientPacketListener (client/multiplayer): 3,090 lines ClientPacketListener client/multiplayer 3,090 MinecraftServer (server): 2,608 lines MinecraftServer server 2,608 ServerGamePacketListenerImpl (server/network): 2,567 lines ServerGamePacketListenerImpl server/network 2,567 ServerPlayer (server/level): 2,516 lines ServerPlayer server/level 2,516 BlockStateData (util/datafix/fixes): 2,270 lines BlockStateData util/datafix/fixes 2,270 ServerLevel (server/level): 2,139 lines ServerLevel server/level 2,139 Items (world/item): 2,067 lines Items world/item 2,067 Blocks (world/level/block): 2,061 lines Blocks world/level/block 2,061 Player (world/entity/player): 2,034 lines Player world/entity/player 2,034 SoundEvents (sounds): 2,023 lines SoundEvents sounds 2,023 Options (client): 2,010 lines Options client 2,010 OceanMonumentPieces (world/level/levelgen/structure/structures): 1,983 lines OceanMonumentPieces world/level/levelgen/structure/structures 1,983 CreativeModeTabs (world/item): 1,737 lines CreativeModeTabs world/item 1,737 ChunkMap (server/level): 1,661 lines ChunkMap server/level 1,661 DataFixers (util/datafix): 1,637 lines DataFixers util/datafix 1,637 Fox (world/entity/animal/fox): 1,625 lines Fox world/entity/animal/fox 1,625 Mob (world/entity): 1,616 lines Mob world/entity 1,616 LevelRenderer (client/renderer): 1,499 lines LevelRenderer client/renderer 1,499 Hud (client/gui): 1,481 lines Hud client/gui 1,481 Bee (world/entity/animal/bee): 1,450 lines Bee world/entity/animal/bee 1,450 StrongholdPieces (world/level/levelgen/structure/structures): 1,436 lines StrongholdPieces world/level/levelgen/structure/structures 1,436 LocalPlayer (client/player): 1,435 lines LocalPlayer client/player 1,435 FriendlyByteBuf (network): 1,414 lines FriendlyByteBuf network 1,414 VanillaBlockLoot (data/loot/packs): 1,412 lines VanillaBlockLoot data/loot/packs 1,412 BlockBehaviour (world/level/block/state): 1,380 lines BlockBehaviour world/level/block/state 1,380 Util (util): 1,372 lines Util util 1,372
The thirty largest classes of 26.3. Blue ships in both jars, orange is client-only; the small grey text is the package under net/minecraft, or under com/mojang where it starts mojang/. Click to enlarge.

64,082 lines are in these thirty classes, 8.6% of the game in 0.4% of its files.

Three kinds of big

Read down the bars and twenty-four of the thirty sort themselves into three kinds, and the kind tells you how to read the class.

The god objects. Entity and LivingEntity are the trunk, each over four thousand lines because each is the base of everything below it. The fork below them is not one chain: Mob is one of LivingEntity‘s direct subclasses and Avatar is another, with Player under it and ServerPlayer and LocalPlayer its two leaves in the thirty (entity anatomy has the tree). Those two leaves are long for the opposite reason to the trunk: no named class descends from either, and each is a whole side of the authority split written out. Minecraft and MinecraftServer are the two programs’ roots; ServerLevel is the world; ChunkMap is the world’s loader. Only two concrete mobs make the thirty — Fox and Bee, the two with the most bespoke behaviour in their own files — and what they are long with is per-species goal classes, which is exactly why Part VI explains the machine and declines to enumerate its instances: there is no Fox page and there is not going to be one.

The switchboards. ClientPacketListener is a handler method for every clientbound play packet, and ServerGamePacketListenerImpl is the same for every serverbound one — all but three of the packets both phases share are handled one class up, in ClientCommonPacketListenerImpl and ServerCommonPacketListenerImpl; between them they are the whole of what play can say, which is why Part IX’s pages keep coming back to them. FriendlyByteBuf is the buffer their packets are read from.

The catalogues written as code. SoundEvents, Blocks and Items are registries populated a constant at a time, and CreativeModeTabs lists every tab’s items a line apiece; DataFixers is the migration history; OceanMonumentPieces and StrongholdPieces are structures built by hand, room by room, in Java rather than in a template; Options is every setting in options.txt, and Hud is the in-world overlay — the crosshair, the hotbar, the bars — inside the wider Gui that also draws screens, toasts and the loading overlay. These are long because they are lists. None is hard.

The table

Forty rows, of which the figure draws thirty. side is which jar ships the class.

classlinesside
net/minecraft/world/entity/Entity4,617shared
net/minecraft/world/entity/LivingEntity4,461shared
net/minecraft/client/Minecraft3,345client
net/minecraft/client/data/models/BlockModelGenerators3,136client
net/minecraft/client/multiplayer/ClientPacketListener3,090client
net/minecraft/server/MinecraftServer2,608shared
net/minecraft/server/network/ServerGamePacketListenerImpl2,567shared
net/minecraft/server/level/ServerPlayer2,516shared
net/minecraft/util/datafix/fixes/BlockStateData2,270shared
net/minecraft/server/level/ServerLevel2,139shared
net/minecraft/world/item/Items2,067shared
net/minecraft/world/level/block/Blocks2,061shared
net/minecraft/world/entity/player/Player2,034shared
net/minecraft/sounds/SoundEvents2,023shared
net/minecraft/client/Options2,010client
net/minecraft/world/level/levelgen/structure/structures/OceanMonumentPieces1,983shared
net/minecraft/world/item/CreativeModeTabs1,737shared
net/minecraft/server/level/ChunkMap1,661shared
net/minecraft/util/datafix/DataFixers1,637shared
net/minecraft/world/entity/animal/fox/Fox1,625shared
net/minecraft/world/entity/Mob1,616shared
net/minecraft/client/renderer/LevelRenderer1,499client
net/minecraft/client/gui/Hud1,481client
net/minecraft/world/entity/animal/bee/Bee1,450shared
net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces1,436shared
net/minecraft/client/player/LocalPlayer1,435client
net/minecraft/network/FriendlyByteBuf1,414shared
net/minecraft/data/loot/packs/VanillaBlockLoot1,412shared
net/minecraft/world/level/block/state/BlockBehaviour1,380shared
net/minecraft/util/Util1,372shared
net/minecraft/gametest/framework/GameTestHelper1,369shared
com/mojang/realmsclient/RealmsMainScreen1,344client
net/minecraft/client/multiplayer/ClientLevel1,334client
net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces1,317shared
net/minecraft/world/item/ItemStack1,274shared
net/minecraft/world/level/levelgen/structure/structures/WoodlandMansionPieces1,185shared
net/minecraft/world/entity/animal/panda/Panda1,121shared
net/minecraft/world/entity/animal/equine/AbstractHorse1,115shared
net/minecraft/client/gui/screens/inventory/CreativeModeInventoryScreen1,107client
net/minecraft/world/entity/monster/cubemob/SulfurCube1,083shared

Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What everything imports

Verified against Minecraft 26.3 · Maps · The thirty most-imported Mojang classes: how many files name each one in an import statement.

One file in six imports BlockPos. That is the least surprising fact on this page; the next one is not. The chart counts Mojang imports only — net.minecraft and com.mojang — because the JDK’s List and the nullability annotation outrank everything on it and say nothing about the game. Under that rule the second most-imported type is not Minecraft’s: it is Codec, from Mojang’s DataFixerUpper library, imported by 943 files, with MapCodec sixth and RecordCodecBuilder seventh. Three of the seven classes the game most depends on are the serialisation vocabulary that turns objects into NBT and JSON and back, which is why Part II teaches codecs before anything a player can see.

The 30 most-imported classes of 26.3: how many files import each BlockPos (core): 1,229 importing files BlockPos core 1,229 Codec (com.mojang.serialization): 943 importing files Codec com.mojang.serialization 943 Identifier (resources): 914 importing files Identifier resources 914 BlockState (world.level.block.state): 864 importing files BlockState world.level.block.state 864 Holder (core): 826 importing files Holder core 826 MapCodec (com.mojang.serialization): 806 importing files MapCodec com.mojang.serialization 806 RecordCodecBuilder (com.mojang.serialization.codecs): 797 importing files RecordCodecBuilder com.mojang.serialization.codecs 797 RandomSource (util): 793 importing files RandomSource util 793 Component (network.chat): 777 importing files Component network.chat 777 Level (world.level): 761 importing files Level world.level 761 ServerLevel (server.level): 735 importing files ServerLevel server.level 735 Mth (util): 703 importing files Mth util 703 ItemStack (world.item): 691 importing files ItemStack world.item 691 Entity (world.entity): 668 importing files Entity world.entity 668 Vec3 (world.phys): 585 importing files Vec3 world.phys 585 Direction (core): 584 importing files Direction core 584 StreamCodec (network.codec): 547 importing files StreamCodec network.codec 547 LivingEntity (world.entity): 536 importing files LivingEntity world.entity 536 LogUtils (com.mojang.logging): 488 importing files LogUtils com.mojang.logging 488 Registries (core.registries): 473 importing files Registries core.registries 473 Util (util): 457 importing files Util util 457 Player (world.entity.player): 445 importing files Player world.entity.player 445 ResourceKey (resources): 419 importing files ResourceKey resources 419 Schema (com.mojang.datafixers.schemas): 404 importing files Schema com.mojang.datafixers.schemas 404 ByteBufCodecs (network.codec): 327 importing files ByteBufCodecs network.codec 327 SoundEvents (sounds): 320 importing files SoundEvents sounds 320 BlockBehaviour (world.level.block.state): 313 importing files BlockBehaviour world.level.block.state 313 Blocks (world.level.block): 299 importing files Blocks world.level.block 299 Packet (network.protocol): 290 importing files Packet network.protocol 290 DSL (com.mojang.datafixers): 289 importing files DSL com.mojang.datafixers 289
The thirty most-imported Mojang classes of 26.3, counting only imports from Mojang's own packages. Blue ships in both jars and grey is a library outside the decompile; none of the thirty is client-only. Click to enlarge.

Seven ideas, and where the book teaches them

The hubs are not thirty ideas; twenty-five of them are seven. The table below groups them, reading down the sixty rows rather than the thirty the figure draws, so that a family is not broken in half by the cut-off — ByteBufCodecs and PacketType are the wire’s vocabulary whether or not they clear it. So the table holds thirty-two names: twenty-five of the thirty, plus seven more from ranks 31 to 60 that their families would be incomplete without. The five of the thirty it leaves out are the two paragraphs after it, two and three.

Part II teaches three of the seven, Reference two, Part IX one, and the seventh — the world’s nouns — is the rest of the book. That is the more useful fact than Part II teaches the vocabulary: the classes a reader must know are not one part’s worth.

ideathe hubswhere the book teaches it
a positionBlockPos, Vec3, Direction, MthMath and primitives
a name and a registryIdentifier, ResourceKey, Registries, BuiltInRegistries, HolderIdentifiers and registries
a shape on diskCodec, MapCodec, RecordCodecBuilderCodecs, NBT and JSON, and for MapCodec the data-driven type pattern
a shape on the wireStreamCodec, ByteBufCodecs, RegistryFriendlyByteBuf, Packet, PacketTypePackets and stream codecs
textComponentText components
chanceRandomSourceMath and primitives
the world’s nounsLevel, ServerLevel, BlockState, Block, Blocks, Entity, LivingEntity, Player, EntityType, ItemStack, SoundEvents, SoundEvent, DataComponentsParts II to X

Two of the thirty belong to no family and get no row above, and neither does the first class after them; each is worth a look. Minecraft (284) is thirty-first, the first client-only class on the list and just outside the figure — the client’s hub is a hub for the client-only side alone, which is under a third of the files, and the shared seven tenths never name it. And Schema (404) and DSL (289) are the migration tree talking to itself: all but ten of the files that import Schema are in util/datafix, and the ten are its sibling util/filefix. One pair inside the table earns the same second look: ServerLevel (735) is imported by nearly as many files as Level (761), because most code that touches the world knows it is on the server and says so in its types.

The remaining three of the thirty have no row either. LogUtils (488) is Mojang’s logging library, the one line at the top of a class that keeps a logger. Util (457) is where the executors and the static odds and ends live (threads) — a hub because it is where the unclassifiable went, not because it is an idea. And BlockBehaviour (313) is the world’s nouns seen one class higher — the same one-class-higher shape what extends what draws for the Block tree.

What the count misses

An import is counted once per file, so the chart says how many files name the class, not how often. It also undercounts every class used inside its own package, because that needs no import: the 260 files that import Block are the files outside world/level/block that use it, and the same is true of BlockBehaviour and Minecraft. A class’s true reach is this number plus its package.

The table

Sixty rows, of which the figure draws thirty. side is which jar ships the class, or library for a class outside the decompile.

classpackageimported byside
BlockPosnet.minecraft.core1229shared
Codeccom.mojang.serialization943library
Identifiernet.minecraft.resources914shared
BlockStatenet.minecraft.world.level.block.state864shared
Holdernet.minecraft.core826shared
MapCodeccom.mojang.serialization806library
RecordCodecBuildercom.mojang.serialization.codecs797library
RandomSourcenet.minecraft.util793shared
Componentnet.minecraft.network.chat777shared
Levelnet.minecraft.world.level761shared
ServerLevelnet.minecraft.server.level735shared
Mthnet.minecraft.util703shared
ItemStacknet.minecraft.world.item691shared
Entitynet.minecraft.world.entity668shared
Vec3net.minecraft.world.phys585shared
Directionnet.minecraft.core584shared
StreamCodecnet.minecraft.network.codec547shared
LivingEntitynet.minecraft.world.entity536shared
LogUtilscom.mojang.logging488library
Registriesnet.minecraft.core.registries473shared
Utilnet.minecraft.util457shared
Playernet.minecraft.world.entity.player445shared
ResourceKeynet.minecraft.resources419shared
Schemacom.mojang.datafixers.schemas404library
ByteBufCodecsnet.minecraft.network.codec327shared
SoundEventsnet.minecraft.sounds320shared
BlockBehaviournet.minecraft.world.level.block.state313shared
Blocksnet.minecraft.world.level.block299shared
Packetnet.minecraft.network.protocol290shared
DSLcom.mojang.datafixers289library
Minecraftnet.minecraft.client284client
EntityTypenet.minecraft.world.entity263shared
Blocknet.minecraft.world.level.block260shared
DataComponentsnet.minecraft.core.component258shared
PacketTypenet.minecraft.network.protocol245shared
RegistryFriendlyByteBufnet.minecraft.network239shared
Dynamiccom.mojang.serialization235library
SoundEventnet.minecraft.sounds235shared
ServerPlayernet.minecraft.server.level231shared
BuiltInRegistriesnet.minecraft.core.registries230shared
LevelReadernet.minecraft.world.level228shared
BlockGetternet.minecraft.world.level217shared
Itemsnet.minecraft.world.item215shared
SoundSourcenet.minecraft.sounds213shared
Paircom.mojang.datafixers.util212library
VoxelShapenet.minecraft.world.phys.shapes210shared
EntityTypesnet.minecraft.world.entity205shared
Itemnet.minecraft.world.item204shared
GuiGraphicsExtractornet.minecraft.client.gui200client
Registrynet.minecraft.core199shared
BlockTagsnet.minecraft.tags198shared
LootContextnet.minecraft.world.level.storage.loot196shared
ExtraCodecsnet.minecraft.util192shared
PoseStackcom.mojang.blaze3d.vertex192client
ModelPartnet.minecraft.client.model.geom191client
InteractionResultnet.minecraft.world190shared
CommandSyntaxExceptioncom.mojang.brigadier.exceptions188library
CommandSourceStacknet.minecraft.commands188shared
ValueOutputnet.minecraft.world.level.storage186shared
TypeRewriteRulecom.mojang.datafixers186library

Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What extends what

Verified against Minecraft 26.3 · Maps · The widest inheritance trees, four of them drawn with the number of descendants on every branch.

Block has 297 descendants and Entity 192 — descendants, not direct subclasses, which is the count both tables below use — and those are the two trees a reader of this book will climb most often. But neither is the widest hierarchy in the game. That is FeatureElement, with 386 descendants from seven implementers — and it is not a hierarchy at all. It is an interface for anything that can be put behind a feature flag, and its seven implementers are BlockBehaviour, Item, EntityType, MenuType, MobEffect, Potion and GameRule, so its descendant count sweeps up the whole block tree and the whole item tree at once. ItemLike, second at 366, is Block plus Item. The interface table is a list of which mix-ins reach furthest; the class table is where the real trees are, and four of them are drawn below.

Entity

The class hierarchy under Entity: each branch shows how many types descend from it Entity (192) LivingEntity (124) Mob (114) PathfinderMob (108) AmbientCreature (1) 3 with no subclasses Avatar (7) Player (4) Mannequin (1) ArmorStand Projectile (26) AbstractHurtingProjectile (8) AbstractWindCharge (2) Fireball (2) 2 with no subclasses ThrowableProjectile (8) ThrowableItemProjectile (7) AbstractArrow (3) 3 with no subclasses 4 with no subclasses VehicleEntity (15) AbstractMinecart (8) AbstractMinecartContainer (2) 5 with no subclasses AbstractBoat (5) AbstractChestBoat (2) 2 with no subclasses BlockAttachedEntity (6) HangingEntity (3) ItemFrame (1) Painting 2 with no subclasses Display (3) 3 with no subclasses 13 with no subclasses
The Entity tree to three levels. The number is how many types descend from the node; subclasses with no subclasses of their own are folded into one italic line per parent, or named where a parent has only one. Click to enlarge.

The shape is a spine with a few branches. LivingEntity holds 124 of the 192, Mob 114 of those, PathfinderMob 108 of those: nearly every mob is four classes deep before it is a species, and Part VI’s entity anatomy is that spine. The non-living entities are two families and a scattering — Projectile (26) and VehicleEntity (15), then hanging things, displays, and thirteen direct subclasses of Entity with no children of their own, from ItemEntity to LightningBolt.

Block

The class hierarchy under Block: each branch shows how many types descend from it Block (297) BaseEntityBlock (64) AbstractSkullBlock (7) WallSkullBlock (3) SkullBlock (2) AbstractChestBlock (5) ChestBlock (3) EnderChestBlock SignBlock (4) 4 with no subclasses AbstractFurnaceBlock (3) 3 with no subclasses AbstractBannerBlock (2) 2 with no subclasses CopperGolemStatueBlock (1) WeatheringCopperGolemStatueBlock DispenserBlock (1) DropperBlock SculkSensorBlock (1) CalibratedSculkSensorBlock 32 with no subclasses VegetationBlock (36) CropBlock (4) 4 with no subclasses DoublePlantBlock (4) 4 with no subclasses DryVegetationBlock (2) 2 with no subclasses FlowerBlock (2) 2 with no subclasses SaplingBlock (1) MangrovePropaguleBlock 18 with no subclasses HorizontalDirectionalBlock (21) FaceAttachedHorizontalDirectionalBlock (3) 3 with no subclasses AbstractBedBlock (2) 2 with no subclasses DiodeBlock (2) 2 with no subclasses TrapDoorBlock (1) WeatheringCopperTrapDoorBlock 9 with no subclasses GrowingPlantBlock (10) GrowingPlantBodyBlock (4) 4 with no subclasses GrowingPlantHeadBlock (4) 4 with no subclasses HalfTransparentBlock (9) TransparentBlock (4) WaterloggedTransparentBlock (1) 2 with no subclasses IceBlock (1) FrostedIceBlock 2 with no subclasses DirectionalBlock (7) RodBlock (3) LightningRodBlock (1) EndRodBlock 3 with no subclasses BaseCoralPlantTypeBlock (6) BaseCoralFanBlock (3) BaseCoralWallFanBlock (1) CoralFanBlock 2 with no subclasses FallingBlock (5) ColoredFallingBlock (1) SandBlock 3 with no subclasses BaseTorchBlock (4) RedstoneTorchBlock (1) RedstoneWallTorchBlock TorchBlock (1) WallTorchBlock CrossCollisionBlock (4) IronBarsBlock (2) 2 with no subclasses FenceBlock LeavesBlock (4) FallingParticlesLeavesBlock (3) TintedParticleLeavesBlock (1) UntintedParticleLeavesBlock AbstractCauldronBlock (3) 3 with no subclasses BaseRailBlock (3) 3 with no subclasses MultifaceBlock (3) MultifaceSpreadeableBlock (2) 2 with no subclasses RotatedPillarBlock (3) ChainBlock (1) WeatheringCopperChainBlock HayBlock SnowyBlock (3) SpreadingSnowyBlock (2) 2 with no subclasses AbstractCandleBlock (2) 2 with no subclasses AmethystBlock (2) 2 with no subclasses BaseFireBlock (2) 2 with no subclasses BasePressurePlateBlock (2) 2 with no subclasses SpeleothemBlock (2) 2 with no subclasses CarpetBlock (1) WoolCarpetBlock CopperBulbBlock (1) WeatheringCopperBulbBlock CraftingTableBlock (1) SmithingTableBlock DoorBlock (1) WeatheringCopperDoorBlock DropExperienceBlock (1) SculkBlock InfestedBlock (1) InfestedRotatedPillarBlock LanternBlock (1) WeatheringLanternBlock PipeBlock (1) ChorusPlantBlock SlabBlock (1) WeatheringCopperSlabBlock StairBlock (1) WeatheringCopperStairBlock 61 with no subclasses
The Block tree to three levels; 61 of its 92 direct subclasses have no subclasses of their own and are folded into the last line. Click to enlarge.

Block is wide and shallow: 92 direct subclasses, most of them terminal. The one deep branch is BaseEntityBlock (64), the abstract base most of the blocks that own a block entity extend — the interface that actually decides is EntityBlock, and two blocks implement it without taking that base at all, ComparatorBlock and JigsawBlock. Part V’s block entities is the branch in prose. The table’s first row, BlockBehaviour at 298, is the same tree seen from one class higher — Block is its only subclass, and it exists so that a block’s behaviour and its registry identity can be separate classes.

Item

The class hierarchy under Item: each branch shows how many types descend from it Item (67) BlockItem (9) StandingAndWallBlockItem (3) 3 with no subclasses 5 with no subclasses PotionItem (3) ThrowablePotionItem (2) 2 with no subclasses ArrowItem (2) 2 with no subclasses ProjectileWeaponItem (2) 2 with no subclasses BucketItem (1) MobBucketItem HangingEntityItem (1) ItemFrameItem 43 with no subclasses
The Item tree to three levels. Click to enlarge.

Sixty-seven types descend from Item, for over a thousand registered items. The tree is small because an item’s behaviour mostly is not in its class: what a stack does is in its data components, and Items registers most of the game as a plain Item or BlockItem with an Item.Properties describing it. Part VII’s items and stacks is why the tree is this shape.

Screen

The class hierarchy under Screen: each branch shows how many types descend from it Screen (161) AbstractContainerScreen (27) AbstractRecipeBookScreen (6) AbstractFurnaceScreen (3) 2 with no subclasses AbstractMountInventoryScreen (2) 2 with no subclasses ItemCombinerScreen (2) 2 with no subclasses 14 with no subclasses RealmsScreen (27) AbstractRealmsCodeScreen (3) 3 with no subclasses RealmsLongRunningMcoTaskScreen (1) RealmsLongRunningMcoConnectTaskScreen 21 with no subclasses OptionsSubScreen (11) 11 with no subclasses ConfirmScreen (6) ConfirmLinkScreen (1) PrivacyConfirmLinkScreen 4 with no subclasses DialogScreen (5) ButtonListDialogScreen (3) 3 with no subclasses SimpleDialogScreen AbstractReportScreen (3) 3 with no subclasses WarningScreen (3) 3 with no subclasses AbstractCommandBlockEditScreen (2) 2 with no subclasses AbstractGameRulesScreen (2) 2 with no subclasses AbstractSignEditScreen (2) 2 with no subclasses BookViewScreen (1) LecternScreen ChatScreen (1) InBedChatScreen 59 with no subclasses
The Screen tree to three levels; 59 of its 71 direct subclasses have no subclasses of their own. Click to enlarge.

Screen (161) is Block’s shape again — 71 direct subclasses, 59 of them terminal — with one deep branch, AbstractContainerScreen (27), the screens that show a menu (GUI and screens is Part X’s), and one branch that is not in this book, RealmsScreen (27). The row above it in the table, AbstractContainerEventHandler at 162, is Screen’s parent with Screen as its only subclass, the same one-class-higher effect as BlockBehaviour.

Two trees the table shows and the figures do not

Goal has 202 descendants from 99 direct subclasses, and 132 of the 202 are nested classes, all but one inside the mob they serve — a fox’s own goals are declared in Fox, not in world/entity/ai/goal. Packet is an interface with 240 descendants from 229 direct implementers, and almost nothing below them: the packet catalogue is flat, and the packets reference is its list.

The tables

Class roots first, then interface roots; each table is the thirty roots of its kind with the most descendants. direct is the number of immediate subclasses or implementers; where is the package of the root.

rootdescendantsdirectkindwhere
BlockBehaviour2981classnet/minecraft/world/level/block/state
Block29792classnet/minecraft/world/level/block
Goal20299classnet/minecraft/world/entity/ai/goal
Entity19218classnet/minecraft/world/entity
Model17014classnet/minecraft/client/model
AbstractContainerEventHandler1621classnet/minecraft/client/gui/components/events
Screen16171classnet/minecraft/client/gui/screens
EntityModel15371classnet/minecraft/client/model
EntityRenderer13328classnet/minecraft/client/renderer/entity
LivingEntity1243classnet/minecraft/world/entity
Mob1145classnet/minecraft/world/entity
PathfinderMob1085classnet/minecraft/world/entity
NamespacedSchema107107classnet/minecraft/util/datafix/schemas
EntityRenderState9923classnet/minecraft/client/renderer/entity/state
AbstractWidget9912classnet/minecraft/client/gui/components
LivingEntityRenderer963classnet/minecraft/client/renderer/entity
MobRenderer9339classnet/minecraft/client/renderer/entity
Particle844classnet/minecraft/client/particle
SingleQuadParticle7446classnet/minecraft/client/particle
LivingEntityRenderState7044classnet/minecraft/client/renderer/entity/state
Item6749classnet/minecraft/world/item
BaseEntityBlock6440classnet/minecraft/world/level/block
StructurePiece638classnet/minecraft/world/level/levelgen/structure
Entry632classnet/minecraft/client/gui/components
Behavior5853classnet/minecraft/world/entity/ai/behavior
BlockEntity5235classnet/minecraft/world/level/block/entity
AgeableMob524classnet/minecraft/world/entity
RenderLayer4839classnet/minecraft/client/renderer/entity/layers
NamedEntityFix4747classnet/minecraft/util/datafix/fixes
AgeableMobRenderer4526classnet/minecraft/client/renderer/entity
rootdescendantsdirectkindwhere
FeatureElement3867interfacenet/minecraft/world/flag
ItemLike3662interfacenet/minecraft/world/level
TabOrderedElement3342interfacenet/minecraft/client/gui/components
GuiEventListener3325interfacenet/minecraft/client/gui/components/events
Renderable2696interfacenet/minecraft/client/gui/components
TypedInstance2525interfacenet/minecraft/core
DebugValueSource2483interfacenet/minecraft/util/debug
Packet240229interfacenet/minecraft/network/protocol
SlotProvider2312interfacenet/minecraft/world/entity
ContainerEventHandler2243interfacenet/minecraft/client/gui/components/events
Nameable2126interfacenet/minecraft/world
DataComponentGetter2004interfacenet/minecraft/core/component
ItemOwner1953interfacenet/minecraft/world/entity
UniquelyIdentifyable1941interfacenet/minecraft/world/level/entity
EntityAccess1931interfacenet/minecraft/world/level/entity
ScoreHolder1931interfacenet/minecraft/world/scores
SyncedDataHolder1931interfacenet/minecraft/network/syncher
Validatable17422interfacenet/minecraft/world/level/storage/loot
LayoutElement1734interfacenet/minecraft/client/gui/layouts
StringRepresentable165164interfacenet/minecraft/util
NarrationSupplier1463interfacenet/minecraft/client/gui/narration
Waypoint1312interfacenet/minecraft/world/waypoints
Attackable1262interfacenet/minecraft/world/entity
WaypointTransmitter1251interfacenet/minecraft/world/waypoints
Leashable1212interfacenet/minecraft/world/entity
ParticleProvider119114interfacenet/minecraft/client/particle
Targeting1162interfacenet/minecraft/world/entity
EquipmentUser1151interfacenet/minecraft/world/entity
NarratableEntry1032interfacenet/minecraft/client/gui/narration
RenderLayerParent971interfacenet/minecraft/client/renderer/entity

Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

I · Anatomy

Verified against Minecraft 26.3 · Part I · The whole program at once: which threads exist, which loop each runs, and how the two halves talk.

Part I is the program the other twelve parts run inside. Java Minecraft is one codebase running as two programs — a server whose whole life is a tick loop, and a client whose life is a frame loop with ticks inside it — on four threads worth memorising: the Render thread, which is also the client’s game thread; the Server thread, which is the world; the Netty event loop, which carries the packets (Netty is the networking library both halves use); and one shared worker pool that chunk generation, the server’s lighting and section meshing all queue onto. A player recognises the split by its symptom, the game that keeps drawing while the world stands still, and the book returns to it wherever the two halves disagree, as when a broken block comes back for a moment. Read this part first for the plainest reason there is: nearly every lane in every sequence diagram after it is a class that runs on one of these threads, and the ten that are not a class are a thread, the process, the wire, the disk, the session service or the game’s own code.

The shape of the part

Two pages: what the program is, and where the book stops. The first is a trace and the second is a boundary, and the boundary is drawn second because a reader decides whether to go on once they can see the whole program.

flowchart LR
    A["1 · Anatomy<br/>a trace: four threads, two loops, one wire"]
    B["2 · What this book skips<br/>a boundary: where the book stops"]
    A -- "the program first" --> B

The part’s two lectures, numbered to the watch order. The arrow is a dependency, not a call: the boundary means nothing until you can see the program it is drawn around.

Before you start

Nothing. This is the first part, and it assumes only that you have played the game.

Watch in this order

  1. Anatomy — from main to a running singleplayer world: the Render thread that is also the game thread, the Server thread that is the world, the Netty threads that run more than bytes, and the one CPU pool that chunk generation, the server’s lighting and section meshing all share.
  2. What this book skips — the boundary, drawn honestly on the treemap of the jar: save migration, Realms, telemetry, the profilers, the management server, the data generators, and where to start if you need one anyway. It is the second lecture and not the last because a boundary is worth drawing before you invest in thirteen parts, not after.

Where the part stops

Part I says which threads exist and hands the two loops and the wire on. What the Server thread does with a tick is Part III; what the frame loop does between ticks is Part X, and the frame itself Part XI; what crosses the wire between them is Part IX. This part owes you only enough of each to read a lane.

The part’s own packages are the two entry points, the client’s GameConfig and SilentInitException, and the two loop classes, and it leaves none of them unnamed: 0% of the part’s lines are named on no page in the book. The boundary that matters here is the book’s rather than the part’s, and drawing it is the second lecture’s whole job.

Reference this part uses

Diagram lanes — the abbreviations every sequence diagram uses, needed from the first figure of the first lecture onward. Threads — every thread, who makes it and what may run on it; Part I names the four to memorise and this is the rest. Naming drift — the 1.21-era names that have moved. The atlas — where everything is, and the treemap the second lecture reads.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Anatomy

Verified against Minecraft 26.3 · Part I · Clicking Singleplayer, picking a world, and standing in it a few seconds later.

A player clicks Singleplayer, picks a world from the list and waits. One thread has been running since main; by the time the world appears there are two that matter, and the second was created by the first, mid-frame, while the first went on drawing. They are two programs sharing a JVM. The server is the world — every chunk, entity and block, and the only thing allowed to change them; the client is a window, a frame loop and a copy of the world it is told about. Between them runs a Netty channel that never touches a socket, and over it the client walks the same handshake, login, configuration and play state machine it would walk against a server on the other side of the planet. The packets are real. What leaks between the two halves is not world state but a setting: pause is decided on the client, by Minecraft.isPaused, and enforced on the server, by IntegratedServer.tickServer running IntegratedServer.tickPaused instead of the world. The decision has a third term that nobody sees in the menu: the client only sets the flag while IntegratedServer.isPublished is false — which is why a world published to LAN never pauses, however deep in the options screen you are.

The cast

classwhat it decidesthread
Minecraftthe client: the Window, the resource system, the renderers, input and Options — and, in three fields, whether we are in a world at allRender
MinecraftServerthe world and the loop that advances it. Abstract, with three concrete subclasses: IntegratedServer, DedicatedServer, and GameTestServer, the headless harness the gametest entry point launchesServer
IntegratedServersingleplayer’s server: the pause, LAN publishing, the player cap, the relaxed limitsServer
BlockableEventLoopthe queue-and-thread pairing both loops are — Minecraft and MinecraftServer each extend ReentrantBlockableEventLoopone per queue, and a thread may own more than one
Connectionone channel, and which PacketListener is currently on itNetty
ServerConnectionListenerwhich channels the server listens on, including the in-memory one singleplayer usesthe binding thread (Render in singleplayer), into Netty
PacketProcessorwhich decoded packets are waiting to be handled on the thread that owns their statefilled from Netty, drained by the owner
Utilthe pools everything else is serialised onto: Util.backgroundExecutor, Util.ioPool, Util.nonCriticalIoPool—

Three of Minecraft’s nullable fields between them mean “we are in a world”: Minecraft.level (a ClientLevel), Minecraft.player (a LocalPlayer) and Minecraft.gameMode (a MultiPlayerGameMode). A fourth, Minecraft.singleplayerServer, holds the IntegratedServer when one is running, and is the client’s answer to “am I the host”.

From main to a world

sequenceDiagram
    box transparent the client
    participant Main as Main
    participant MC as Minecraft
    participant Conn as Connection
    end
    box transparent the server
    participant IS as IntegratedServer
    participant SCL as ServerConnection<br/>Listener
    end

    Main->>Main: tryDetectVersion, loadLibraries, bootStrap, validate
    Note over Main: from here this thread is the Render thread
    Main->>MC: the constructor: a backend, a Window, every reload listener
    MC->>MC: the first ReloadInstance, the LoadingOverlay on screen
    Main->>MC: run: pollEvents, then runTick, until running goes false
    Note over Main,Conn: one loop so far. The next line makes the second.
    MC->>IS: spin builds it on this thread, then starts the Server thread
    par on the Server thread
        IS->>IS: runServer: initServer loads the level
    and meanwhile, on the Render thread
        MC->>MC: doWorldLoad: draw a frame, drain the queue, until isReady
    end
    MC->>SCL: startMemoryChannel: a Netty local address, no socket
    MC->>Conn: connectToLocalServer: the client's end of that channel
    Conn->>Conn: handshake, login, configuration, play — the walk any client makes

The one-time start-up, and the line where one loop becomes two: in the split box the Render thread goes on drawing while IntegratedServer.initServer loads the level. Everything below it is the client dialling a server that happens to be in the same JVM.

That is the book’s first sequence diagram, and its lanes are abbreviated the way every later one is: two or more letters of a class name, one meaning throughout. The key is diagram lanes.

Five main methods, and this trace starts in one of them. client/main/Main is the client’s, server/Main the dedicated server’s, data/Main the data generator’s, client/data/Main the generated client assets’ — models, atlases, equipment assets, waypoint styles — and gametest/Main the one that launches GameTestServer. (The tree holds a sixth main, SnbtDatafixer, which converts files and starts nothing.) Each parses its own command line — the client’s into a GameConfig the Minecraft constructor is built from — and then reads its own settings file: options.txt through Options on the client and server.properties through DedicatedServerProperties on the dedicated server. The two generator entry points are build-time programs, and what this book skips says how far that is true.

Bootstrap before anything exists. SharedConstants.tryDetectVersion reads version.json as the first statement of every one of those main methods, before the option parser exists; NativeLibrariesBootstrap.loadLibraries unpacks the natives, CrashReport.preload warms the reporter, and Bootstrap.bootStrap builds and freezes the static registries — blocks, items, entity types, the things that cannot be data-driven because the data loader itself needs them (identifiers and registries is what the freeze proves). ClientBootstrap does the client-only equivalents between that and Bootstrap.validate, which checks the result. DataFixers.optimize is kicked off concurrently before the registries are built and joined much later.

The GPU backend is chosen in the constructor. RenderSystem.initBackendSystem runs first and returns SDL’s clock, which Minecraft installs through Util.setTimeSource — so Util’s clock, on the client and on a singleplayer server beside it, comes from the windowing library. Then a GpuBackend is chosen by trying candidates in an order Options sets, unless a launch argument names one, until one of them loads its library and makes a GpuDevice — there are two, GlBackend and VulkanBackend (the window) — and only then is the one Window made, through the backend that succeeded. From then on the renderer only ever sees the GpuDevice abstraction in com/mojang/renderpearl.

Construction registers, it does not load. The constructor creates each manager and registers it on the ReloadableResourceManager; the loading is one ReloadInstance whose prepare phases run on Util.backgroundExecutor and whose apply phases run on the Render thread, with the LoadingOverlay on screen. Pressing F3+T re-runs exactly that path — see the resource system.

Opening a world spins a server. Minecraft.doWorldLoad calls MinecraftServer.spin, which constructs the IntegratedServer on the caller’s thread before starting the new one (starting a server has that order in full); the new thread’s body is MinecraftServer.runServer, which calls IntegratedServer.initServer and enters the loop. Meanwhile Minecraft.doWorldLoad keeps the Render thread drawing frames and draining its own queue until MinecraftServer.isReady, waiting out each sixtieth of a second in BlockableEventLoop.managedBlock: a thread that blocks on this half of the game keeps running that half’s queue while it waits, which is the reason the wait cannot deadlock.

The client connects like any other client. ServerConnectionListener.startMemoryChannel binds a Netty local address and Connection.connectToLocalServer connects to it; the client then walks handshake, login, configuration and play as it would against a remote server, with ClientHandshakePacketListenerImpl as its login listener. The server gives the local channel a handshake listener of its own, MemoryServerHandshakePacketListenerImpl, and skips the encryption request and compression it would send a remote client. Almost nothing in the play path knows it is singleplayer: the exceptions are the handful of places that ask Connection.isMemoryConnection or ServerCommonPacketListenerImpl.isSingleplayerOwner directly, among them ClientPacketListener.handleUpdateTags, which skips applying the tags it was sent because the server’s registries are already the client’s. The connection has what the local channel does and does not install, and protocol phases is the walk in full.

Two loops, and a wire between them

The client’s loop is a frame loop with ticks inside it; the server’s is a tick loop with no frames at all. They are not the same shape, and no page later in this book is readable until that difference is fixed in mind.

flowchart TD
    subgraph Client["the Render thread"]
        CR["Minecraft.run: RenderSystem.pollEvents,<br/>then Minecraft.runTick, once per frame"]:::client
        subgraph Frame["Minecraft.runTick"]
            CD["the DeltaTracker counts the whole ticks owed"]:::client --> CP["PacketProcessor.processQueuedPackets"]:::client
            CP --> CQ["BlockableEventLoop.runAllTasks"]:::client
            CQ --> CT["Minecraft.tick, 0 to 10 times"]:::client
            CT --> CF["Minecraft.renderFrame, on the partial tick"]:::client
        end
        CR --> CD
        CF --> CR
    end
    subgraph Server["the Server thread"]
        SR["MinecraftServer.runServer: the deadline is set first"]:::server
        subgraph STick["one server tick"]
            SP["PacketProcessor.processQueuedPackets"]:::server --> SS["MinecraftServer.tickServer: every ServerLevel, then the connections"]:::server
        end
        SW["MinecraftServer.waitUntilNextTick:<br/>run queued tasks, then park"]:::server
        SR --> SP
        SS --> SW
        SW --> SR
    end

The two loops side by side: the Render thread’s ring is a frame with a tick inside it, and the Server thread’s ring is the tick with no frame anywhere in it. The inner box in each is the call that holds the tick — Minecraft.runTick on one side, MinecraftServer.processPacketsAndTick on the other.

The frame loop. Minecraft.run polls SDL events and calls Minecraft.runTick once per frame, as fast as vsync or the frame-rate limit allow. Inside each frame a DeltaTracker.Timer running at twenty ticks a second says how many whole game ticks have elapsed since the last frame — usually zero or one, at most ten are run — and Minecraft.tick is called that many times, with BlockableEventLoop.runAllTasks draining the thread’s own queue just before them. The fractional remainder is the partial tick the renderers read, to interpolate with, from the DeltaTracker that Minecraft.renderFrame hands them. So the client has a 20 Hz tick, but it is a sub-step of the frame loop rather than a loop of its own; the client loop is the arithmetic in detail.

The tick loop. MinecraftServer.runServer re-reads this tick’s length every iteration from TickRateManager.nanosecondsPerTick — 50 ms by default, whatever /tick rate says otherwise, and zero under /tick sprint — and calls MinecraftServer.processPacketsAndTick, which drains the PacketProcessor and then runs MinecraftServer.tickServer. Afterwards MinecraftServer.waitUntilNextTick spends the slack running queued tasks and then parks until the next tick is due. There is no frame and no partial tick here at all. The “Can’t keep up!” warning is MinecraftServer.runServer’s own, decided before either call from how far behind the deadline already is; the server tick owns what is inside them — the tick budget, the deferrable work and the flush bracket around outbound packets.

Both are event loops first and game loops second. Minecraft and MinecraftServer both extend ReentrantBlockableEventLoop — the same base class, not an analogy — so each is an Executor whose queue drains on its own thread, and another thread that wants to touch that half’s state submits a task, or leaves its work in one of the queues beside the loop’s own that the owning thread drains — a packet in the PacketProcessor (below), a dedicated server’s console line in a list DedicatedServer.tickConnection drains, a level’s chunk work in its ServerChunkCache.MainThreadExecutor (below). BlockableEventLoop.managedBlock is the blocking form, and the reason the owning thread can wait for a future without deadlocking: it keeps draining its own queue while it waits.

A packet is decoded on one thread and handled on another. A packet arrives on a Netty IO thread and Connection.channelRead0 hands it to the current PacketListener; a handler that touches game state calls PacketUtils.ensureRunningOnSameThread, which, when it is off-thread, queues the packet on the owning side’s PacketProcessor instead of running it — the connection is that crossing in both directions. What matters here is when the queue is drained, because the two loops do not agree: first thing in MinecraftServer.processPacketsAndTick, but on the client early in Minecraft.runTick, after the delta tracker advances and before the ticks — so a client at 200 frames a second takes the server’s packets ten times more often than it ticks.

Four threads worth memorising

threadmade byrunsnotes
Render threadthe JVM main thread, renamed in client/main/MainMinecraft.runAlso the client’s game thread: Minecraft.gameThread is this thread. Priority 10 on machines with more than four processors.
Server threadMinecraftServer.spinMinecraftServer.runServerOne per server, so singleplayer has exactly one. Priority 8, on the same more-than-four-processors condition.
Netty IOEventLoopGroupHolderthe Connection pipelineNamed Netty NIO IO #n — Epoll or Kqueue when native transport is on, Netty Local IO #n for the in-process singleplayer channel. Decode, decrypt, decompress — and, unlike the play phase, the handshake and login handlers, which never call PacketUtils.ensureRunningOnSameThread; the first handler that hops is in configuration. The login state machine is still advanced from the Server thread, because ServerLoginPacketListenerImpl is a TickablePacketListener and MinecraftServer.tickConnection ticks it.
Worker-Main-nUtil.backgroundExecutora ForkJoinPool sized to the JDK’s available-processor count minus oneUtil.maxAllowedExecutorThreads clamps it, and Util.getMaxThreads reads a max.bg.threads system property that overrides the ceiling. The shared CPU pool: chunk generation and lighting (ChunkMap through ChunkTaskDispatcher), section meshing (SectionRenderDispatcher), resource-reload prepare phases, chunk serialisation.

That is the set worth memorising, not the set that exists. The IO workers, the sound engine’s event loop, the dedicated server’s watchdog, console, RCON, query and management threads, the timer hack thread and the situational ones — authentication, chat filtering, server pinging, telemetry, world upgrades, the shutdown hooks — are all in Threads, with who makes each and what it is allowed to touch.

There is no fifth thread hiding on the client. The Render thread is the game thread: Minecraft.gameThread and the thread RenderSystem guards with RenderSystem.assertOnRenderThread are the same one, and there is no initGameThread and no isOnGameThread anywhere in the tree — only RenderSystem.isOnRenderThread. A slow client tick costs frames directly. What that thread does with a world is animate and predict one (Minecraft.tick calls ClientLevel.tickEntities, and block entities tick too), but nothing it concludes is authoritative, and the server’s packets overwrite whatever the prediction got wrong — which is a claim about the world, and the client’s own player is the exception the book spends Part VIII and prediction and acknowledgement on.

Everything else that matters is serialised onto a pool rather than given a thread. ConsecutiveExecutor and PriorityConsecutiveExecutor, in util/thread, are the mechanism: a queue that promises to run its tasks one at a time on a pool that otherwise runs many, which is how “worldgen” and “light” stay ordered on the worker pool, and how the IOWorker stays ordered on Util.ioPool — which is its own pool of IO-Worker-n threads, not one of the four. And ServerChunkCache.MainThreadExecutor is a further event loop layered on the server thread, which is why a tick that waits on a chunk does not deadlock the chunk that needs the tick.

What singleplayer shares by direct call

Every block, entity and inventory change a player makes crosses as a packet, even in one process. But the two halves share a JVM, and a handful of things do cross by direct call — settings, debug state and the options screen’s own updates — and a setting can reach the world before the call returns. The server reads Minecraft.isPaused every tick and the client’s render and simulation distances every tick it runs the world; WorldOptionsScreen.applyChanges calls IntegratedServer.publishServer and its siblings straight from the Render thread, and among them IntegratedServer.setPersonalGameType sets the host’s own game mode through ServerPlayer.setGameMode on the Render thread, not the Server thread; and IntegratedServer.latestTicksGizmos is a volatile list the server thread writes and the client reads. Treat “everything crosses as a packet” as a rule about play, not about the process.

Singleplayer differs in more than pausing, too. Beyond the pause and the distances following Options, IntegratedServer caps the player list at eight, owns LAN publishing and the LanServerPinger, drops the chat and command spam thresholds to zero where a dedicated server defaults to ten, takes native transport from the client’s own option rather than a server property, and answers the operator-permission questions differently.

A crash in singleplayer surfaces on the wrong half

Neither loop throws. Both catch everything and wrap it in a CrashReport, and a background thread that dies parks its report for a loop to pick up (how a server dies owns that relay, and the client loop the client’s own three exits). The asymmetry is Part I’s, because it follows from two programs sharing a JVM: only a loop constructed to propagate crashes rethrows a parked report, and IntegratedServer is not one. So a worker that dies doing the server’s work in singleplayer takes down the client, and the crash report names a thread the player was not watching.

Questions players ask

Does a dedicated server pause? Yes — an empty server stops ticking on its own (the server tick has the counter and what still runs). Pausing is not a singleplayer concept; only the client-decides-it half is.

Is twenty ticks a second a constant? No, it is a server field. The TickRateManager.nanosecondsPerTick the loop re-reads lives on a ServerTickRateManager, the server’s subclass of it, which adds the sprint /tick manipulates and sends clients the rate and the freeze; the client mirrors those two in ClientLevel, so the DeltaTracker slows and freezes with the server.

Does a busy server skip work? Less than you would think. The tick budget is one predicate, MinecraftServer.haveTime, travelling from MinecraftServer.tickServer down through every level, and what it actually gates is a short list that does not include loading or generating a chunk. The server tick has that list, and the sprint’s inverted effect on it.

For a 1.21-era reader. The client’s clock is DeltaTracker, which was Timer, and the partial tick is a DeltaTracker.Timer you ask rather than a float you are handed — it appears in the frame loop above and in every frame’s extract in Part XI. GLFW is gone; SDL makes the window, delivers its events and supplies the time source. The rest of the drift a 1.21 reader will trip on is naming drift.

Where to look

client/main/Main · Minecraft · server/Main · MinecraftServer · IntegratedServer · BlockableEventLoop · Util (the executors) · EventLoopGroupHolder · ServerConnectionListener · Connection · PacketProcessor · PacketUtils


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What this book skips

Verified against Minecraft 26.3 · Part I · A reader opens the atlas, sees fourteen packages hatched, and asks what is in them and why no part counts them.

Open the atlas and part of the jar is drawn hatched. That hatching is this page. Java Minecraft is 7,301 source files and 741,069 lines, and the parts do not reach all of it. Some of what is left out is excluded on purpose by the newest-version-only rule — save migration is version-difference code, and a book that documents only the current version has nothing to say about it. Some is out of scope because it is a client for a service this book cannot read. And one hatched box is not skipped code so much as skipped ground: net/minecraft/data is the program that writes vanilla’s own content as a data pack, it ships in the dedicated server jar — all 174 files of it — and the running game compiles against it and calls into it. Blocks names TreeFeatures keys while it constructs mushroom blocks; MinecraftServer reaches for a MiscOverworldFeatures key for the bonus chest; the F3 screen’s biome-builder line runs through NoiseRouterData.peaksAndValleys into TerrainProvider. A boundary drawn honestly has to show where it leaks, and that is the largest leak in it.

The 26.3 jar as a treemap of packages: area is lines of decompiled source, colour is which jar ships the package, hatching marks the packages what this book skips tours net/minecraft/world — 2715 classes, 313,098 lines, shared net/minecraft/world/level — 1418 classes, 151,146 lines, shared level 151,146 net/minecraft/world/entity — 727 classes, 110,760 lines, shared entity 110,760 net/minecraft/world/item — 323 classes, 30,661 lines, shared item 30,661 net/minecraft/world/inventory — 64 classes, 7,152 lines, shared inventory 7,152 net/minecraft/world/phys — 28 classes, 3,036 lines, shared phys net/minecraft/world/attribute — 30 classes, 1,936 lines, shared net/minecraft/world — 25 classes, 1,686 lines, shared net/minecraft/world/scores — 16 classes, 1,442 lines, shared net/minecraft/world/effect — 20 classes, 1,306 lines, shared net/minecraft/world/damagesource — 12 classes, 936 lines, shared net/minecraft/world/ticks — 14 classes, 869 lines, shared net/minecraft/world/waypoints — 9 classes, 677 lines, shared net/minecraft/world/timeline — 5 classes, 446 lines, shared net/minecraft/world/clock — 11 classes, 390 lines, shared net/minecraft/world/flag — 7 classes, 356 lines, shared net/minecraft/world/food — 6 classes, 299 lines, shared world 42% net/minecraft/client — 1891 classes, 176,818 lines, client-only net/minecraft/client/gui — 445 classes, 59,594 lines, client-only gui 59,594 net/minecraft/client/renderer — 715 classes, 50,649 lines, client-only renderer 50,649 net/minecraft/client/model — 268 classes, 12,712 lines, client-only model 12,712 net/minecraft/client/multiplayer — 63 classes, 11,303 lines, client-only multiplayer 11,303 net/minecraft/client — 41 classes, 10,821 lines, client-only (itself) 10,821 net/minecraft/client/resources — 110 classes, 8,326 lines, client-only resources 8,326 net/minecraft/client/particle — 88 classes, 6,839 lines, client-only particle 6,839 net/minecraft/client/data — 28 classes, 6,189 lines, client-only — toured in what this book skips data 6,189 net/minecraft/client/sounds — 17 classes, 2,174 lines, client-only sounds net/minecraft/client/player — 11 classes, 2,072 lines, client-only player net/minecraft/client/telemetry — 18 classes, 1,221 lines, client-only — toured in what this book skips net/minecraft/client/server — 6 classes, 885 lines, client-only net/minecraft/client/color — 17 classes, 879 lines, client-only net/minecraft/client/tutorial — 10 classes, 671 lines, client-only net/minecraft/client/main — 4 classes, 523 lines, client-only net/minecraft/client/animation — 23 classes, 509 lines, client-only net/minecraft/client/searchtree — 8 classes, 505 lines, client-only net/minecraft/client/input — 8 classes, 304 lines, client-only net/minecraft/client/quickplay — 3 classes, 284 lines, client-only net/minecraft/client/entity — 4 classes, 228 lines, client-only net/minecraft/client/profiling — 2 classes, 81 lines, client-only net/minecraft/client/waypoints — 2 classes, 49 lines, client-only client 24% net/minecraft/util — 735 classes, 54,700 lines, shared net/minecraft/util/datafix — 411 classes, 27,048 lines, shared — toured in what this book skips datafix 27,048 net/minecraft/util — 93 classes, 12,767 lines, shared (itself) 12,767 net/minecraft/util/profiling — 70 classes, 4,265 lines, shared — toured in what this book skips profiling 4,265 net/minecraft/util/filefix — 57 classes, 3,553 lines, shared — toured in what this book skips filefix net/minecraft/util/parsing — 29 classes, 1,879 lines, shared net/minecraft/util/debug — 19 classes, 1,348 lines, shared net/minecraft/util/worldupdate — 6 classes, 958 lines, shared net/minecraft/util/thread — 9 classes, 842 lines, shared net/minecraft/util/valueproviders — 19 classes, 707 lines, shared net/minecraft/util/eventlog — 4 classes, 459 lines, shared net/minecraft/util/random — 4 classes, 374 lines, shared net/minecraft/util/debugchart — 8 classes, 199 lines, shared net/minecraft/util/context — 4 classes, 181 lines, shared net/minecraft/util/monitoring — 2 classes, 120 lines, shared util 7% net/minecraft/server — 432 classes, 53,789 lines, shared net/minecraft/server/commands — 111 classes, 13,357 lines, shared commands 13,357 net/minecraft/server/level — 42 classes, 12,166 lines, shared level 12,166 net/minecraft/server/network — 29 classes, 5,566 lines, shared network 5,566 net/minecraft/server — 27 classes, 5,153 lines, shared (itself) 5,153 net/minecraft/server/packs — 57 classes, 5,122 lines, shared packs 5,122 net/minecraft/server/jsonrpc — 65 classes, 4,118 lines, shared — toured in what this book skips jsonrpc 4,118 net/minecraft/server/players — 19 classes, 2,770 lines, shared players net/minecraft/server/dedicated — 7 classes, 1,811 lines, shared net/minecraft/server/dialog — 35 classes, 885 lines, shared net/minecraft/server/rcon — 9 classes, 837 lines, shared — toured in what this book skips net/minecraft/server/permissions — 12 classes, 402 lines, shared net/minecraft/server/chase — 3 classes, 354 lines, shared net/minecraft/server/notifications — 5 classes, 343 lines, shared net/minecraft/server/bossevents — 3 classes, 328 lines, shared net/minecraft/server/gui — 4 classes, 310 lines, shared net/minecraft/server/waypoints — 2 classes, 180 lines, shared net/minecraft/server/advancements — 2 classes, 87 lines, shared server 7% net/minecraft/network — 416 classes, 23,319 lines, shared net/minecraft/network/protocol — 298 classes, 12,872 lines, shared protocol 12,872 net/minecraft/network/chat — 63 classes, 4,799 lines, shared chat 4,799 net/minecraft/network — 42 classes, 3,590 lines, shared net/minecraft/network/codec — 7 classes, 1,533 lines, shared net/minecraft/network/syncher — 6 classes, 525 lines, shared network 3% com/mojang/renderpearl — 146 classes, 18,659 lines, client-only com/mojang/renderpearl/backend — 76 classes, 13,866 lines, client-only backend 13,866 com/mojang/renderpearl/frontend — 12 classes, 2,876 lines, client-only frontend com/mojang/renderpearl/api — 53 classes, 1,879 lines, client-only api com/mojang/renderpearl/util — 4 classes, 34 lines, client-only com/mojang/renderpearl — 1 classes, 4 lines, client-only renderpearl net/minecraft/data — 174 classes, 16,849 lines, shared net/minecraft/data/worldgen — 64 classes, 6,016 lines, shared — toured in what this book skips worldgen 6,016 net/minecraft/data/loot — 21 classes, 3,023 lines, shared — toured in what this book skips loot 3,023 net/minecraft/data/recipes — 20 classes, 2,507 lines, shared — toured in what this book skips net/minecraft/data/tags — 29 classes, 1,756 lines, shared — toured in what this book skips tags net/minecraft/data — 10 classes, 1,272 lines, shared — toured in what this book skips net/minecraft/data/advancements — 10 classes, 896 lines, shared — toured in what this book skips net/minecraft/data/info — 8 classes, 518 lines, shared — toured in what this book skips net/minecraft/data/structures — 5 classes, 473 lines, shared — toured in what this book skips net/minecraft/data/registries — 5 classes, 325 lines, shared — toured in what this book skips net/minecraft/data/metadata — 2 classes, 63 lines, shared — toured in what this book skips data 2% com/mojang/realmsclient — 137 classes, 14,701 lines, client-only com/mojang/realmsclient/gui — 46 classes, 7,091 lines, client-only — toured in what this book skips gui 7,091 com/mojang/realmsclient/client — 19 classes, 2,643 lines, client-only — toured in what this book skips client com/mojang/realmsclient/dto — 43 classes, 1,986 lines, client-only — toured in what this book skips dto com/mojang/realmsclient — 4 classes, 1,492 lines, client-only — toured in what this book skips com/mojang/realmsclient/util — 20 classes, 1,425 lines, client-only — toured in what this book skips util com/mojang/realmsclient/exception — 5 classes, 64 lines, client-only — toured in what this book skips realmsclient net/minecraft/commands — 124 classes, 13,229 lines, shared net/minecraft/commands/arguments — 73 classes, 9,017 lines, shared arguments 9,017 net/minecraft/commands — 14 classes, 1,921 lines, shared net/minecraft/commands/synchronization — 12 classes, 905 lines, shared net/minecraft/commands/execution — 18 classes, 756 lines, shared net/minecraft/commands/functions — 7 classes, 630 lines, shared commands 2% net/minecraft/core — 123 classes, 12,359 lines, shared net/minecraft/core — 39 classes, 5,499 lines, shared (itself) 5,499 net/minecraft/core/component — 32 classes, 3,067 lines, shared net/minecraft/core/registries — 14 classes, 1,525 lines, shared net/minecraft/core/dispenser — 14 classes, 1,098 lines, shared net/minecraft/core/particles — 21 classes, 863 lines, shared net/minecraft/core/cauldron — 3 classes, 307 lines, shared core 2% com/mojang/blaze3d — 86 classes, 10,437 lines, client-only com/mojang/blaze3d/platform — 26 classes, 4,382 lines, client-only com/mojang/blaze3d/vertex — 14 classes, 2,322 lines, client-only vertex com/mojang/blaze3d/audio — 12 classes, 1,013 lines, client-only — toured in what this book skips com/mojang/blaze3d/systems — 5 classes, 773 lines, client-only com/mojang/blaze3d/framegraph — 3 classes, 437 lines, client-only com/mojang/blaze3d/font — 7 classes, 432 lines, client-only com/mojang/blaze3d/pipeline — 5 classes, 366 lines, client-only com/mojang/blaze3d — 5 classes, 289 lines, client-only com/mojang/blaze3d/buffers — 3 classes, 228 lines, client-only com/mojang/blaze3d/resource — 6 classes, 195 lines, client-only blaze3d 1% net/minecraft/nbt — 43 classes, 7,791 lines, shared net/minecraft/nbt — 36 classes, 7,279 lines, shared (itself) 7,279 net/minecraft/nbt/visitors — 7 classes, 512 lines, shared nbt 1% net/minecraft/advancements — 115 classes, 7,647 lines, shared net/minecraft/advancements/predicates — 53 classes, 3,450 lines, shared net/minecraft/advancements/triggers — 49 classes, 2,816 lines, shared net/minecraft/advancements — 13 classes, 1,381 lines, shared net/minecraft/gametest — 48 classes, 5,756 lines, shared net/minecraft/gametest/framework — 46 classes, 5,737 lines, shared net/minecraft/gametest — 2 classes, 19 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared net/minecraft/sounds — 6 classes, 2,124 lines, shared sounds net/minecraft/tags — 29 classes, 1,874 lines, shared net/minecraft/tags — 29 classes, 1,874 lines, shared tags net/minecraft — 19 classes, 1,665 lines, shared net/minecraft — 19 classes, 1,665 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared net/minecraft/references — 5 classes, 1,483 lines, shared — toured in what this book skips net/minecraft/resources — 12 classes, 1,473 lines, shared net/minecraft/resources — 12 classes, 1,473 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared com/mojang/math — 10 classes, 1,054 lines, shared math net/minecraft/stats — 10 classes, 874 lines, shared net/minecraft/stats — 10 classes, 874 lines, shared — toured in what this book skips net/minecraft/gizmos — 15 classes, 569 lines, shared net/minecraft/gizmos — 15 classes, 569 lines, shared — toured in what this book skips net/minecraft/recipebook — 3 classes, 351 lines, shared net/minecraft/locale — 3 classes, 247 lines, shared net/minecraft/realms — 4 classes, 203 lines, client-only in both jars client only hatched: toured in what this book skips 741,069 lines, 7301 classes — area is lines of decompiled source
The jar by package, area by lines of decompiled source. Hatched boxes are the packages this page tours, which no part counts. Click to enlarge.

The sizes, and which jar ships them

Each entry below says what the thing is, roughly how big it is, whether the dedicated server ships it, one fact worth knowing, and where to start reading. The treemap deliberately does not hatch net/minecraft/gametest: Part XIII covers it, so it is a gap that closed rather than a skip. It cannot hatch player reporting either, which is why the table has fifteen top-level rows against the map’s fourteen hatched packages: the map’s smallest box is a package four levels deep, and net/minecraft/client/multiplayer/chat/report is six, sitting inside a package Parts IX and X between them own. The counts in the tables are files unless a cell says classes, so a package-info.java counts as one there and wherever the prose quotes the tables; a cell or a sentence that counts a package’s working classes leaves it out.

packagefileslinesside
net/minecraft/util/datafix41127,048both
net/minecraft/util/filefix573,553both
net/minecraft/client/telemetry181,221client
net/minecraft/util/profiling704,265both
net/minecraft/server/jsonrpc654,118dedicated server
net/minecraft/server/rcon9837dedicated server
com/mojang/realmsclient13714,701client
net/minecraft/realms4203client
net/minecraft/stats10874both
net/minecraft/gizmos15569both
net/minecraft/references51,483both
net/minecraft/data17416,849both — see below
└ net/minecraft/data/worldgen646,016both — see below
net/minecraft/client/data286,189client
com/mojang/blaze3d/audio121,013client
net/minecraft/client/multiplayer/chat/report12952client

The oracle for “server or client” throughout is the list of classes the dedicated server jar ships, which lives beside the decompile. It answers exactly one question — does the dedicated server have this class — so it can prove “client-only” and it can prove “both jars”, and it cannot prove “dedicated server only”. Two rows are labelled that way on the strength of a different check: nothing under net/minecraft/client or com/mojang/blaze3d references them. rcon is reached from DedicatedServer alone; jsonrpc from six files, and two of them — BuiltInRegistries and Registries — are classes the client loads at bootstrap, so the package’s types are on the client and the server it configures is not.

Save migration, and the fixer that moves files

net/minecraft/util/datafix — the largest thing on this page and the most explicitly out of scope. DataFixers is one static class whose whole body is the migration history of the game written out longhand: just over three hundred schema registrations and four hundred-odd fixes, from schema 99 up to the current world version. The rewriting machinery itself is Mojang’s external DataFixerUpper library; what lives here is the vanilla catalogue — util/datafix/schemas describing the shape of the data at each version, and util/datafix/fixes doing the individual rewrites.

A version number becomes a chain of fixes through DataFixTypes, an enum of about thirty type references (level, chunk, player, entity chunk, POI chunk, options, stats, advancements, and a long tail of saved-data kinds). DataFixTypes.updateToCurrentVersion takes the data version as an argument — every one of its fourteen callers reads the version itself — and asks the fixer to compose every rule from there to now. DataFixTypes.wrapCodec is the one that reads the version out of the tag: it wraps an ordinary codec so decoding pulls the data version, runs the chain, and encoding stamps the current version back in. It is the rarer door — two callers, PlayerAdvancements and DebugScreenEntryList — while player data takes the first one and reads the version itself, and chunk storage reads it too and takes a third, DataFixTypes.update. The rules are pre-compiled on a dedicated bootstrap thread, and that thread is built with some care to stay out of the way: one thread, daemon, at minimum priority, with a single caller in the client’s entry point, optimising exactly one type (the level-summary schema, so the world list opens fast). The dedicated server never asks for it at all.

net/minecraft/util/filefix does what the other cannot. A data fixer rewrites the contents of a tag after it has been read, so it can never move, rename, split or delete a file. FileFixerUpper operates on the world directory: its operations are moves, regex moves, group moves, deletions, content modifications and one composite that scopes a nested list of operations to matching folders, and the concrete fixes do things like relocate dimension storage, gather player storage into one folder and pull data out of level.dat into saved data (level data and rules).

It stays safe by working somewhere else: the whole upgrade runs against a custom copy-on-write file system over the world folder, whose writes land in a scratch directory, and the result is swapped in at the end. Exactly how safe depends on the filesystem underneath. Where hard links are available it uses them. Where they are not, it writes one file, upgrade_in_progress.json, recording the moves, and an upgrade cut short by a crash resumes from it the next time the world is opened, while one that fails on an error tries to move its files back. And where atomic move is unavailable it stops before the swap rather than risk a half-moved world.

The client does not grey out a world that needs the upgrade — it relabels the button. LevelSummary.primaryActionMessage turns Play into Upgrade and Play while leaving it active; what is disabled is Edit and Recreate, which would otherwise touch a directory the fixer is about to rearrange.

Telemetry writes to disk before it writes to the network

net/minecraft/client/telemetry, client-only. Exactly seven event types: world loaded, world unloaded, graphics capabilities (which now carries the backend name and the reason a backend failed — see Blaze3D), and four opt-in ones covering performance metrics, world load times, advancements and game load times. TelemetryProperty is the vocabulary; each property carries both an internal name and an export key.

Opting out is two-tier, and the game controls only one of the tiers. Minecraft.allowsTelemetry reads an account-level flag the game only reports; the in-game control only chooses whether the four opt-in events are sent, and is only offered when the account carries the flag that allows it. Everything sent is also written locally as a JSON event log with a seven-day expiry — and the send waits on the log opening, so a log that cannot be opened suppresses the send. A player can read their own outgoing telemetry, though not in the game: the telemetry screen renders the catalogue of event types and their properties, and a button next to it opens the log directory in the platform’s file manager.

Start at ClientTelemetryManager, TelemetryEventType.

Four profiling systems, and Tracy is the surprise

net/minecraft/util/profiling holds four profiling systems. Two sit in the package itself — the tick profiler and the Tracy bridge — and two in subpackages of it, jfr and metrics.

The tick profiler is the familiar one: Profiler is a thread-local holder of a ProfilerFiller, ActiveProfiler records the push/pop tree of named sections that pages in this book quote, and /perf and the client’s F3 profiler chart drive it.

Tracy is the surprise, and it is one class bridging out to Mojang’s Tracy binding. TracyZoneFiller implements the same interface, and Profiler.get falls back to the Tracy filler rather than the inactive one when Tracy is available — and Profiler.decorateFiller combines the two, so an attached Tracy build and a running /perf both see every section. With a Tracy build attached, every profiler section in the game streams out with no command run. Tracy reaches outside this package too, into Blaze3D’s frame capture, into the GPU abstraction’s TracyGpuProfiler and into the executor wrappers.

JFR (util/profiling/jfr) registers ten custom flight-recorder events under a Minecraft category — chunk generation, region reads and writes, packets sent and received, network summaries, server tick time, client FPS, structure generation, world load. Packet events are emitted straight from the packet codecs, so a recording gives a per-packet-type byte breakdown that this book’s packet reference cannot. Start with –jfrProfile or /jfr start.

Metrics (util/profiling/metrics) is /perf: sampling by nine MetricCategory values — pathfinding, event loops, consecutive executors, the tick loop, JVM, chunk rendering, chunk-rendering dispatch, CPU and GPU — written out as CSVs that PerfCommand zips.

The management server is not RCON

net/minecraft/server/jsonrpc, dedicated server only, and genuinely new. It is JSON-RPC 2.0 over a WebSocket, served by its own Netty bootstrap with an HTTP codec, an authentication handler, the WebSocket handshake and optional TLS. It is disabled by default; when enabled, TLS is on unless explicitly turned off, and the server refuses to start without a forty-character alphanumeric secret, generating one if absent.

What it exposes is the administrator’s surface, not the game’s: allow-list, bans and IP bans, players and kicks, operators, game rules, server status, save and stop, system messages, and a family of live server settings — including the idle-pause window, whose behaviour belongs to the server tick, because a dedicated server pauses too. Implementations sit behind service interfaces so the wire layer never touches the server object directly, and an executor service marshals calls onto the server thread.

The description of the API cannot drift from the handlers, because it is derived from them: every method is registered with a description and typed parameter and response schemas, and a discovery method returns an OpenRPC 1.3.2 document built by walking the two method registries and filtering on a per-method discoverable flag. There is also an outgoing direction for server-initiated notifications. The audience is panel and hosting operators.

Start at JsonRpc, ManagementServer.

RCON, query, and the pre-1.7 ping that removes itself

net/minecraft/server/rcon is seven classes of pre-Netty blocking socket code on its own threads. RconThread accepts the connections and RconClient speaks Valve’s Source RCON framing; commands execute as a RconConsoleSource, a command source that accumulates output into a string rather than a chat feed (Brigadier and commands). QueryThreadGs4 speaks the GameSpy4 UDP query protocol with a challenge-token handshake and a five-second cache on its full-status reply.

The pre-1.7 ping is not in that package. LegacyQueryHandler sits in the server’s network package and is installed into the Netty pipeline before the length-prefix splitter and the packet codec, right after the read timeout (the connection). It peeks at the first byte; if it is the legacy ping marker it answers in the old format and closes, and otherwise it resets the reader index, removes itself from the pipeline, and re-fires the bytes downstream. It costs one byte comparison per connection and then vanishes. The same encoding is used client-side so the server list can still ping ancient servers.

Realms is a client for a server nobody here can read

com/mojang/realmsclient, client-only, 137 files and 14,701 lines — more lines than the whole packet catalogue in network/protocol. Roughly sixty per cent is screens and the records behind them — subscriptions, world slots, templates, invites, backups, minigames, upload and download — and the rest is a task framework, the world-upload pipeline and the HTTP layer, a list of REST paths with a small request wrapper. net/minecraft/realms is three classes and a package-info.java vanilla keeps for Realms, RealmsScreen among them, the base the Realms screens extend; beyond it the Realms UI extends ordinary vanilla screens and widgets.

Out of scope because it is a service client: its behaviour is defined by a server this book cannot read. One fact anyway. The environment is chosen from an environment variable falling back to a system property, defaulting to production, in a static final field of the release client — and there are three environments, not two, because the third points at localhost.

Statistics, the scoreboard, and the recipe book

net/minecraft/stats is nine classes covering two concerns, plus a link into a third package that is the reason it is worth a paragraph.

Statistics: Stats declares eight registry-backed stat types — mined, crafted, used, broken, picked up, dropped, killed, killed by — plus a custom type holding the seventy-odd hand-declared counters (play time, distances by every mode of travel, damage dealt and blocked), each bound to a formatter that affects display only. A stat type is a lazily-populated map over a registry, so stat objects are interned; ServerStatsCounter adds the per-player file and a dirty set, and only dirty stats are sent. Statistics are one of two parts of the save that go through the data fixer as JSON rather than NBT — the other is advancement progress (advancements), and they are the only two.

The scoreboard link is why the package is worth a paragraph — and note that the class doing the linking is not in it: ObjectiveCriteria lives in net/minecraft/world/scores/criteria, which is why a scoreboard objective can name a statistic and why this package is reachable from a command at all (scores, teams and stored data is how the name is parsed).

The recipe book is the second concern, and it is in this package for historical reasons rather than architectural ones — RecipeBook, RecipeBookSettings and ServerRecipeBook are not skipped, they belong to recipes, with advancements reaching in from the other side. The address is the only thing surprising about them.

Two packages nobody will recognise

net/minecraft/gizmos is a debug-drawing API, in the game-engine sense of the word: the immediate-mode “draw me a box in the world for one frame” facility most engines have and Minecraft did not. Every one of the debug renderers — chunk borders, hitboxes, pathfinding, brains, points of interest, raids, light sections — is now written against it, and it is not client-only, which is the surprise. It is taught where its output is, in Part X, by debugging the running game; the address is what belongs here, because a reader hunting for the debug renderers’ drawing code will look under client/renderer/debug and find nothing that draws.

net/minecraft/references is not “references” in the data-fixer sense. It is a set of id-constant tables, and the split is not the one the package names suggest: BlockIds holds the keys for blocks with no item form (water, lava, wall torches, piston heads, wall signs), ItemIds the items with no block, and BlockItemIds — seven times BlockIds and not quite twice ItemIds — the pairs. Look for stone in BlockIds and it is not there. They exist to break a class-initialisation cycle: exactly twelve files outside the package name it, and four of them are the ones that need to name a block or item before the block and item classes are loaded — Blocks and Items themselves, and GrassBlock and MyceliumBlock, which name another block during that initialisation — while PotDecorations looks its brick up by key. The other seven are data generators that work in keys: four tag providers, a tag appender and two feature bootstraps, AquaticFeatures and NetherFeatures. A resource key is a registry plus an identifier, so it can be built with nothing loaded. Practically, it is the canonical machine-readable list of block and item ids, and a better starting point than the block and item holder classes if that is what you want — but not the id list: five sibling tables for entity types, block-entity types, potions, fluids and atlases live outside the package, in the trees they belong to.

The data generators, and why “data-driven” is both true and misleading

Most of net/minecraft/data is a build-time program: a second entry point with its own options, a generator that groups providers into packs, and a hash cache that skips unchanged files. net/minecraft/client/data is its client half, generating block and item models, the atlas definitions, equipment assets and waypoint styles.

The significance is a genuine paradox worth stating plainly. Vanilla’s own content is a data pack. net/minecraft/data/worldgen is most of the vanilla worldgen data pack written as Java — the biome feature lists, the material rules, the carvers, the jigsaw pools, the structures and structure sets, the processor lists — and the loot, recipe, tag and advancement packages do the same for their domains, all serialised through the same codecs the game uses to read a pack.

So “Minecraft is data-driven” is true: the running game only ever sees JSON parsed by codecs, with no vanilla-specific path (the resource system). And “you cannot change it without a data pack” is nearly true — which is the more useful statement, because the exceptions are load-bearing and a reader who believes the absolute version will misread three other pages.

The package is not build-time only, and the dedicated server ships all 174 files of it. Three kinds of exception:

  • Plain id tables. AtlasIds is read at runtime by the model manager, the atlas manager, the map, sky, painting and particle renderers, and by a chat component. Nothing build-time about it.
  • The bootstrap interface itself. BootstrapContext — in net/minecraft/data/worldgen — is what every vanilla data-pack registry’s bootstrap is written against, from damage types and enchantments to chat types, dialogs and world clocks. It is the most-imported type in the package by a wide margin.
  • Constants and math the running game calls. Blocks itself names TreeFeatures keys while constructing mushroom and fungus blocks, and a CaveFeatures key and a VegetationFeatures key for the moss and pale moss blocks; MinecraftServer reaches for a MiscOverworldFeatures key for the bonus chest; a jigsaw block entity defaults to a Pools key; NoiseRouterData and NoiseGeneratorSettings, both shipped worldgen classes, are compiled against TerrainProvider and the material rules in net/minecraft/data/worldgen/material; and the F3 screen’s biome-builder line calls NoiseRouterData.peaksAndValleys, one line that delegates straight into TerrainProvider (density functions).

Vanilla’s density functions and noise settings still reach the running game as JSON. NoiseRouterData.bootstrap and NoiseGeneratorSettings.bootstrap are collected by VanillaRegistries, which the data-generator entry point runs and which Commands.validate borrows for its ambiguity check; the game itself reads the generated files out of the built-in pack. Editing TerrainProvider changes terrain by changing what that generator writes.

So net/minecraft/data holds a build-time program and a handful of tables and functions the shipped game compiles against and executes. The generator half is inert at runtime but for one static helper StructureTemplateManager borrows from NbtToSnbt, and it is the half worth reading — it is the fastest way to understand what a vanilla biome or structure declares, because it is typed and cross-referenced where the JSON is not, a point biomes and structure placement both depend on. And the report providers are how you get machine-readable dumps of the registries and the packets, two of the tables this book’s own reference layer covers.

The audio backend lives in Blaze3D, and is not skipped

com/mojang/blaze3d/audio is hatched for its address, like net/minecraft/gizmos above: the book teaches what it does and the atlas still counts it outside every part, because it is filed where nobody looks for it. It wraps OpenAL, and it sits inside Blaze3D, beside the window and RenderSystem, rather than in the client’s sound package where the engine, the manager, the channel bookkeeping and the Ogg decoding live. That is the boundary fact: Blaze3D is the platform layer for both devices, not only the graphics one, and a reader looking for the sound code under client/sounds will not find the half that talks to the driver. Everything the package does — the device and context, the channel limits (which are counters and not pools, as that page’s own heading insists), binaural rendering through Options.directionalAudio, hot-plugging a headset mid-game — is taught, in Part X, by the sound engine.

Player reporting

net/minecraft/client/multiplayer/chat/report, client-only. ReportingContext holds the sender, the environment (which server or realm), a log of the last thousand-odd received messages, and at most one draft report. There are three report kinds — chat, skin, name — and an eleven-value reason enum.

The piece worth naming is the context builder: a chat report does not send just the offending line, it walks the log backwards to assemble surrounding signed context, which is what makes the report verifiable at the other end. The report machinery is the consumer of the chat-signing system that chat and signing documents. Neither the transport nor the policy is in the game — both come from the account service library.

Gaps, and the ruling on each

The hatched boxes above are the boundary as drawn. Beside them sits a shorter list of things that were never excluded on principle — they were simply not written when the book reached them, and each has since been ruled on. Ten are now taught, either as a page of their own (net/minecraft/gametest, com/mojang/blaze3d/platform, the post-effect chains, the scoreboard and command storage) or as a section of one (the debug cluster across the HUD and debugging pages, item models on models and atlases, the packrat parser on Brigadier and on codecs, the animation framework on entity rendering, the pack classes on the resource system, and net/minecraft/client/resources, which turned out to be five systems with five owners rather than one gap). Naming where each landed is the landing pages’ job, not this one’s; what belongs here is the other ruling.

Declined, with the reason. A decline is a promise that a reader will not miss it, not a shrug.

whatsizewhywhat carries it instead
com/mojang/renderpearl/backend/vulkan33 files, 7,387 linesa faithful second implementation of an interface already documented, and the abstraction is the lectureBlaze3D
net/minecraft/client/data28 files, 6,189 linesbuild-time model, atlas, equipment and waypoint generators, the same category as the generator half of net/minecraft/data, but big enough that a reader trips over itnamed here and in the list of main methods on anatomy
the catalogues~140 entity models, ~80 particles, 102 render states, 50 render layers, 16 animation definitions, 56 of 58 worldgen features, 57 tree kits, the entity sub-predicateseach is one shape repeated, and the shape is on the page that owns the frameworkthe reference layer
client/quickplay, client/profiling, client/renderer/gizmosone or two classes eachno mechanism a lecture needs—
net/minecraft/data/worldgen as content64 files, 6,016 linesdeclined as content: it is the datagen bootstrap that emits vanilla’s JSONthe runtime exceptions named above, which are not a decline
net/minecraft/client/animation’s keyframe definitions16 of its 21 classespure data in Java clothing — and lines is the wrong unit for it: 266 lines and 665 KB, one file’s longest line thirty thousand characters, because the decompiler renders each animation as one builder chainentity rendering has the framework

Two things inside com/mojang/renderpearl/backend/vulkan are named before the decline rather than after it: DestructionQueue, the deferred-free discipline OpenGL needs no equivalent of, which is the clearest illustration of what the device seam hides, and vulkan/checkpoints, vendor breadcrumb extensions for GPU crash reports. The shader compiler is not in the declined tree. GlslCompiler and SPIRVModule, the shaderc and spirv-cross pair, sit in front of both backends in renderpearl/frontend/shaders, because Minecraft still authors GLSL and compiles it to SPIR-V once — Vulkan takes the SPIR-V as it is and OpenGL cross-compiles it back to GLSL — which is the whole reason one shader source can feed two backends. The interiors of com/mojang/renderpearl/backend/opengl are declined on the same grounds.

Named, and not yet written. These are real systems with real lectures in them, and no ruling above covers them: the carver tunnel walk; the dragon fight (EnderDragonFight); client/multiplayer‘s joining-a-server tail; and two corners of the pack system that the resource system names without teaching — the server-resource-pack prompt and download flow in client/resources/server, and the linkfs synthetic file system that presents the launcher’s hash-named asset files as the one tree their index describes. Beside them sit the maps’ saved data and the wandering trader’s timer, which the level data table names and no page teaches, four Part V mechanisms its landing page does not list: rails, the shelf, the beehive and fire spread; local difficulty (DifficultyInstance), which only that table’s page explains; and four things a player carries that Part VIII lists and does not teach — the attack’s visual effects, the damage statistics, the warden-spawn tracker and the spectator camera behind /spectate. They are named here so that a reader who wants one knows the book knows it is missing, and knows where to start.

Where to look

If you need one of these the entry points are named in each section; if you want a list rather than a system, start at net/minecraft/references for ids and the report providers in net/minecraft/data for everything else, and the atlas for the shape of the whole jar.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

II · Foundations

Verified against Minecraft 26.3 · Part II · The machinery every later part assumes: how anything becomes data, gets a name and a number, is loaded, and reaches into code.

Part II is the vocabulary the other twelve parts speak without pausing to define it. Nothing here is a thing a player does; everything here is what happens underneath the things a player does — and a player still recognises it by its symptoms: the square brackets after an item name in a /give, the #minecraft:logs in a recipe file, the type line at the top of a recipe file. Each of those is one of these seven pages surfacing. The claim the part makes is that they are not seven mechanisms but three: a codec describes a value, a registry gives it a name and a number, a pack stack decides which copy of the file wins, and everything above is those three used again — which is why a world made almost entirely of JSON loads in seconds and why a data pack can recombine the game without adding a single behaviour to it.

The shape of the part

Part II is not a stack but a fan. Codecs and registries come before everything else here, and the five pages below them lean on those two far more than on each other. The last page is the pattern the rest exists to make possible.

flowchart TD
    C["1 · Codecs, NBT and JSON"]
    R["2 · Identifiers and registries"]
    S["3 · The resource system"]
    T["4 · Tags"]
    D["5 · Data components"]
    X["6 · Text components"]
    P["7 · The data-driven type pattern"]
    C -- "decodes every pack-written element" --> R
    C -- "a persisted component has one" --> D
    C -- "text is a value like any other" --> X
    R -- "/reload rebuilds the reloadable ones" --> S
    R -- "the tag table is swapped after the freeze" --> T
    R -- "an entry carries its starting components" --> D
    R -- "the registry of kinds is built in" --> P
    S -- "tags are read from the same stack" --> T
    T -- "any set field takes a tag" --> P
    D -- "a component type is a key, not a kind" --> P

The part as a fan, numbered to the watch order. An arrow points from the machinery to the page that takes it for granted: codecs at the top is what nothing else here is built without, registries hang directly off it, and the pattern at the foot, which no arrow leaves (none leaves text components either), is where the part is going.

Before you start

Anatomy, for the threads: built-in registries are frozen before either program exists, data-pack loading runs on the worker pool with hops back to the owning thread, and a reload’s apply phase runs on whichever thread owns the state being replaced.

Watch in this order

  1. Codecs, NBT and JSON — one ItemStack written four ways: into a chunk file, into a packet, as a checksum in a click, and out of the text of a /give. The click sends no component values at all, only hashes.
  2. Identifiers and registries — how minecraft:diamond_sword becomes an Item before the game exists, and how a data-pack biome becomes a Holder the client is told about. The wire id of a sword is its place in the order the items were registered.
  3. The resource system — F3+T as a pipeline: a stack of packs, a snapshot of the list, every listener preparing at once, applying in order; /reload as the same pipeline on the server. On the client a failed reload deselects every pack it can, not the bad one.
  4. Tags — #minecraft:logs from a JSON file to the set a parrot checks before it perches. A frozen registry’s contents never change, and yet /reload changes what the tag contains.
  5. Data components — the prototype an item type supplies and the patch a stack carries. The prototype is built on every reload, with the world’s registries in hand, not in the constructor.
  6. Text components — a death message built on the server, sent as a translation key, and worded by the client’s language file. The client receives it before it knows what it says.
  7. The data-driven type pattern — the type field that picks a kind, a recipe’s or a loot function’s, is a lookup in a built-in registry of kinds. Fifty-six of the registries in BuiltInRegistries are read that way, and they are why a pack can compose the game’s behaviours without adding one.

Where the part stops

Every page here follows a scenario, so each names the classes its own scenario touches and names the families beside them without walking every member — the concrete Tag classes one per primitive, the fifteen partial component predicates, the read-only Holder views. Two omissions are deliberate: net/minecraft/util is a grab-bag rather than a system, named only where a page needs one of its helpers, and net/minecraft/core/dispenser is eleven dispense behaviours, the interface they share and the record each is handed, which the book names nowhere because what a dispenser is belongs to Part V and what each behaviour does is a catalogue. Between them that leaves a floor: 23% of the part’s lines are named on no page in the book. Save migration, the largest thing in net/minecraft/util, is outside that count altogether, out of scope by the newest-version-only rule (what this book skips has the boundary in full).

Reference this part uses

Math and primitives — the coordinate spaces, packings, shapes and random sources every page assumes; it was a Part II page and is now looked up, not watched. Registries — every registry key but the root’s: built-in, data-pack, synced. Data components — every DataComponentType in DataComponents. Naming drift — Identifier was ResourceLocation. Diagram lanes — the abbreviations every sequence diagram’s lanes are written with. The glossary is worth more here than in any other part: this is where the vocabulary every later part leans on is defined, and it is the page to check when a later part uses one of these words in a second sense.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Codecs, NBT and JSON

Verified against Minecraft 26.3 · Part II · One ItemStack written four ways: into a chest’s chunk file, into a container packet, as a checksum in a click, and out of the text of a /give.

A player types /give @s diamond_sword[damage=5], drops the sword into a chest, logs out, comes back and clicks the slot. In those few seconds the same sword has been written four times into four different shapes: parsed out of the square brackets, written into the chunk file the chest lives in, sent down a socket as bytes, and sent back up when the slot was clicked. The click is the one worth stopping on. It carries no component values at all. It names the item and the count in the clear — HashedStack.ActualItem is a Holder<Item>, an int and a hashed patch — and for each component the sword’s patch adds it sends one 32-bit checksum, produced by running that component’s own codec into HashOps, a DynamicOps whose output is a hash rather than a document. And it is the same codec every time. The codec that hashed the damage value is the codec that wrote it into the chunk file and the codec that parsed it out of the square brackets. One description of a type, and the format is an argument.

The cast

classwhat it decidesthread
CodecDataFixerUpper’s description of a type: encode to and decode from any format, given the ops for itany
DynamicOpswhat a format’s map, list, string and number are made of — the argument a codec takesany
NbtOpstags: the binary tree on disk, and what SNBT text parses intoany
RegistryOpsthe registry lookup a codec over a dynamic registry needs, wrapped around another opsany
HashOpsthe format whose finished document is a hash; nothing is serialised on the wayRender on the client, Server on the comparison
StreamCodecthe exception: hand-laid bytes on a Netty ByteBuf, written once, read onceNetty
TagValueOutput · TagValueInputthe only ValueOutput and ValueInput there are — a CompoundTag plus its ops, and what save code seeswhichever thread saves
ProblemReporterthat a codec failure inside a save is a logged path, not an exception in the tickthe failing thread

All of the game’s part of it ships in both jars. The thread column uses the names anatomy gives the threads, and which one holds a codec matters only where the same codec runs on two of them.

The four paths, side by side

into the chunk fileonto the wireback as a checksumout of the text
who starts itChestBlockEntity.saveAdditional, inside chunk serialisationClientboundContainerSetSlotPacket, from AbstractContainerMenu.broadcastChangesMultiPlayerGameMode.handleContainerInput, on the clickGiveCommand through ItemArgument
the opsRegistryOps over NbtOpsa RegistryFriendlyByteBuf, and RegistryOps over NbtOps wherever a component’s wire codec is a Codec run into NBTRegistryOps over HashOps.CRC32C_INSTANCERegistryOps over NbtOps, held by a TagParser
the codecItemStack.MAP_CODEC, inside ItemStackWithSlot.CODECItemStack.OPTIONAL_STREAM_CODECeach component’s own codec, through TypedDataComponent.encodeValueDataComponentType.codecOrThrow, one component at a time
what is carrieda document — id, count, componentsa count, an item id, then a DataComponentPatchone int per added component, and the bare registry ids of the removed onesSNBT text, then a Tag
the threadServer, then the worker pool, then an IO worker for the file itselfNettyRender on the client, Server on the comparisonServer
when it failsa problem is recorded on a ProblemReporter and logged when the scope closesthe decoder throws, Connection.exceptionCaught sees it, and the connection dropsthe hash disagrees, and the server sends the slot backa CommandSyntaxException with the cursor position in it

Four columns, four diagrams. Read them as four answers to the same question.

Disk: a chest writes a list of slots

sequenceDiagram
    participant CBE as ChestBlockEntity
    participant CHelp as ContainerHelper
    participant TVO as TagValueOutput
    participant NbtIo as NbtIo

    Note over CBE: the server thread, inside chunk serialisation
    CBE->>TVO: createWithContext, from a ProblemReporter and the registries
    CBE->>CHelp: saveAllItems, handed the ValueOutput straight on
    CHelp->>TVO: list Items with ItemStackWithSlot.CODEC, one entry per occupied slot
    TVO->>TVO: ItemStack.MAP_CODEC writes id, count and components
    TVO-->>CBE: buildResult gives a CompoundTag
    Note over NbtIo: an IO worker, later
    NbtIo->>NbtIo: write, from RegionFileStorage: the chunk's tag into its region file

The disk path, and the thing to notice is what has no lane: ItemStack itself. Nothing here is a save method on a stack — an output object is made with context at the top, handed down two levels, and hands back its CompoundTag on the way out, with ProblemReporter.ScopedCollector.close logging anything that went wrong as the scope shuts.

ItemStack has no NBT method — there is no save and no parse on it. The ValueOutput the whole chain writes into is one TagValueOutput.createWithContext, made at the top with a ProblemReporter and the registries, whose CompoundTag TagValueOutput.buildResult hands back on the way out. ChestBlockEntity.saveAdditional receives that ValueOutput and calls ContainerHelper.saveAllItems, which opens a typed list under Items with ItemStackWithSlot.CODEC, a record of slot plus the stack’s own ItemStack.MAP_CODEC fields inlined. That map codec writes id, count (defaulting to 1) and components, the last being DataComponentPatch.CODEC, which spells a removal as !minecraft:foo. NbtUtils.addCurrentDataVersion stamps the data version on the way out. Load is the mirror: BlockEntity.loadStatic reads the id, ChestBlockEntity.loadAdditional calls ContainerHelper.loadAllItems over ValueInput.listOrEmpty.

Wire: a count, an id and a patch

sequenceDiagram
    participant PEnc as PacketEncoder
    participant IStack as ItemStack
    participant DCP as DataComponentPatch
    participant PDec as PacketDecoder

    Note over PEnc: Netty, clientbound
    PEnc->>IStack: OPTIONAL_STREAM_CODEC, a varint count where anything non-positive means empty
    IStack->>DCP: Item.STREAM_CODEC for the id, then STREAM_CODEC for the patch
    DCP-->>PEnc: added count, removed count, each type with its value, then the removed ids
    Note over PDec: Netty, serverbound, the creative slot alone
    PDec->>IStack: validatedStreamCodec over OPTIONAL_UNTRUSTED_STREAM_CODEC
    IStack->>IStack: re-encode through ItemStack.CODEC into NullOps, keeping only the errors

The wire path, in both directions, and the two halves are not symmetrical: the clientbound half above the second note lays the stack’s own fields into the buffer by hand, in a fixed order, while the serverbound half runs a Codec it does not want the output of, purely to see whether it throws.

The clientbound half is laid by hand. ItemStack.OPTIONAL_STREAM_CODEC writes a varint count where anything non-positive means empty and nothing else follows, then a registry id that resolves because the buffer is a RegistryFriendlyByteBuf, then DataComponentPatch.STREAM_CODEC, which writes each value with its component’s own wire codec — a varint for the damage, and for a component with no wire codec of its own, its Codec run into NBT. ItemStack.STREAM_CODEC is the same codec that refuses an empty stack. Serverbound is a different animal: ServerboundSetCreativeModeSlotPacket uses ItemStack.validatedStreamCodec over ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC, and the decoded stack is re-encoded through ItemStack.CODEC into NullOps — output thrown away, only the errors kept — to prove that the persistent codec would have accepted it. Why that packet in particular is fenced, and what the other two fences are, belongs to packets and stream codecs.

Checksum: a hash instead of a stack

sequenceDiagram
    box transparent the client
    participant CPL as ClientPacketListener
    participant MPGM as MultiPlayer<br/>GameMode
    end
    box transparent the server
    participant SP as ServerPlayer
    participant ACM as Abstract<br/>ContainerMenu
    end

    Note over CPL: once, when configuration ends
    CPL->>CPL: a RegistryOps over <br/>HashOps.CRC32C_INSTANCE,<br/>from its own registries
    Note over SP: and the same ops, built the same way, from the server's
    Note over MPGM: the Render thread, on the click
    MPGM->>ACM: ServerboundContainerClickPacket: a HashedStack per changed slot
    Note over ACM: the server thread, after the click has been re-run
    ACM->>SP: the same hash of the server's own stack
    ACM->>ACM: RemoteSlot.<br/>Synchronized <br/>compares the two

Two machines computing the same number the same way, which is the only reason the comparison means anything. The packet carries no component values at all — one int per added component and the removed types by registry id — so a client that has been lied to about a stack cannot hash its way back to agreement.

HashOps is a DynamicOps like any other, and a codec cannot tell the difference: it builds maps and lists and strings as usual, and what comes back at the end is a hash code rather than a tree. There is no intermediate byte form to hash. Only one instance is ever built, HashOps.CRC32C_INSTANCE, and both sides wrap it in a RegistryOps — ClientPacketListener builds one from the registries it received during configuration, ServerPlayer from the server’s own — because a component value can name a registry entry, and a hash of an unresolvable name is no hash at all. The server’s is behind a 256-entry cache keyed on the TypedDataComponent, so the common components are hashed once per player and then looked up. Removals are not hashed: HashedPatchMap is a map of added component type to int plus a plain set of removed types. What the server does with the comparison — the either-or in RemoteSlot.Synchronized, and the promotion to a concrete stack when the hash agrees — is containers and menus.

Text: square brackets into a Tag

sequenceDiagram
    participant IP as ItemParser
    participant TagP as TagParser

    IP->>TagP: create once, as the command is built, over RegistryOps on NbtOps
    Note over IP: the server thread, while Brigadier parses the command line
    IP->>TagP: parseAsArgument at each component's value
    TagP-->>IP: a Tag, read no further than the value
    IP->>IP: DataComponentType.<br/>codecOrThrow parses that Tag

The shortest of the four paths, and the one that ends where the first began: the Tag the parser hands back goes through the very codec the chunk file used. Like the disk and checksum paths, it reaches a component’s codec by asking the DataComponentType for it.

ItemArgument hands GiveCommand an ItemInput, and ItemParser is what builds it. The parser holds a RegistryOps over NbtOps.INSTANCE and a TagParser created for that ops, so [damage=5] becomes a Tag and then goes through the very codec the chunk file used, reached by DataComponentType.codecOrThrow. A leading ItemParser.SYNTAX_REMOVED_COMPONENT is the command-line spelling of the same removal the disk codec writes. Data packs are the JSON twin of this path: RegistryDataLoader decodes every registry file in a data pack, loot tables among them, through a RegistryOps over JsonOps.INSTANCE, and a set_components function in a loot table reads its patch with DataComponentPatch.CODEC, the codec a stack’s components go through on disk.

One abstraction, and the ops that are not formats

A Codec is a description of a type and nothing else; it does not know what it is writing into. The format is the DynamicOps handed to it at the call, so the same object describes NBT on disk, JSON in a data pack, and SNBT typed at a command line. Most of the game’s codecs are records built with DataFixerUpper’s RecordCodecBuilder, from primitives and the combinators in ExtraCodecs, the game’s own vocabulary in net/minecraft/util; the class index is where to look one of them up.

Two of the game’s ops are not formats at all. HashOps, in the cast above, answers every question an encoding codec asks and returns a checksum instead of a document. NullOps, which the cast does not list, returns Unit: it encodes to nothing, and exists so that a codec can be run for its errors alone, which is exactly what ItemStack.validatedStreamCodec does to a creative-mode stack.

StreamCodec is the genuine exception, and it is not a Codec at all. A StreamCodec pairs an encoder and a decoder over a ByteBuf directly, with StreamCodec.composite building one out of field codecs; the catalogue of primitives is ByteBufCodecs. A packet is written once, read once and must be small, so it gets hand-laid bytes rather than a document in some format. The two worlds meet at ByteBufCodecs.fromCodec and ByteBufCodecs.fromCodecWithRegistries, which run an ordinary Codec into NBT and put the tag on the wire; the composing vocabulary on the far side of that meeting belongs to packets and stream codecs.

Where the registry context comes from

A codec that names an entry of a dynamic registry cannot resolve it on its own. RegistryFixedCodec demands a RegistryOps, and RegistryFileCodec and HolderSetCodec need one for a reference or a tag — a DelegatingOps carrying a RegistryOps.RegistryInfoLookup beside whatever real ops it wraps. There are two routes worth knowing: HolderLookup.Provider.createSerializationContext, which is what nearly every caller uses, and RegistryDataLoader.createContext, used during registry loading itself, when the registries are still being built (identifiers and registries). Both end at RegistryOps.create, which a handful of callers reach directly.

A codec over a built-in registry is a different case: Registry.holderByNameCodec — and so Item.CODEC — resolves against the registry instance captured inside the codec, and works on bare NbtOps.INSTANCE. The distinction is not academic. It decides which paths must build a context before they can decode anything, and in practice almost every real path must: TagValueOutput.createWithoutContext exists and is called by nothing at all, and there is no context-free ValueInput even in principle.

On the network the same context arrives as a decorator. RegistryFriendlyByteBuf adds RegistryFriendlyByteBuf.registryAccess to a FriendlyByteBuf, and RegistryFriendlyByteBuf.decorator is bound when the protocol switches from configuration to play. Configuration packets have no registry context — which is why registry data and tags are sent in that phase as NBT and ids, and why the ByteBufCodecs.holder family is play-only (protocol phases).

What NBT actually is

Tag is a sealed interface, and the scalar leaves are records.

branchmembersnotes
CompoundTag—a final class; the only keyed shape
CollectionTagListTag, ByteArrayTag, IntArrayTag, LongArrayTagthe arrays are final classes, ListTag is a list
PrimitiveTagStringTag and the six NumericTag recordsrecords of one value each
EndTag—the terminator

Because the leaves are records, the old getAsInt family is gone: Tag.asInt and Tag.asString return Optional, CompoundTag.getInt is Optional and CompoundTag.getIntOr is its defaulted form, CompoundTag.get is still nullable, and the two-argument contains taking a type id no longer exists.

Three things about the binary form surprise people.

A mixed-type list is boxed on the way out. Binary NBT stores one element type per list, so ListTag.write promotes a heterogeneous list to compounds and wraps each element in a one-entry compound under the empty key, with ListTag.addAndUnwrap the exact inverse on read. This is not a legacy artefact left lying around — it is written today, and it is the whole reason mixed lists are legal at all.

A numeric array stays an array, but nothing turns a list into one. NbtOps.createCollector hands back a NbtOps.GenericListCollector for an empty list and only reaches for NbtOps.ByteListCollector, NbtOps.IntListCollector or NbtOps.LongListCollector when it is handed an existing ByteArrayTag, IntArrayTag or LongArrayTag — and those degrade back to the generic collector the moment an element does not fit. A codec building a fresh list gets a ListTag, whatever is in it; the arrays on disk are written by the codecs that asked for arrays.

A whole file need not be read to answer a question about it. StreamTagVisitor and the visitors beside it let NbtIo.parse pull a named handful of fields out of a region chunk without materialising the chunk: a CollectFields is built from the FieldSelectors the caller wants, two for the IOWorker reading a chunk’s data version and its blending data, three for StructureCheck, which is how it answers without loading a chunk; the world-list screen does the same to level.dat with SkipFields.

Every read that came from outside carries a budget. NbtAccounter is charged as the per-type read strategy in TagType walks the stream, with NbtAccounter.DEFAULT_NBT_QUOTA at 2 MiB, NbtAccounter.UNCOMPRESSED_NBT_QUOTA at 100 MiB and a depth cap of NbtAccounter.MAX_STACK_DEPTH; overrunning any of them raises an NbtException. Region compression is a separate global: RegionFileVersion.configure sets one process-wide selection from region-file-compression, a RegionFile captures it once for writing, but every chunk carries its own version byte and reads honour that — so one world can hold chunks in several compressions at once. RegionFileVersion.VERSION_CUSTOM (id 127) is the marker for a chunk written with a compression the game does not name; the marker for a chunk that grew too big and lives in its own external file is a different one — RegionFile’s external-stream flag, 128, set in the version byte. And NbtIo.writeCompressed (GZIP) is for the standalone files, level.dat and player data.

The text form is TagParser, and it is generic over its output ops rather than producing tags: SNBT can decode straight into any format’s target without a CompoundTag in between. TagParser.parseCompoundFully is the plain-string entry point; TagParser.FLATTENED_CODEC accepts an SNBT string only, and TagParser.LENIENT_CODEC, built as an alternative of the two, is the one that takes a string or an object interchangeably. Under it, SnbtGrammar is a packrat grammar and SnbtOperations supplies the built-in bool and uuid functions.

Save code never sees a CompoundTag

ValueOutput and ValueInput are the façade every BlockEntity and Entity writes through — ValueOutput.store with a codec, ValueOutput.child, ValueOutput.list, and typed getters with defaults on the way back in (ValueInput.getIntOr, ValueInput.getStringOr). The only implementations are TagValueOutput and TagValueInput, which wrap a CompoundTag and a DynamicOps, with ValueInputContextHelper holding the shared provider and the empty instances. The CompoundTag-returning methods on BlockEntity are final shells that build one of these, and they differ only in how much metadata they add: BlockEntity.saveCustomOnly none, BlockEntity.saveWithoutMetadata the block entity’s own components as a full DataComponentMap, and BlockEntity.saveWithFullMetadata the id and x, y and z. BlockEntity.saveWithId adds the id too, but only in the ValueOutput form — it has no CompoundTag shell.

The point of the façade is that an encode failure is reported, not thrown. Everything goes through a ProblemReporter, and ProblemReporter.ScopedCollector logs the whole collected tree of problems when it closes, rooted at BlockEntity.problemPath or Entity.problemPath; TagValueOutput.EncodeToFieldFailedProblem and its siblings are what a bad codec produces. An item that will not serialise costs you the item, not the tick. The deliberate exceptions to the façade are CustomData and TypedEntityData, the two components that carry a CompoundTag verbatim so that data packs have an escape hatch (data components).

Migration is the one thing that runs before any of this. DataFixTypes is the enum of every kind of file the game owns — DataFixTypes.CHUNK, DataFixTypes.PLAYER, DataFixTypes.LEVEL, DataFixTypes.OPTIONS and the rest — and each calls DataFixTypes.updateToCurrentVersion on the DataFixerUpper DataFixer from DataFixers.getDataFixer before the codec is shown the tag, using the data version NbtUtils.addCurrentDataVersion stamped when the file was written. The fixes themselves are out of scope here.

Trusted, untrusted and validated

The wire’s own vocabulary for how far to trust a document belongs to packets and stream codecs: the trusted/plain pairs are a read budget chosen by direction, and the creative slot is the one packet that carries an arbitrary stack and is fenced three ways for it. What belongs here is the fact that stands behind those fences: this page’s four paths run one ItemStack four ways, and the creative slot’s is the only one where the codec is run for its errors rather than its output. ItemStack.validatedStreamCodec re-encodes a decoded stack through ItemStack.CODEC into NullOps and keeps nothing but the problems — the persistent codec is used as a validator for the wire one, which is why a creative-mode stack cannot carry a component the disk would reject.

JSON, the third format in the title and the smallest on the wire

JSON is the format with the smallest footprint. It is the data packs, and on the wire it survives in exactly two places, both outside the play phase: ClientboundStatusResponsePacket and ClientboundLoginDisconnectPacket, sent through ByteBufCodecs.lenientJson. That is why the game ships two JSON parsers — LenientJsonParser for the wire and StrictJsonParser for data packs. Neither is where most JSON reading happens, though. GsonHelper is the toolbox — sixty-nine static helpers, most of them pulling one typed field out of a parsed object and naming the field in the exception when it is missing or the wrong shape — and it is the pre-codec way of reading JSON, still called from 124 places: the model loaders, the particle definitions, the operator, allow and ban lists, and the data fixers. It sits outside the codec pipeline but for one length check inside ComponentSerialization.flatRestrictedCodec; a field it reads is read by a hand-written parser and a field a codec reads is not. Chat text itself is NBT by the time it reaches the play phase: ComponentSerialization holds that whole matrix in one class (text components).

Questions players ask

Why does an item look identical and not stack? Because equality is prototype plus patch, and the patch is what these codecs round-trip. Two stacks that took different routes to the same components compare equal; one that kept a component the other dropped does not, however the tooltip reads.

Why can a data pack put arbitrary NBT on an item at all? Because CustomData and TypedEntityData are components whose value is a CompoundTag, passed through verbatim. Everything else on a stack has a real codec and is checked by it.

Why does a corrupt item vanish instead of crashing the world? Because the save façade reports rather than throws. The bad field becomes a problem on a ProblemReporter and is logged when the scope closes, and the chunk saves without it.

Why is a creative-mode item the one thing the server double-checks? Because it is the only packet on which a client authors a whole stack. Everywhere else the client either receives stacks or asserts hashes about stacks the server already sent it.

Why can a world hold chunks in two different compressions? Because the compression setting is captured per RegionFile for writing only, and every chunk stores the version byte it was written with. Reads honour the byte.

Where to look

Codec · DynamicOps · ExtraCodecs · RegistryOps · HolderLookup.Provider.createSerializationContext · Tag · CompoundTag · ListTag · NbtOps · NbtIo · NbtAccounter · TagParser · StreamTagVisitor · HashOps · NullOps · ValueOutput · ValueInput · TagValueOutput · ProblemReporter · BlockEntity (the save shells) · ContainerHelper · ItemStackWithSlot · ItemStack (the codec fields) · StreamCodec · ByteBufCodecs · RegistryFriendlyByteBuf · PacketEncoder · HashedPatchMap · ItemParser · DataFixTypes · RegionFileVersion


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Identifiers and registries

Verified against Minecraft 26.3 · Part II · A player types /give @s minecraft:diamond_sword, and the sword reaches their inventory as the number of the line it was registered on.

A player types /give @s minecraft:diamond_sword. The string on the right is parsed into an Identifier, the identifier is paired with Registries.ITEM to make a ResourceKey, and the key is looked up in a table the server froze at startup, before any world existed — BuiltInRegistries.ITEM. The stack that then lands in the player’s inventory does not carry the name. Item.STREAM_CODEC is ByteBufCodecs.holderRegistry over Registries.ITEM, and it writes an integer. That integer is a line number. MappedRegistry.byId is an insertion-ordered list, MappedRegistry.register appends to it, and an entry’s numeric id is its position — so the wire id of a diamond sword is where its line falls in Items, reordering two lines in Blocks changes a block’s wire id, and a resource pack cannot. A dynamic registry, a biome from a data pack, gets its numbers the other way: its elements are decoded in parallel but registered in sorted order of their ids, so the server’s numbering does not depend on which file finished first. The client does not re-derive those numbers at all — it registers what the server sent, in the order the packet lists them.

The cast

classwhat it decidesthread
Identifierthe name — namespace and path — and which strings are legal onesany
ResourceKeythe name paired with the registry it belongs to, interned so equal keys are one objectany
Registry · MappedRegistrythe table: key to object to integer, the frozen flag, the two tag tableselements written on the launching thread or a load task’s worker, tags by whichever thread applies them; read from anywhere
Holdera reference to an entry that may be handed out before the entry exists (Holder.Reference), or an inline value that belongs to no registry (Holder.Direct)any
HolderLookup.Provider · RegistryAccessthe read-only view a codec resolves against: all the registries it may nameany
BuiltInRegistriesthe static registries — created empty at class init, filled and frozen by Bootstrap.bootStrapthe launching thread, before any server or client object exists
RegistryDataLoaderthe dynamic registries — one load task per registry, JSON from the packs on the server; on the client NBT from the wire, or its own jar’s JSON for a pack both sides knowUtil.backgroundExecutor
LayeredRegistryAccesswhich layer may see which: four RegistryLayers on the server, two ClientRegistryLayers on the clientthe server thread owns MinecraftServer.registries; the Render thread owns the client’s

All of this is server and client: MappedRegistry, BuiltInRegistries and RegistryDataLoader ship in the dedicated server jar. Client-only, of the classes this page names, are ClientRegistryLayer, RegistryDataCollector, KnownPacksManager and the client’s CommonListenerCookie — and, less surprisingly, ClientPacketListener, ClientConfigurationPacketListenerImpl and IntegratedServer.

The name

Identifier, in net/minecraft/resources, is a namespace and a path, and it is the class a 1.21-era reader knows as ResourceLocation. It is not a record: a final class with a private constructor whose validation is an assertion on the trusted path and a real check on the parsing paths (Identifier.isValidNamespace, Identifier.isValidPath). The two character sets differ, and the difference bites: a path may contain /, a namespace may not, and .. is rejected outright as a namespace. Identifier.DEFAULT_NAMESPACE is minecraft, and Identifier.withDefaultNamespace supplies it — as does parsing a string with no separator at all, or one that starts with the separator. Identifier.parse throws IdentifierException (which lives in net/minecraft, not beside Identifier) where Identifier.tryParse returns null. Identifier.read exists twice, once returning a DataResult for Identifier.CODEC and once over a Brigadier StringReader, with Identifier.readNonEmpty beside it, and Identifier.resolveAgainst carries a path-traversal guard. The one that surprises people: Identifier.compareTo orders by path first and namespace second, so a sorted list of ids is not grouped by mod.

A ResourceKey is an Identifier paired with the Identifier of the registry it belongs to, and keys are interned through a weak map keyed by ResourceKey.InternKey, so two keys for the same registry and id are literally the same object. Registries holds the 156 ResourceKeys of registries (Registries.ITEM, Registries.BIOME …) — 155 distinct objects, because two of the keys are one: Registries.DIMENSION and Registries.LEVEL_STEM are both built from the string dimension, so interning hands back one object under two names and two (unchecked) element types — Registries.LEVEL_STEM is the data-pack registry the RegistryLayer.DIMENSIONS layer loads, Registries.DIMENSION keys the ServerLevels, and the conversion helpers between them are identity functions at run time. Five more registry keys are declared by the class that owns them rather than here, which is why the catalogue’s total is 161 and this one is 156 (registries). Interning is what makes identity work: ResourceKey declares no equality of its own, so MappedRegistry.byKey, MappedRegistry.registrationInfos, Holder.Reference.is for a ResourceKey and every == against a key like Level.OVERWORLD rely on equal keys being one object. BlockIds, ItemIds and BlockItemIds (net/minecraft/references) hold the per-element keys the block and item initialisers use.

The table

Registry is the read interface — Registry.getValue, Registry.getKey, Registry.getId, Registry.getTags, and the codecs Registry.byNameCodec and Registry.holderByNameCodec — and it extends IdMap, so every registry is also an int-to-object table. WritableRegistry adds WritableRegistry.register and WritableRegistry.bindTags; DefaultedRegistry answers a default entry — air for items and blocks — instead of null. MappedRegistry is the one real implementation, with DefaultedMappedRegistry its only subclass. It holds the same entries in five indexes at once, and every lookup direction is one of them: MappedRegistry.byKey and MappedRegistry.byLocation from a name, the insertion-ordered MappedRegistry.byId from a number, and, from the object itself, the two identity maps MappedRegistry.byValue — what answers Registry.getKey — and MappedRegistry.toId, which returns −1 for anything it has never seen, an equal-but-distinct object included. It carries the MappedRegistry.frozen flag that MappedRegistry.validateWrite checks on every ordinary write.

A Holder is the seam between a registry and the code that names its entries. It is a sealed interface with two kinds (Holder.Kind). Holder.Reference is an entry in a registry — and is itself non-sealed: it knows its HolderOwner, its key, its tags and its components, and any of those may be unbound until the registry binds them (Holder.Reference.bindValue, Holder.Reference.bindTags, Holder.Reference.bindComponents). A reference is a promise: Holder.Reference.value throws until Holder.Reference.bindValue has run, codecs hand these out freely during a load, and the freeze is what makes every promise kept — or fails loudly. Holder.Direct is a record wrapping an inline value and a DataComponentMap that belongs to no registry: it has no key, is in no tag, and serialises inline. A HolderSet is a set of holders — HolderSet.Named is a tag, HolderSet.Direct a literal list.

What a codec sees is a read-only view. HolderGetter, HolderLookup and HolderOwner are those views — and HolderOwner exists for exactly one question, HolderOwner.canSerialize: the context asking to serialise a holder answers whether it is that holder’s owner, which is why a holder from one world refuses to be written by another; HolderLookup.Provider is “all the registries I may resolve against” and HolderLookup.RegistryLookup is one of them. RegistryAccess is a HolderLookup.Provider over a set of registries, and RegistryAccess.Frozen is a bare marker for the finished kind. Every static initialiser writes its elements through Registry.register or Registry.registerForHolder (the registries themselves go into the root through WritableRegistry.register); every codec that names a dynamic entry — RegistryFileCodec, RegistryFixedCodec, RegistryCodecs.holderSet, HolderSetCodec — resolves through a RegistryOps (codecs, NBT and JSON); and at runtime MinecraftServer.registryAccess and ClientPacketListener.registryAccess are where anything that must resolve a key goes, except the server’s reloadable registries, which MinecraftServer.reloadableRegistries holds.

Before the game exists

sequenceDiagram
    participant Boot as Bootstrap
    participant BIR as BuiltInRegistries
    participant Items as Items
    participant Item as Item
    participant DMR as Defaulted<br/>MappedRegistry

    Note over Boot,DMR: the launching thread, before any server or client object exists
    Boot->>BIR: class init: one empty registry per key, 95 of the 156, each with a loader
    Note over Boot,Items: the dispenser and cauldron bootstraps run first, so Items is already initialised
    Boot->>Items: class init: registerItem per item, Item.Properties.setId stores the key
    Items->>Item: new Item(properties), which knows its own key
    Item->>DMR: createIntrusiveHolder: a Holder.Reference with a value, no key
    Items->>DMR: register(key, item, BUILT_IN): bindKey, numeric id = byId.size()
    Boot->>BIR: bootStrap: createContents runs every loader, then freeze, then validate
    BIR->>BIR: freeze: the root, then each registry — tags bound empty, then MappedRegistry.freeze
    DMR->>DMR: bindValue on every holder, refuse an unbound one, build componentLookup
    Note over Boot,DMR: components and tags are bound together later, when a world loads

The bootstrap ladder, on one thread, before either program exists. The numeric id an item gets is the size of the registry when it registers — the line number of its registration — which is why the wire ids are stable only because the registration order is.

Both Main classes call Bootstrap.bootStrap early, after argument parsing and crash-report preload (anatomy has the order), on the launching thread before any server or client object exists, and Bootstrap.isBootstrapped is set before any registry is touched. Bootstrap.bootStrap calls BuiltInRegistries.bootStrap, and after it returns every built-in registry is frozen and any WritableRegistry.register throws.

Registries exist before their contents. BuiltInRegistries class init creates every registry empty and records the loader that fills it in BuiltInRegistries.LOADERS, an insertion-ordered map. Bootstrap.bootStrap then runs BuiltInRegistries.createContents — the loaders in that order. By then Items, Blocks and EntityTypes are already initialised: Bootstrap.bootStrap reaches FireBlock.bootStrap, EntityTypes.PLAYER, the dispense behaviours’ bootstrap and CauldronInteractions.bootStrap before it calls BuiltInRegistries.bootStrap, and those touch Blocks, EntityTypes and Items in that order — so BuiltInRegistries.createContents finds the loaders’ values already there rather than triggering them. Bootstrap.checkBootstrapCalled is the guard that makes “touched Blocks from a static initialiser” a crash rather than a silent empty registry; it works because the bootstrap flag is set before the registries are touched, not after.

The key travels in the properties. Items.registerItem takes a ResourceKey from ItemIds or BlockItemIds and calls Item.Properties.setId before constructing, so an Item knows its own key at construction time. Blocks do the same with BlockIds and BlockItemIds and BlockBehaviour.Properties.setId.

Five registries hand the object its own holder. BuiltInRegistries.BLOCK, BuiltInRegistries.ITEM, BuiltInRegistries.FLUID, BuiltInRegistries.ENTITY_TYPE and BuiltInRegistries.BLOCK_ENTITY_TYPE are created with intrusive holders: the constructor asks the registry for a Holder.Reference (MappedRegistry.createIntrusiveHolder) that wraps the object before it has a key, and stores it in Item.builtInRegistryHolder. Registration then binds the key to that holder rather than creating a new one, so Item.builtInRegistryHolder and the registry’s own holder are the same object, and a tag check on a block or item is a set lookup on the holder’s own bound tag set with no registry hop (tags). Holder.Reference.createIntrusive is marked deprecated — the mechanism is load-bearing but not encouraged.

The numeric id is the line number. MappedRegistry.register appends to MappedRegistry.byId, and every static registration carries RegistrationInfo.BUILT_IN — a Lifecycle and the KnownPack an entry came from, which for a static entry is nothing. The wire id of an item is the position of its line in Items, and Item.STREAM_CODEC encodes that integer. A dynamic registry numbers the other way, and the difference is worth holding: ResourceManagerRegistryLoadTask decodes its files in parallel and then registers them in sorted order of their ids, so the server’s numbering is the same whichever file finished first. The client still never derives a number — it registers what the packet lists, in the order the packet lists it — but because the server sorted, that order is reproducible rather than accidental.

Freeze is a proof, stated in full below. BuiltInRegistries.freeze freezes the root registry first, then every registry it holds, and BuiltInRegistries.validate closes the bootstrap: an empty registry is logged, a DefaultedRegistry whose default key is missing throws.

When a world opens

The server keeps its registries as a LayeredRegistryAccess in MinecraftServer.registries, one layer per RegistryLayer — RegistryLayer.STATIC, RegistryLayer.WORLD, RegistryLayer.DIMENSIONS, RegistryLayer.RELOADABLE, in that order — with MinecraftServer.registryAccess the flattened view. LayeredRegistryAccess.getAccessForLoading is everything before a layer, which is what that layer’s JSON may reference; LayeredRegistryAccess.compositeAccess is everything. The client mirrors this with two layers, ClientRegistryLayer.STATIC and ClientRegistryLayer.REMOTE. On both sides the STATIC layer is RegistryAccess.fromRegistryOfRegistries over BuiltInRegistries.REGISTRY — a live view of the frozen root registry, not a copy of it.

flowchart TD
    WL["WorldLoader.load, on Util.backgroundExecutor"]:::worker
    RDL["RegistryDataLoader.load for the world<br/>registries, then the dimensions"]:::worker
    T1["one RegistryLoadTask: feature"]:::worker
    T2["one RegistryLoadTask: placed feature"]:::worker
    T3["… one per RegistryDataLoader.RegistryData, 51 then 1"]:::worker
    F["RegistryLoadTask.freezeRegistry binds every promise,<br/>then the RegistryValidator"]:::worker
    LRA["LayeredRegistryAccess.replaceFrom:<br/>world and dimensions in one call"]:::worker
    WL --> RDL
    RDL --> T1
    RDL --> T2
    RDL --> T3
    T1 -. "an unbound Holder.Reference" .-> T2
    T2 -. "an unbound Holder.Reference" .-> T1
    T1 --> F
    T2 --> F
    T3 --> F
    F --> LRA

World load, as a task graph rather than a conversation: one task per registry, all on the worker pool, each able to ask any other in the same call for an element it has not registered yet. The dotted edges are the forward references, the freeze is where they stop being promises, and the last box runs on the worker pool too, not on the thread that called WorldLoader.load.

Later, a client logs in and reaches the configuration phase, and the server sends it what it just built.

sequenceDiagram
    box transparent the server
    participant SRT as Synchronize<br/>RegistriesTask
    participant SCPL as ServerConfiguration<br/>PacketListenerImpl
    end
    box transparent the client
    participant CCPL as ClientConfiguration<br/>PacketListenerImpl
    participant RDC as RegistryData<br/>Collector
    end

    SRT->>CCPL: ClientboundSelectKnownPacks: which packs do you have?
    CCPL->>SCPL: ServerboundSelectKnownPacks: accepted all-or-nothing
    SRT->>CCPL: one ClientboundRegistryDataPacket per synced registry
    Note over SRT,CCPL: an entry from a pack the client has carries no data
    SRT->>CCPL: ClientboundUpdateTagsPacket: tags as registry ints
    SCPL->>CCPL: ClientboundFinish<br/>ConfigurationPacket
    CCPL->>RDC: collectGameRegistries: rebuild REMOTE, patch the static tags
    RDC->>CCPL: a RegistryAccess.Frozen, into the play listener's cookie
    Note over CCPL,RDC: with an IntegratedServer, that Frozen is thrown away<br/>for the server's own registries
    CCPL->>SCPL: ServerboundFinish<br/>ConfigurationPacket

The same registries crossing to a client: SynchronizeRegistriesTask sends the three data packets, and the fourth, which lets play begin, comes through the listener from a later task, JoinWorldTask. The client decodes none of it until that finish packet arrives, and the last note is the singleplayer case, where the work of the whole exchange is discarded.

WorldLoader.load creates the resource manager on the main thread, runs RegistryDataLoader.load on Util.backgroundExecutor, and returns to the main thread for the final assembly; this is where the RegistryLayer.WORLD and RegistryLayer.DIMENSIONS layers are filled, and the RegistryLayer.RELOADABLE layer of the copy MinecraftServer.reloadableRegistries holds. The configuration phase is the third moment: SynchronizeRegistriesTask sends the dynamic registries on the server thread, and the client rebuilds its ClientRegistryLayer.REMOTE layer in RegistryDataCollector.collectGameRegistries — decoding on the worker pool, joined on the Render thread — before it will accept play packets.

Layers load against the layers before them. RegistryDataLoader.load is given lookups built from LayeredRegistryAccess.getAccessForLoading — built by TagLoader.buildUpdatedLookups, so that the static registries’ freshly read tags are visible to the worldgen codecs before they are applied (tags) — so a biome JSON may reference a placed feature (same layer) or a sound event (RegistryLayer.STATIC) but never a level stem (RegistryLayer.DIMENSIONS, which loads after). The lists RegistryDataLoader.WORLD_REGISTRIES, RegistryDataLoader.DIMENSION_REGISTRIES, RegistryDataLoader.RELOADABLE_REGISTRIES and RegistryDataLoader.SYNCHRONIZED_REGISTRIES say which keys belong to which step and which subset the client is told about. Both the world and the dimensions layers are installed in a single LayeredRegistryAccess.replaceFrom call, and the dimensions layer that wins is the world data’s, not necessarily the one just decoded — a saved world’s dimension set survives.

Loading is a task graph. Each registry is one RegistryLoadTask owning a fresh MappedRegistry and a lock-guarded ConcurrentHolderGetter. RegistryDataLoader.createContext hands every task’s getter to every other, so Biome.DIRECT_CODEC decoding on one worker can ask for a carver that another worker is still registering — the getter returns an unbound Holder.Reference, and the reference is bound when that registry freezes. Forward references cost nothing, even in a cycle — a feature can name placed features and a placed feature its feature — because nothing waits on them. Fourteen of the fifty-two world and dimension registries also carry a RegistryValidator other than the empty one in their RegistryDataLoader.RegistryData, run after the freeze — thirteen of them entity-variant registries running the same check, RegistryValidator.nonEmpty, and the fourteenth Registries.TIMELINE, whose Timeline.validateRegistry is its own.

Provenance is recorded per entry, and it is coarser than it looks. ResourceManagerRegistryLoadTask gives each element a RegistrationInfo naming the KnownPack it came from and a Lifecycle. The rule is presence, not vanilla-ness: an element from any pack that reports a KnownPack is stable, and only an element from a pack with no known-pack info is experimental. KnownPack.isVanilla is computed on that path and then discarded. Anything received over the network is experimental, and the RegistryLayer.RELOADABLE layer is read by the same task under the same rule; the registry’s own lifecycle is the merge of its entries’, and that merge is what the “experimental features” warning on world open reads.

The client is told what it does not already have. SynchronizeRegistriesTask first asks the client which KnownPacks it has (ClientboundSelectKnownPacks). The request lists only the server’s packs that declare one — PackLocationInfo.knownPackInfo is an optional, so a world’s own datapack is simply absent from it — and the client matches them against its bundled vanilla repository through KnownPacksManager.trySelectingPacks. The comparison is all or nothing: the client’s answer must equal the request exactly, or every element of every synced registry is sent in full. On a match, RegistrySynchronization.packRegistries sends every element’s id but leaves the NBT payload empty for entries from those packs, and the client’s RegistryLoadTask.PendingRegistration.findAndLoadFromResource re-decodes the JSON from its own jar. A modified biome from a custom data pack is sent in full through Biome.NETWORK_CODEC — the network codec, which omits the generation and mob-spawn settings the client never needs.

The client rebuilds one layer and patches the other. RegistryDataCollector accumulates the packets, and RegistryDataCollector.collectGameRegistries runs when configuration finishes — dispatching the load onto a background executor against the negotiated packs and then blocking on the result, so the decoding leaves the Render thread, which waits for it, without the phase becoming asynchronous. The ClientRegistryLayer.REMOTE layer is rebuilt wholesale and frozen — but the static registries cannot be rebuilt, so their tags are applied in place, through the mechanism tags owns, and when no registry data arrived at all the collector takes a tags-only path that patches and returns the original registries untouched. The result is a RegistryAccess.Frozen in CommonListenerCookie that every RegistryFriendlyByteBuf in the play phase decodes against.

Singleplayer throws most of that away. When an IntegratedServer exists, ClientConfigurationPacketListenerImpl.handleConfigurationFinished substitutes the server’s own registries for the ones the client just built, and the memory connection suppresses re-applying static tags and components client-side. Both halves then share the same registry objects — which is worth remembering whenever a page says “the client’s copy”.

The freeze rule, stated

A frozen registry’s contents never change. Its tags and its elements’ components do. Everything in this part that looks like an exception to the first sentence is one of the two things in the second.

MappedRegistry.freeze is a proof, not a switch. It binds every holder’s value and throws if any holder is still unbound, if any intrusive holder was created but never registered, or if any tag declared by a TagKey was never bound. The tag half of that proof works because a registry keeps its declared tags apart from its bound ones, which is the split tags is built on: the freeze checks the declared table and installs the bound one. For the static registries the real tags do not exist until a data pack is read, so BuiltInRegistries.freeze first binds the tags the bootstrap asked for to empty (MappedRegistry.bindAllTagsToEmpty) and the proof passes on empty sets. The freeze also builds MappedRegistry.componentLookup. After it, the MappedRegistry.frozen flag makes MappedRegistry.validateWrite throw on every ordinary write. Two things still change: MappedRegistry.prepareTagReload requires the frozen flag, and the component prototypes are rebound beside the tags (data components).

What changes afterwards changes through two doors. Tags: a world load swaps the tag tables of the static registries, and /reload does more than refill the RegistryLayer.RELOADABLE layer — it re-reads and re-applies tags for every registry in the server’s composite access. How a frozen registry’s tags are swapped is the pay-off of tags, and the mechanics of the reload itself belong to the resource system. Components: components are not part of the freeze at all. Every registry element’s DataComponentMap is bound after it by Holder.Reference.bindComponents, and /reload rebinds every one of them; data components owns how.

Feature flags: the same registry, narrowed

A frozen registry’s contents never change — but what a lookup will show you can be narrower than what the registry holds, and that is most of the feature-flag mechanism; the rest is a direct FeatureElement.isEnabled check where an element is used. FeatureFlagSet is a bitmask: a 64-bit long and the FeatureFlagUniverse it belongs to, with FeatureFlagSet.MAX_CONTAINER_SIZE at 64 flags. There is one universe, main, and FeatureFlags declares four flags in it — FeatureFlags.VANILLA and the three experiments, FeatureFlags.TRADE_REBALANCE, FeatureFlags.REDSTONE_EXPERIMENTS and FeatureFlags.MINECART_IMPROVEMENTS. A set that is not a subset of FeatureFlags.VANILLA_SET is what FeatureFlags.isExperimental reports, and what puts the warning on a world.

What carries a flag is a registry element. FeatureElement is an interface whose one abstract method is FeatureElement.requiredFeatures, implemented by exactly seven types — Item, BlockBehaviour, EntityType, GameRule, MenuType, Potion and MobEffect — and FeatureElement.FILTERED_REGISTRIES names the seven registries those live in. HolderLookup.RegistryLookup.filterFeatures is the filter: handed the world’s enabled set, it returns the same lookup untouched for any registry not in that set of seven, and a delegating lookup that hides the disabled elements for one that is. The registry underneath is not touched, and neither is its numbering — a disabled item keeps its wire id.

That is why an experiment is enabled per world and not per install. The set is WorldDataConfiguration.enabledFeatures, read from level.dat, and the filtered lookup is what CommandBuildContext hands every argument type, so /give cannot name a disabled item and /setblock cannot name a disabled block; GameRules builds its map from the filtered BuiltInRegistries.GAME_RULE, so a disabled rule is not merely refused but absent; and LevelReader.holderLookup filters for anything that asks a level for a registry. Where the flags come from is the pack stack — a feature pack is a built-in pack carrying a FeatureFlagsMetadataSection, and turning on an experiment is enabling a data pack (the resource system).

What crosses the wire, and where the files are

Built-in registry elements never cross the network — both sides ran the same static initialisers — but their tags do, and dynamic elements do: ClientboundSelectKnownPacks and ServerboundSelectKnownPacks, then ClientboundRegistryDataPacket (one per synchronised registry, entries as RegistrySynchronization.PackedRegistryEntry) in the configuration phase, then ClientboundUpdateTagsPacket, which is a common packet and arrives again mid-play after a server /reload; and a registry element, built-in or dynamic, crosses inside other packets as a bare varint id resolved against the buffer’s registry access, except in the few packets that send its key. Only one variant shifts that numbering: ByteBufCodecs.holder reserves 0 for an inline Holder.Direct and writes every registry id one higher, where ByteBufCodecs.holderRegistry — which Item.STREAM_CODEC uses — writes the raw id. On disk, every key in RegistryDataLoader.WORLD_REGISTRIES and RegistryDataLoader.DIMENSION_REGISTRIES reads data/<namespace>/<registry path>/*.json (Registries.elementsDirPath) through FileToIdConverter.registry over a ResourceManager (the resource system), its tags live under Registries.tagsDirPath — there is a third path builder, Registries.componentsDirPath, but it names a reports directory the data generator writes and nothing in the running game reads — and the reloadable set (loot tables, predicates, item modifiers, recipes, advancements and three more) is read the same way, through ReloadableServerRegistries. Which registry is which kind is reference/registries.

For a 1.21-era reader. Identifier was ResourceLocation, and the vanilla data you remember being registered in code is now read as JSON like anyone else’s: RegistrySetBuilder, BootstrapContext and VanillaRegistries are the data generator that writes those files into the jar, and the running game only ever reads them (what this book skips has how far that is true). And Block.BLOCK_STATE_REGISTRY is not a registry: IdMapper is the standalone IdMap behind block-state ids and similar palettes, sharing an interface with Registry and nothing else.

Where to look

Identifier · ResourceKey · Registries · Registry · MappedRegistry.register · MappedRegistry.freeze · DefaultedMappedRegistry · Holder · HolderSet · HolderLookup · Bootstrap.bootStrap · BuiltInRegistries.bootStrap · RegistryLayer · LayeredRegistryAccess · WorldLoader.load · RegistryDataLoader.load · RegistryLoadTask · ResourceManagerRegistryLoadTask · RegistryValidator · RegistrySynchronization.packRegistries · SynchronizeRegistriesTask · RegistryDataCollector.collectGameRegistries · NetworkRegistryLoadTask · ClientConfigurationPacketListenerImpl.handleConfigurationFinished · RegistryOps


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The resource system

Verified against Minecraft 26.3 · Part II · A player presses F3+T, the screen goes to the logo and a bar, and every texture, model, sound and font is rebuilt from a stack of packs without the game stopping.

A player presses F3+T. The screen goes red, the Mojang Studios logo comes up, and a white bar creeps across under it while the old world keeps rendering behind. What is happening is one pipeline that everything the game reads from a pack goes through — textures, models, sounds, language strings, recipes, advancements, loot tables, tags, worldgen JSON: a stack of packs is discovered, merged into a resource manager that is a snapshot of the stack, and a list of reload listeners each rebuild their world from it, preparing at once on the worker pool (anatomy) where they have anything to prepare, and swapping their live state on the owning thread in the order they were registered. The client’s stack is resource packs (PackType.CLIENT_RESOURCES, the assets tree); the server’s is data packs (PackType.SERVER_DATA, the data tree) — same classes, two instances, two directories, and /reload is the same pipeline run by the server. The surprising part is the end. A reload that fails does not find the offending pack. Minecraft.rollbackResourcePacks deselects every resource pack it can, clears the options lists, saves, and reloads again — and if vanilla was the only selected pack it rethrows and crashes instead.

The cast

classwhat it decidesthread
PackRepository · Packwhich packs exist, which are selected, and in what orderwhoever asks: Render on the client, Server on the server
MultiPackResourceManager · FallbackResourceManagerwhich pack’s copy of a file wins, one stack per namespacebuilt on the asking thread, read from workers
ReloadableResourceManagerthe client’s long-lived façade: the current snapshot and the listener listRender
PreparableReloadListener · SimplePreparableReloadListener · SimpleJsonResourceReloadListenerwhat to read off-thread and what to swap on-threadprepare on the worker pool, apply on the owner
SimpleReloadInstancethe schedule: every prepare at once, every apply in order behind a barrierbuilt on the caller’s thread, barriers resolved on the owner
LoadingOverlaywhen the client’s reload is done, and what to do if it failedRender
ReloadableServerResourcesthe server’s one listener, and the registries that replaced the restServer

The pipeline

flowchart TD
    D["discover: PackRepository.reload re-runs every RepositorySource"] --> S["snapshot: a new MultiPackResourceManager over the opened packs"]
    S --> P["prepare: the worker pool reads, all at once"]
    P --> A["apply: each listener swaps its live state, in registration order"]
    A -- "ReloadInstance.checkExceptions finds none" --> F["finish: the level re-extracted"]
    A -- "a listener threw" --> R["roll back: every pack deselected"]
    R -- "the reload run again" --> D

The five stages and the one branch, which comes at the end and not at the start. A snapshot is of the pack list, not of the bytes; the rollback arrow runs back to discover because what follows a failure is another whole reload, not a retry of the stage that failed.

The rest of the page is a section per stage: what comes in, what is decided, what goes out. F3+T is the grounding trace; /reload is the coda, as a table of where the server’s run of the same pipeline differs.

Discover: the repository and its packs

What comes in is a set of RepositorySources (server/packs/repository), each a place packs are found. ClientPackSource and ServerPacksSource are the built-ins, both extending BuiltInPackSource, which also lists the packs bundled inside the vanilla pack — the art packs, the accessibility packs and every feature pack (BuiltInPackSource.TESTS_ID names one more, tests, which only a development build enables); FolderRepositorySource is a directory of user packs; DownloadedPackSource is server-sent packs, client only. PackRepository.reload re-runs every source into the available map and then rebuilds the selected list: prior choices are kept, and a pack whose Pack.isRequired is true is force-inserted at its Pack.getDefaultPosition. Reading the order out of options.txt is a startup step — Options.loadSelectedResourcePacks runs once in the Minecraft constructor, not on every F3+T.

A Pack is a discoverable pack: a PackLocationInfo (id, title, PackSource, optional KnownPack), a Pack.ResourcesSupplier that can open it, its Pack.Metadata (description, PackCompatibility, requested feature flags, overlays) and a PackSelectionConfig — required, default Pack.Position, fixed. Pack.Position owns the insertion algorithm that makes a fixed pack stick, and the list it inserts into runs from the bottom of the stack to the top: Pack.Position.BOTTOM inserts at the front of the list, past any pack already fixed there, and Pack.Position.TOP at the back. The last pack in the list wins (next section), which is why vanilla is BOTTOM and why “higher in the UI” means “later in the list”. The client’s vanilla pack cannot be deselected and the server’s can: ClientPackSource marks vanilla required and bottom; ServerPacksSource marks it bottom but optional.

What a pack is on disk is PackResources (server/packs): the raw file source with PackResources.getResource, PackResources.listResources and PackResources.getNamespaces. FilePackResources is a zip; PathPackResources a directory; OverlayedPackResources a pack plus its overlays subdirectories, which the Pack.ResourcesSupplier assembles for zip and folder packs. VanillaPackResources is not a PackResources itself: its Pack.ResourcesSupplier hands out FixedPathPackResources layers — the jar’s own assets and data, and on the client the launcher’s asset index above them — and never an overlay. Discovery is guarded: DirectoryValidator, ForbiddenSymlinkInfo and PackDetector decide what a folder is allowed to be, allowed_symlinks.txt is parsed into a DirectoryValidator by LevelStorageSource.parseValidator. One corner of server/packs is worth a sentence here. packs/linkfs is a synthetic read-only file system — LinkFileSystem, LinkFSProvider and a LinkFSPath that is a name in a tree rather than a name on disk — which presents the launcher’s asset files, stored under their hashes, as the one directory tree their index describes, so the game can open a pack that was never assembled.

What pack.mcmeta says

The file is read as ResourceMetadata sections: PackMetadataSection (description and a PackFormat range), FeatureFlagsMetadataSection, OverlayMetadataSection, ResourceFilterSection. Compatibility is a range, not a number. PackMetadataSection carries an inclusive range of PackFormat major/minor pairs and PackCompatibility reports too old, too new, unknown or compatible against the game’s own — resource 97.1 and data 121.0. Above PackFormat.lastPreMinorVersion (64 for assets, 81 for data) the min_format / max_format fields are mandatory and the old integer pack_format is not enough. A pack.mcmeta the strict codec rejects gets one more chance through a description-only fallback so the pack can at least be listed as incompatible. Overlays are versioned sub-packs: OverlayMetadataSection maps a PackFormat range to an overlays subdirectory that OverlayedPackResources layers on top of the pack itself, so one zip can carry variants for several game versions.

Feature flags are packs, but not auto-selected ones. A feature pack is a built-in pack carrying a FeatureFlagsMetadataSection, and its PackSource deliberately reports that it must not be added automatically. MinecraftServer.enableForcedFeaturePacks force-selects the ones matching the world’s forced features, and the world’s flag set is the selected packs’ requested flags joined with those forced ones. Turning on an experiment is enabling a data pack — through a different door than ordinary packs use. What the resulting FeatureFlagSet then gates is a registry lookup, and that is identifiers and registries.

What goes out of the stage is PackRepository.openAllSelected: the selected Packs opened into a list of PackResources, in order.

Snapshot: the manager

MultiPackResourceManager (server/packs/resources) is built from that list. It is a snapshot of the pack list, not of the bytes: it asks each pack for its namespaces — the first half of every id, the name of a directory under the pack’s assets or data root: minecraft for vanilla’s own files and whatever its author chose for the rest — and builds one FallbackResourceManager per namespace, each a stack searched from the last selected pack down. The old world stays up until the last apply, but the old files do not: on the client, ReloadableResourceManager.createReload closes the previous MultiPackResourceManager — with every file handle it held — before building the new one. What is frozen is which packs are in the stack; a Resource still opens its file when it is read, so a folder pack edited on disk mid-reload is observable. A zip is not: FilePackResources holds its zip open for the life of the pack.

A Resource is what a lookup returns: its source pack, an IoSupplier for the bytes — opened lazily, at read time — and a lazily-read ResourceMetadata found beside it. A .mcmeta is looked for in the winning pack or those above it, never in one below, so a pack overriding a texture without its .mcmeta loses the animation. ResourceFilterSection lets a pack hide lower packs’ files by pattern without providing replacements: MultiPackResourceManager reads each pack’s filter section and pushes it onto the namespace stacks as a filter, and a lookup that reaches a filtered entry stops there. Some loaders want every copy, not the winner: ResourceManager.getResourceStack and ResourceManager.listResourceStacks return all packs’ copies bottom-first, which is how languages, tags and atlas sources merge instead of overriding.

One more manager frames this one. ReloadableResourceManager is the long-lived client façade that holds the current snapshot and the PreparableReloadListener list; the server has no façade — each reload is a fresh MultiPackResourceManager inside MinecraftServer.ReloadableResources.

What goes out is one ResourceManager, wrapped in a PreparableReloadListener.SharedState, and a ReloadInstance that has already started, which is a SimpleReloadInstance, or its subclass ProfiledReloadInstance when the logger is at debug.

Prepare: every listener at once

A PreparableReloadListener.reload takes the shared state (which carries the ResourceManager), a background executor, a PreparableReloadListener.PreparationBarrier and a main-thread executor. SimplePreparableReloadListener splits that into SimplePreparableReloadListener.prepare (background) and SimplePreparableReloadListener.apply (main thread); SimpleJsonResourceReloadListener is the “every JSON file in a directory through one codec” specialisation, using FileToIdConverter to map assets/ns/equipment/foo.json to ns:foo; ResourceManagerReloadListener is the apply-only shape.

SimpleReloadInstance is the schedule, and this is what it does, read from SimpleReloadInstance.prepareTasks. It wraps both executors in counters. It calls PreparableReloadListener.prepareSharedState on every listener first, synchronously. Then it walks the listener list once, calling each listener’s PreparableReloadListener.reload and handing it a barrier chained to the previous listener’s returned future (the first listener’s barrier is chained to the initial task). The barrier’s PreparableReloadListener.PreparationBarrier.wait does two things: it posts a task to the main-thread executor that removes the listener from the set still preparing and completes the all-preparations future when that set empties, and it returns that future combined with the previous listener’s. So listener N’s apply runs only after every listener has reached its barrier and listener N−1 has finished entirely — apply included — and a listener that never reaches its barrier holds every apply behind it. The futures are sequenced fail-fast: the first listener to throw fails the reload as a whole. It does not stop the others — Util.sequenceFailFast completes the outer future exceptionally and leaves every prepare running; Util.sequenceFailFastAndCancel, which would cancel them, is not the one used here. What never happens is the applies.

flowchart TD
    SS["PreparableReloadListener.prepareSharedState:<br/>one synchronous pass, before any prepare starts"]:::client
    subgraph PREP["prepare, all at once"]
        TMp["TextureManager: read every texture"]:::worker
        AMp["AtlasManager: stitch every atlas"]:::worker
        MMp["ModelManager: load models and block states"]:::worker
        ETC["nineteen more listeners"]:::worker
    end
    ALL["all preparations in: every listener has reached its barrier"]:::client
    subgraph APP["apply, in registration order"]
        TMa["TextureManager: swap texture contents"]:::client --> AMa["AtlasManager: upload the atlases"]:::client
        AMa --> MMa["ModelManager: install the baked models"]:::client
    end
    SS --> PREP
    PREP --> ALL
    ALL --> APP
    SS -. "the pending sprite futures" .-> MMp
    AMp -. "completes them" .-> MMp

Three of the client’s twenty-two listeners, preparing on the worker pool and applying on the thread that owns the state, with the two rules that order them drawn once each: nothing in the lower box starts until the gate opens, and inside it each apply waits on the one above. The dotted pair is the single exception — AtlasManager.PENDING_STITCH, published in the first pass and completed during prepare, so the models load while the atlases stitch instead of queueing behind them.

The listeners registered between the three drawn are elided. Every apply waits on the all-preparations node; each apply also waits on the apply before it; and the dotted pair is the only way one listener’s prepare depends on another’s.

The shared-state channel

PreparableReloadListener.prepareSharedState is a separate first pass for a reason: it is the one place a listener can publish something for another listener’s prepare to consume, keyed by a PreparableReloadListener.StateKey. The game declares exactly one — AtlasManager.PENDING_STITCH. AtlasManager publishes a future per atlas there before any prepare starts; ModelManager and ParticleResources pull the pending sprite futures out of it and wait on them inside their own prepare, so model loading overlaps atlas stitching rather than queueing behind it. This is why the model/atlas dependency is not an apply-order dependency, and why reasoning about it from the registration list gets the wrong answer. Which thread runs that first pass depends on who started the reload: the Render thread on the client, but a worker on the server, because the server’s reload instance is created from inside an already-async chain.

Prepare never touches live state. That is the whole contract, and it is one-directional: a listener that reads from the manager in SimplePreparableReloadListener.apply is reading the new snapshot and that is fine (LanguageManager does exactly this, in the apply-only shape: LanguageManager.onResourceManagerReload reads the language files on the Render thread), while one that mutates live state in SimplePreparableReloadListener.prepare is racing the Render thread. Nothing a listener owns is torn down when a reload starts; the client keeps rendering with the old atlases while the new ones bake.

Apply: registration order

Registration order is apply order. The client registers, in order, LanguageManager, TextureManager, ShaderManager, SoundManager, AtlasManager, FontManager, the three colour listeners (GrassColorReloadListener, FoliageColorReloadListener, DryFoliageColorReloadListener), ModelManager, EquipmentAssetManager, PalettedTextureManager, EntityRenderDispatcher, BlockEntityRenderDispatcher, ParticleResources, GameRenderer, LevelExtractor, the cloud renderer, GpuWarnlistManager, a PeriodicNotificationManager, then SplashManager from Gui and WaypointStyleManager from Hud — twenty-two in all. On the client ReloadableResourceManager.createReload is called with Util.backgroundExecutor (named resourceLoad) and Minecraft itself as the main-thread executor, so apply runs on the Render thread, interleaved with frames. On the server a /reload calls ReloadableServerResources.loadResources with Util.backgroundExecutor and MinecraftServer, so apply runs on the Server thread; the first load, before any server runs, is handed the thread that waits for it — ServerMain, draining Util.blockUntilDone’s queue, on a dedicated server, and the Render thread when singleplayer opens a world.

The counters SimpleReloadInstance wrapped the executors in are where the progress bar’s numbers come from: its ReloadInstance.getActualProgress weighs prepare and apply tasks double and listeners past their barrier single, and the overlay smooths it.

Finish, or roll back

On the client LoadingOverlay is a poll. It draws the logo from the vanilla pack outside the reload (via VanillaPackResources.asResourceManager) and a smoothed bar from ReloadInstance.getActualProgress; a manual reload fades it in over half a second and it will not fade out until a full second has passed. Each tick, once ReloadInstance.isDone, it calls ReloadInstance.checkExceptions and hands the result to its finish callback. Success runs LevelExtractor.allChanged, which is why every chunk section rebuilds after F3+T, then ResourceLoadStateTracker.finishReload, DownloadedPackSource.onReloadSuccess and Minecraft.onResourceLoadFinished. Failure runs Minecraft.rollbackResourcePacks, which does not find the offending pack — it deselects every resource pack it can, clears the options lists, saves, and reloads again, and if vanilla was the only selected pack it rethrows and crashes instead. That recovery reload bypasses the one-at-a-time guard, skips the fade, and if it fails the client abandons recovery: Minecraft.abortResourcePackRecovery drops the overlay, disconnects any level and returns to the title screen with a failure toast. ShaderManager is constructed with Minecraft.triggerResourcePackRecovery for exactly this, so a shader that fails at runtime rather than at load takes the same road. Throughout, ResourceLoadStateTracker records what kind of reload this was and with which packs, so a crash report can say.

On the server there is no overlay; the finish is a continuation on the server thread, and the coda below lists it.

F3+T, end to end

sequenceDiagram
    participant KH as KeyboardHandler
    participant MC as Minecraft
    participant RRM as Reloadable<br/>ResourceManager
    participant SRI as SimpleReloadInstance
    participant Worker as Worker
    participant LO as LoadingOverlay

    KH->>MC: reloadResourcePacks, on the F3+T key mapping
    MC->>MC: PackRepository.reload, then openAllSelected: keep the selection
    MC->>RRM: createReload: close the old manager, build the new snapshot
    RRM->>SRI: create: prepareSharedState on every listener, then reload on each
    SRI->>Worker: every listener's prepare, all at once
    MC->>LO: new, then Gui.setOverlay: the logo and a bar from getActualProgress
    Worker->>SRI: each listener reaches its<br/>Preparable<br/>ReloadListener.<br/>PreparationBarrier
    SRI->>MC: BlockableEventLoop.execute: each barrier posts a task, the last opens the gate
    MC->>MC: apply, one listener per registration slot, between frames
    Note over LO: a later tick
    LO->>SRI: isDone, then checkExceptions
    LO->>MC: rollbackResourcePacks on failure, allChanged on success

The keypress, end to end, with the worker pool drawn as one lane for a whole pool. The two arrows into Minecraft from below are the reload handing work back to the Render thread: the barriers’ posted tasks, which open the gate for the applies, and the overlay’s callback once it finds the instance done.

The key does nothing but ask. KeyboardHandler.handleDebugKeys matches Options.keyDebugReloadResourcePacks and calls Minecraft.reloadResourcePacks. If a reload is already showing an overlay, the request is parked in Minecraft.pendingReload and drained from Minecraft.runTick, and a second request while one is parked simply returns the same future. The one path that bypasses the guard is a recovery reload after a failure. The rest is the five stages above: PackRepository.reload and PackRepository.openAllSelected on the Render thread, ReloadableResourceManager.createReload closing the old snapshot and building the new one, SimpleReloadInstance fanning prepares out to the resourceLoad pool and marshalling applies back through Minecraft, and LoadingOverlay.tick polling for the end.

The first reload of the game’s life runs the same way from the Minecraft constructor, tagged ResourceLoadStateTracker.ReloadReason.INITIAL rather than manual; a world being opened builds its own first snapshot through WorldLoader.load, whose WorldLoader.PackConfig.createResourceManager runs MinecraftServer.configurePackRepository and opens the packs; and a server-sent pack is just one more RepositorySource, so DownloadedPackSource triggers an ordinary Minecraft.reloadResourcePacks.

/reload, the same pipeline on the server

F3+T (client)/reload (server)
who starts itKeyboardHandler.handleDebugKeys → Minecraft.reloadResourcePacks, parked in Minecraft.pendingReload if an overlay is already upReloadCommand, at Commands.LEVEL_GAMEMASTERS → MinecraftServer.reloadResources
discoveryPackRepository.reload keeps the current selection; required packs are force-insertedReloadCommand.discoverNewPacks runs PackRepository.reload and then selects every available pack not in the world’s disabled list — which is how a datapack dropped into the folder is picked up
where the packs are openedon the Render thread, before the overlay goes upon the server thread first, one Pack.open per selected id, before any background work starts
the managera façade swap: ReloadableResourceManager.createReload closes the old MultiPackResourceManager and holds the new onea fresh MultiPackResourceManager inside a new MinecraftServer.ReloadableResources; the old one is closed only when the new one is installed, and the new one is closed if the reload fails
which thread applies, and whether it blocksthe Render thread, between frames; nothing blocksthe Server thread; if /reload is issued from the server thread the method blocks it with BlockableEventLoop.managedBlock until done — /reload stalls the tick
how many listenerstwenty-two, in registration orderone — ServerFunctionLibrary (ReloadableServerResources.listeners)
what is a registry insteadnothing; the client’s registries arrive over the wiretags are read before the reload instance by TagLoader.loadTagsForExistingRegistries and applied after it (tags); loot tables, predicates, item modifiers, recipes, advancements and the rest of RegistryDataLoader.RELOADABLE_REGISTRIES load as the RegistryLayer.RELOADABLE layer in ReloadableServerRegistries.reload (identifiers and registries); item component prototypes rebind through BuiltInRegistries.DATA_COMPONENT_INITIALIZERS (data components)
when success is reportedat the keypress, in the debug chat line, before the reload has done anythingbefore the reload runs — the success message is sent first, and a failure arrives later
what happens on completionLevelExtractor.allChanged · ResourceLoadStateTracker.finishReload · DownloadedPackSource.onReloadSuccess · Minecraft.onResourceLoadFinishedclose the old MinecraftServer.ReloadableResources · install the new · PackRepository.setSelected · write the new WorldDataConfiguration into level data · ReloadableServerResources.updateComponentsAndStaticRegistryTags · RecipeManager.finalizeRecipeLoading · PlayerList.saveAll · PlayerList.reloadResources — which re-reads every player’s advancements, broadcasts ClientboundUpdateTagsPacket and ClientboundUpdateRecipesPacket, and re-sends every player’s whole recipe book · ServerFunctionManager.replaceLibrary · StructureTemplateManager.onResourceManagerReload
what happens on failureMinecraft.rollbackResourcePacksthe new manager is closed, the old resources stay installed, and the command source is told
timingProfiledReloadInstance only when the logger is at debugthe same

Two of those rows are worth a second look. The command tree is rebuilt by every /reload — a new Commands inside the new ReloadableServerResources — and nothing re-sends it, so what a connected client holds is the old tree’s shape. It is less than it sounds, because both server-side parses read the new dispatcher immediately (Brigadier and commands). And the reload is debug-timed only: the “Resource reload finished after N ms” line, the per-listener timings and the total-blocking-time figure all come from ProfiledReloadInstance, selected only when the logger is at debug.

Across the wire

A server pushes a pack with ClientboundResourcePackPushPacket (id, URL, hash, required, prompt) and withdraws one with ClientboundResourcePackPopPacket, sent by ServerResourcePackConfigurationTask in the configuration phase and, in a development build, by ServerPackCommand (/serverpack push|pop) at any time in play; the client answers with a ServerboundResourcePackPacket and its ServerboundResourcePackPacket.Action. Packs are keyed by UUID and stack, and a server-sent pack pins itself to the top of the selection. ServerCommonPacketListenerImpl.handleResourcePackResponse disconnects a client that declines — but the “required” it consults is MinecraftServer.isResourcePackRequired, a server-wide server.properties setting, not the flag on the individual pack, so a declined /serverpack push can disconnect you on a server whose properties pack is required.

What the client keeps of them is a cache with a hard cap. DownloadQueue gives every pack a directory of its own under a cache root, runs the downloads one at a time on a ConsecutiveExecutor over Util.nonCriticalIoPool, appends every attempt to a JsonEventLog beside them, and calls DownloadCacheCleaner.vacuumCacheDir at construction to trim the root to DownloadQueue.MAX_KEPT_PACKS. The cap counts files, not packs — twenty, newest first, and one file per directory before any directory’s second — so with one file per pack it is the twenty newest packs that survive on disk, and the twenty-first is downloaded again next time.

The system is data-driven by pack.mcmeta (PackMetadataSection, with min_format / max_format replacing the integer pack_format), options.txt (Options.resourcePacks), level.dat’s WorldDataConfiguration (DataPackConfig enabled/disabled plus the FeatureFlagSet), server.properties for the server-sent pack, and allowed_symlinks.txt via DirectoryValidator.

Questions players ask

Why does my pack’s animated texture stop animating when another pack overrides the image? Because the .mcmeta is looked for in the winning pack or those above it, never below. The override won the image and brought no metadata.

Why does a datapack I dropped into the folder appear after /reload but a resource pack I dropped in does not after F3+T? ReloadCommand.discoverNewPacks selects every newly available pack; PackRepository.reload on the client only re-discovers and keeps the selection you had.

Why did F3+T turn all my packs off? A listener threw. The rollback does not know which pack did it, so it clears them all and reloads with only the packs it cannot remove: vanilla, and any pack a server sent that was already in use.

Why does /reload freeze the server? When it is issued from the server thread, MinecraftServer.reloadResources blocks that thread with BlockableEventLoop.managedBlock until the reload is done.

Why can I disable the vanilla data pack but not the vanilla resource pack? ClientPackSource marks it required; ServerPacksSource does not.

For a 1.21-era reader. CompositePackResources is OverlayedPackResources. The vanilla pack is not a PackResources any more: VanillaPackResources hands out FixedPathPackResources layers, and its asProvider is VanillaPackResources.asResourceManager. RecipeManager and ServerAdvancementManager are not reload listeners: recipes and advancements are reloadable registries, and ServerFunctionLibrary is the server’s one listener.

Where to look

PackType · PackResources · Pack · PackRepository · PackCompatibility · PackFormat · ServerPacksSource · ClientPackSource · BuiltInPackSource · FolderRepositorySource · MultiPackResourceManager · FallbackResourceManager · ReloadableResourceManager · PreparableReloadListener · SimplePreparableReloadListener · SimpleJsonResourceReloadListener · SimpleReloadInstance · ResourceLoadStateTracker · Minecraft.reloadResourcePacks · LoadingOverlay · ReloadCommand · MinecraftServer.reloadResources · ReloadableServerResources · WorldLoader · DownloadedPackSource · ServerPackManager


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Tags

Verified against Minecraft 26.3 · Part II · A parrot flies down through the canopy looking for a log to perch on, a moment after a data pack put a new block in the logs tag.

A parrot is looking for somewhere to sit. Parrot.ParrotWanderGoal scans the blocks around it and asks the block state beneath each one whether it is leaves or in BlockTags.LOGS — a leaves block it recognises by class, a log only by tag. Nothing in that goal names oak, or spruce, or the block a data pack added an hour ago; the tutorial toast that tells a new player to punch a tree asks the same question. A tag is a named set of registry entries, defined in data-pack JSON and tested against by key, and it is how data reaches into hard-coded behaviour without the behaviour naming any specific block. The registries page ended on a promise: a frozen registry’s contents never change. Its tags do. Type /reload with a data pack that adds a block to logs and the parrot perches on it a moment later — with BuiltInRegistries.BLOCK frozen since before the title screen. Nothing is unfrozen to do it, and no lock is taken; the swap is safe for a reason that has nothing to do with either.

The cast

classwhat it decidesthread
TagKeythe name: a registry key and an Identifier, interned so equal keys are one objectany
TagFile · TagEntrythe on-disk shape: a list of entries, each an element or a reference to another tag, and the replace flag—
TagLoaderreads every pack’s copy of every file and resolves them in dependency order; what an id resolves to is its TagLoader.ElementLookupWorker at world load, Server on /reload
MappedRegistrythe tag half: MappedRegistry.frozenTags, one HolderSet.Named per freeze-time key, and MappedRegistry.allTags, the bound MappedRegistry.TagSetread from any; written before the freeze by the launching thread, which binds a static registry’s tags to empty, and by a data-pack registry’s load tasks, and after it by whichever thread is applying
Registry.PendingTagsa loaded table not yet installed, with a Registry.PendingTags.lookup that hands out the new table’s tags, bound only when it is appliedbuilt on a worker at world load, on the server thread for /reload, on the Render thread from the packet; applied on the owning thread
HolderSet.Namedone tag’s contents, bound at each reload that keeps the tag—
Holder.Referenceone element’s own Set of TagKeys — the thing a membership test reads—
TagNetworkSerializationthe wire form: tag id to a list of registry intsServer encodes, client decodes

A tag is a key and a file

In code a tag is a TagKey: a record of the registry’s ResourceKey and an Identifier, interned through a weak interner so that TagKey.create always returns the canonical instance and a membership test is a set lookup on a canonical key. The keys are declared once, in catalogues that hold no contents — BlockTags, ItemTags, EntityTypeTags, BiomeTags, FluidTags and their siblings, twenty-one files in net/minecraft/tags including DamageTypeTags, EnchantmentTags, StructureTags, PoiTypeTags, FeatureTags and TimelineTags. A key that a block and its item share is declared once as a BlockItemTagId in BlockItemTags and projected into the catalogues that use it: BlockTags.LOGS is the block half of BlockItemTags.LOGS.

On disk a tag is a TagFile: a list of TagEntry and a replace flag. Each entry is an element id or a #-prefixed reference to another tag, with a required flag that defaults to true. The file lives at data/<namespace>/tags/<registry path>/<name>.json — Registries.tagsDirPath builds that string in exactly one place, and the registry path is singular: tags/block, tags/item, tags/entity_type, tags/worldgen/biome. Vanilla’s own files are written by the data generator (TagsProvider, TagBuilder), which the running game never calls, and players reach tags through the #tag syntax of ResourceOrTagArgument and ResourceOrTagKeyArgument (Part XIII). ResourceSelectorArgument, beside them, is a glob over ids and takes no tag at all.

Between the two sits TagLoader, generic over what an id resolves to: a Holder for a registry, a CommandFunction for function tags. It has two instance steps. TagLoader.load reads every pack’s copy of every file into lists of TagLoader.EntryWithSource; TagLoader.build resolves them in dependency order, TagLoader.tryBuildTag doing one tag at a time through the TagLoader.ElementLookup the loader was built with. Its output, a map from each tag to its holders, is resolved but bound to nothing. Nothing owns the loader: it is called as static functions from WorldLoader, MinecraftServer.reloadResources, ReloadableServerRegistries, the registry load tasks and the client’s RegistryDataCollector, with no manager between them. Function tags are the exception — ServerFunctionLibrary is a real reload listener and runs its own TagLoader inside it. Everything in net/minecraft/tags ships in both jars.

Inside a MappedRegistry a tag is two things. MappedRegistry.frozenTags holds one canonical HolderSet.Named per key for the tags that existed when the registry froze, and MappedRegistry.allTags is a MappedRegistry.TagSet, the bound view, which starts out as MappedRegistry.TagSet.unbound, where every read throws. Beside the registry, each Holder.Reference carries its own Set of TagKeys, bound by Holder.Reference.bindTags; that set, not the registry, is what Holder.Reference.is reads.

The four moments tags are loaded

World load comes first, on the worker pool (anatomy). TagLoader.loadTagsForExistingRegistries runs before the reload listeners, over the RegistryLayer.STATIC layer, and produces a Registry.PendingTags for every static registry that has at least one tag that builds. The pending tables are made visible to worldgen and reloadable-registry loading through TagLoader.buildUpdatedLookups, and applied, one registry at a time, in ReloadableServerResources.updateComponentsAndStaticRegistryTags once every listener has finished. For the whole of a load the old tags are what Registry.getTags answers and the new table’s names are what the loading codecs see.

/reload is the same call on the server thread, handed the composite access of MinecraftServer.registries — so it re-reads and re-applies tags for the dynamic worldgen registries too, not only the static ones. Of the loading paths only the reloadable registries re-run — loot tables, recipes, advancements and the rest — and a worldgen registry keeps its elements and gets new tags.

The third is a data-pack registry, which loads its tags inside its own load task rather than through the mechanism above: ResourceManagerRegistryLoadTask reads them after its elements, with TagLoader.ElementLookup.fromGetters, and RegistryLoadTask.registerTags binds them under the registry’s write lock — before the freeze, so there is a lock here and no swap. (The reloadable registries take this path too — they load through the same load tasks — so a data pack’s loot-table tags bind like any other’s, and vanilla ships none.)

The fourth moment is the client’s. One ClientboundUpdateTagsPacket covering every static and every synced registry is sent by SynchronizeRegistriesTask after the registry data. In configuration the client merely buffers it — ClientConfigurationPacketListenerImpl.handleUpdateTags hands it to RegistryDataCollector.appendTags, and nothing resolves until configuration finishes. After a server /reload, PlayerList.reloadResources broadcasts the same packet into the play phase, and ClientPacketListener.handleUpdateTags handles that one at once.

From JSON to a parrot’s decision

sequenceDiagram
    participant WL as WorldLoader
    participant TL as TagLoader
    participant MR as MappedRegistry
    participant RSR as Reloadable<br/>ServerResources
    participant Parrot as Parrot

    Note over WL,MR: world load, on the worker pool
    WL->>TL: loadTagsForExistingRegistries over the STATIC layer
    TL->>TL: load: every pack's tags/block/logs.json, lowest first
    TL->>TL: build: DependencySorter orders the nested tags, then tryBuildTag
    TL->>MR: prepareTagReload: a Registry.PendingTags, which Registry.getTags cannot see yet
    Note over WL,RSR: worldgen and reloadable-registry codecs see the new table through Registry.PendingTags.lookup
    Note over MR,RSR: the thread driving the load, after the last reload listener has applied
    RSR->>MR: Registry.PendingTags.apply: bind, swap, rebind every holder
    Note over Parrot: a later server tick, the parrot's wander goal
    Parrot->>Parrot: state.is(BlockTags.LOGS) — a set lookup on the holder's own tags
    Note over MR,Parrot: no arrow to the registry: the check never reaches it

One tag, from the pack files to the check that uses it. The gap between MappedRegistry.prepareTagReload and Registry.PendingTags.apply is where the opening’s riddle lives, a table built that Registry.getTags cannot yet see, and the last note marks an absence: the parrot’s check touches no registry at all.

Every pack’s file, lowest first

TagLoader.load asks the resource system for resource stacks — a FileToIdConverter.json over the tag directory, listed with FileToIdConverter.listMatchingResourceStacks — so every copy of tags/block/logs.json across the enabled packs is visited in priority order and merged, and a replace in a higher pack discards what the lower packs contributed to that id. A pack whose file fails to parse is logged and skipped, never fatal.

Tags of tags resolve in order, and a tag with a hole is dropped whole

The vanilla logs file names no block at all: it is three tag references, logs_that_burn, crimson_stems and warped_stems, and logs_that_burn is in turn ten references, oak_logs among them, before oak_logs finally lists four blocks. TagLoader.build feeds every tag reference into a DependencySorter and resolves the leaves first. A tag with any failing entry — a missing required element as much as a missing required tag reference — is dropped whole, not loaded minus the entry, and is then absent from Registry.getTags, so neither the network payload nor a lookup will find it. An optional entry (required: false) resolves to nothing and the tag still builds: TagEntry.build answers not required on a miss, whichever lookup it was handed. What the two lookups differ on is where a required id is looked up, and the difference decides what a missing one costs. TagLoader.ElementLookup.fromFrozenRegistry, used for a registry that has already frozen, asks it either way: a name it does not hold is a miss, and the tag is dropped. TagLoader.ElementLookup.fromGetters, used by data-pack registries as they load, sends a required id through the registration lookup, which manufactures a placeholder Holder.Reference for a name nothing has registered (MappedRegistry.createRegistrationLookup), and an optional one through the immutable lookup, which does not. A data-pack registry has registered all its elements before it reads its tags, so a placeholder there is never bound: the registry’s freeze fails, with unbound values, and takes the whole load with it.

Prepared, then applied

This is the swap the opening promised, and it is three steps and no lock. MappedRegistry.prepareTagReload refuses a registry that is not frozen and builds the new table, reusing the freeze’s HolderSet.Named objects where it can; nothing is installed yet, and the Registry.PendingTags.lookup it hands back answers with the new table’s names, which is what the worldgen and reloadable-registry codecs are given while they load. Registry.PendingTags.apply is then three ordered steps: bind each HolderSet.Named, swap the MappedRegistry.TagSet, then rebind every holder’s tag set in MappedRegistry.refreshTagsInHolders. Each step replaces an immutable collection with one reference write, so a reader on another thread — worldgen on the worker pool during a /reload, the Render thread in singleplayer — sees a holder’s old tags or its new ones and never a half-built set; what it can see, for the length of the rebind, is some holders already new and others still old. Which thread applies depends on the occasion: at world load the Server thread does not exist yet, so the apply runs on whichever thread is driving the load — the launching thread on a dedicated server, through Util.blockUntilDone, and the Render thread on the client. Only a reload of a running server — /reload, or /datapack enable or /datapack disable — applies on the Server thread.

The client gets integers

TagNetworkSerialization.serializeTagsToNetwork walks RegistrySynchronization.networkSafeRegistries and writes each tag as a list of registry ids, dropping any registry whose payload came out empty. That set is every RegistryLayer.STATIC registry, unconditionally, concatenated with the synced dynamic ones — RegistrySynchronization.isNetworkable filters only the second group. Ids for dynamic registries are meaningful only once both sides have built the same registry in the same order, which is why SynchronizeRegistriesTask sends the registry data first. In the configuration phase that ordering is a send-order constraint rather than a handling one: the client buffers both packets and resolves everything at the end. The play-phase packet resolves immediately against the live registry access, and the client then rebuilds the creative-inventory search tree from the new tags.

Singleplayer skips only what it already has

On the play path ClientPacketListener.handleUpdateTags always prepares, and skips only the apply on a memory connection, because the integrated server’s apply already rebound the registries both halves share. In configuration the suppression is narrower still: only the non-networkable (static) registries’ tags are skipped, and the client still binds tags on its own copies of the remote dynamic registries, copies it then discards for the server’s own (identifiers and registries).

The check is a field read

BlockBehaviour.BlockStateBase is a TypedInstance; TypedInstance.is asks the type holder — for a block, Block.builtInRegistryHolder, the intrusive holder from identifiers-and-registries — and Holder.Reference.is is set-contains on an interned TagKey. No registry is consulted. Parrot (the perch search), TrunkPlacer (worldgen) and the client’s PunchTreeTutorialStepInstance all ask this way; FluidState, Entity and ItemStack go through the same interface.

The other way tags cross the wire

ClientboundUpdateTagsPacket is not the only one. ByteBufCodecs.holderSet encodes a HolderSet.Named as a marker plus the tag’s Identifier, and decodes it by looking the tag up in the receiving side’s registry — so any packet or data component carrying a tag-shaped HolderSet hard-fails on a client that does not have that tag. That, more than the id numbering, is why the tags packet must reach the client before play traffic does.

The same idea appears in data: TagKey.hashedCodec, HolderSetCodec and RegistryCodecs are what turn “#minecraft:logs” in an ordinary JSON field — a recipe ingredient, a loot condition, a placement predicate — into a HolderSet without any of those files being tag files.

Questions players ask

What if two tags reference each other? Both are dropped, and both are logged. DependencySorter.addDependencyIfNotCyclic drops the edge that would close the cycle, so an order exists and the sort does not hang — but whichever tag is built first asks for a tag that is not in the new table yet, gets nothing, and fails as a missing required reference; the second then asks for the first, which failed, and fails the same way. Two Couldn’t load tag lines, and neither tag exists.

What happens to a tag a /reload deleted? It keeps its old contents, and nothing tells you. The tag is absent from the pending map, so apply neither rebinds nor clears it: anything still holding that HolderSet.Named will iterate the old list, while HolderSet.Named.contains — which reads the holder’s refreshed tag set — answers false, and Registry.get for the key answers empty.

Is picking a random element from a tag deterministic? Yes, per pack stack. Duplicate entries collapse and file order is preserved — TagLoader.tryBuildTag collects into an insertion-ordered set — so iterating a tag, or picking from it with HolderSet.getRandomElement, gives the same sequence for the same packs.

Which tags does a client never hear about? Those of RegistryLayer.RELOADABLE-layer registries (loot tables, predicates) and of non-synced worldgen registries (features, structures) — and on the receiving side, ids the client’s own registry does not know are dropped from the payload silently.

For a 1.21-era reader. There is no TagManager and no reload listener for registry tags: loading is static functions on TagLoader, called from WorldLoader, MinecraftServer.reloadResources, ReloadableServerRegistries, the registry load tasks and the client’s RegistryDataCollector, and only function tags still go through a real listener. The directory is singular and has no plural fallback — tags/block, not tags/blocks — because Registries.tagsDirPath builds that string in one place and accepts nothing else.

Where to look

TagKey · BlockTags · TagFile · TagLoader · Registry.PendingTags · MappedRegistry (the tag half) · HolderSet.Named · ReloadableServerResources · TagNetworkSerialization · Parrot.ParrotWanderGoal


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Data components

Verified against Minecraft 26.3 · Part II · A player types /give @s diamond_sword[enchantments={sharpness:3}], and later rolls Sharpness onto a plain sword at the enchanting table: what the square brackets are, and how the client finds out.

A player types /give @s diamond_sword[enchantments={sharpness:3}]. The part in square brackets is a patch: one keyed, typed value laid over the sword. A data component is exactly that — one typed, keyed piece of data attached to an item stack: its damage, its enchantments, its lore, the food it is, the armour slot it goes in. The item type supplies a prototype map of components; a stack carries only a patch against that prototype. Everything that used to be “the NBT on an item” is a component with a codec, and most of what used to be a subclass of Item carrying behaviour (a sword, a piece of armour) is now a kit of components on a plain Item. The surprise is where the prototype comes from. It is not built in the item’s constructor: Item.Properties only records an initializer, and the map is built again on every reload with the world’s registries in hand — DataComponentInitializers.build on the reload worker, Holder.Reference.bindComponents on the owning thread. That is why a data pack can change what a jukebox plays without touching the item, and why a stack cannot even be decoded before the first reload: Item.CODEC_WITH_BOUND_COMPONENTS guards on Holder.areComponentsBound.

The cast

classwhat it decidesthread
DataComponentTypethe key: which codec writes the value to disk, which to the wire, and whether it is saved at allstatic, registered at bootstrap
DataComponentMapthe map interface; a prototype is an immutable, identity-keyed oneany; a prototype is never mutated
PatchedDataComponentMapwhat an ItemStack owns: a shared prototype, a patch, and whether the patch is still someone else’swhichever thread owns the stack
DataComponentPatchthe serialisable form of the patch: additions and removalsNetty for packets; server and workers for saves
DataComponentInitializersbuilds every registry element’s prototype, from the recorded initializers, once the registries existthe reload worker on the server; the Render thread on the client
Holder.Referenceholds the bound prototype for one registry element, and throws until it has onebound by the thread driving a world load, and by the server thread on /reload; on the client at the end of configuration
ItemStackthe writes (ItemStack.set, ItemStack.update, ItemStack.remove, ItemStack.copyFrom) and the reads it inherits from DataComponentHolderwhichever thread owns the stack
DataComponentLookupthe reverse index: which elements of a registry carry this component valuebuilt at freeze, populated lazily

Everything here ships in both jars; in the trace below only ClientPacketListener and RegistryDataCollector are client-only.

The shape of a stack

classDiagram
    class Item {
        Holder.Reference builtInRegistryHolder
    }
    class ItemStack {
        PatchedDataComponentMap components
    }
    class PatchedDataComponentMap {
        DataComponentMap prototype
        Map patch
        boolean copyOnWrite
    }
    class DataComponentPatch {
        added values
        removed keys
    }
    class DataComponentType {
        a persistent codec
        a network codec
        or only the latter
    }
    ItemStack *-- PatchedDataComponentMap
    PatchedDataComponentMap ..> Item : many stacks, one prototype
    PatchedDataComponentMap --> DataComponentPatch : asPatch
    DataComponentPatch --> PatchedDataComponentMap : fromPatch, given a prototype
    DataComponentType <.. Item : keys the prototype
    DataComponentType <.. DataComponentPatch : keys the patch

What a stack holds and what it only points at: a stack owns its PatchedDataComponentMap and that map owns the patch, while the dotted line to Item points at a prototype every diamond sword made between two reloads shares and none can write. DataComponentPatch is the form that leaves, on disk and on the wire, and PatchedDataComponentMap.fromPatch is how it comes back.

The item’s prototype is built by DataComponentInitializers and bound onto the item’s Holder.Reference, which a section below is about. A stack points at that shared prototype and owns only a patch over it, in which a removal is a marker value, Removed.INSTANCE. The patch’s serialisable twin is a DataComponentPatch, and the key of every entry in all of them is a DataComponentType. The rest of the page is a tour of those objects, each grounded in one small trace: Sharpness III arriving on a sword at the enchanting table.

The key: DataComponentType

A type is built by DataComponentType.Builder. DataComponentType.Builder.persistent gives the disk and JSON codec; DataComponentType.Builder.networkSynchronized a hand-written wire codec. A type with no persistent codec is transient (DataComponentType.isTransient): never saved, only sent. DataComponentType.Builder.networkSynchronized is not a gate — without it, DataComponentType.Builder.build derives a wire codec from the persistent codec (NBT over the wire), and with neither it throws. The only real switch is transient-versus-persistent, and it gates saving. Three types exist only on the wire: DataComponents.CREATIVE_SLOT_LOCK, DataComponents.ADDITIONAL_TRADE_COST, DataComponents.MAP_POST_PROCESSING.

122 vanilla types are registered in DataComponents into BuiltInRegistries.DATA_COMPONENT_TYPE; 30 of them have slash-shaped ids (villager/variant and its siblings). The catalogue is reference/components. Component types are code, in a registry data packs cannot extend; what data packs reach is the prototype (below) and CustomData, which carries arbitrary NBT for them to use.

Two more flags live on the builder. DataComponentType.Builder.cacheEncoding routes a type’s Codec encodes — disk and JSON, not the wire — through EncoderCache (DataComponents.ENCODER_CACHE). DataComponentType.Builder.ignoreSwapAnimation is set on exactly one type, DataComponents.DAMAGE, and the first-person hand that reads it belongs to items and stacks. Underneath, DataComponentType.PERSISTENT_CODEC and DataComponentType.VALUE_MAP_CODEC are the shared dispatch machinery that DataComponentMap.CODEC and the exact predicate are built on.

The maps

A prototype DataComponentMap is immutable and identity-keyed; DataComponentMap.Builder builds one and can carry a DataComponentMap.Builder.addValidator, which is where the prototype-time structural rule lives (below). DataComponentMap.EMPTY is the map every registry element gets when nothing declared one. DataComponentMap.composite is dead API: it exists, and nothing in 26.3 calls it; the layering that actually happens is PatchedDataComponentMap’s prototype-plus-patch.

PatchedDataComponentMap is the map an ItemStack owns: a PatchedDataComponentMap.prototype shared with every stack of that item made since the last reload, a PatchedDataComponentMap.patch in which the marker Removed.INSTANCE means removed from the prototype, and a PatchedDataComponentMap.copyOnWrite flag. It sanitises on every write: PatchedDataComponentMap.set stores nothing when the value equals the prototype’s, and PatchedDataComponentMap.remove stores a removal marker only if the prototype had the key. In the trace, the sword’s prototype carries an empty ItemEnchantments (every item’s does, through DataComponents.COMMON_ITEM_COMPONENTS), so Sharpness III differs from the prototype and the patch gains one entry. Set the enchantments back to empty and the entry vanishes rather than becoming an explicit default.

The first write pays for the copy. PatchedDataComponentMap.ensureMapOwnership clones the backing map only when PatchedDataComponentMap.copyOnWrite is set; ItemStack.copy, PatchedDataComponentMap.asPatch and ItemStack.transmuteCopy can all alias the same map and set the flag, so copying a stack is O(1) until someone writes. ItemStack.applyComponentsAndValidate relies on that: it snapshots with PatchedDataComponentMap.asPatch, applies, runs ItemStack.validateStrict, and on failure PatchedDataComponentMap.restorePatch.

Equality is prototype plus patch. ItemStack.isSameItemSameComponents bottoms out in PatchedDataComponentMap.equals. Because setting a value equal to the prototype default removes it from the patch, two stacks that reached the default by different routes compare equal.

The patch, on the wire and on disk

DataComponentPatch is the serialisable form: additions and removals. DataComponentPatch.CODEC writes removals as !minecraft:foo; DataComponentPatch.STREAM_CODEC writes two counts then the entries; DataComponentPatch.DELIMITED_STREAM_CODEC is the length-prefixed variant for untrusted input; and DataComponentPatch.split is the added/removed decomposition that the hashing and block-entity paths are both built on. TypedDataComponent — a type with its value — has its own “type id then value” stream codec, distinct from the patch encoding, for the places one value travels alone.

The wire patch never contains defaults — for a real stack. Because PatchedDataComponentMap sanitises on every write, a value equal to the prototype’s is dropped rather than sent. ItemStackTemplate is the exception, and items and stacks says why.

On the network the patch travels inside every ItemStack in ClientboundContainerSetSlotPacket, ClientboundContainerSetContentPacket, ClientboundSetCursorItemPacket, ClientboundSetPlayerInventoryPacket and ClientboundSetEquipmentPacket; the client answers ordinary clicks with hashes, not stacks, and the creative slot alone sends a full, re-validated stack — codecs, NBT and JSON owns the four serialisations of a stack, and containers and menus owns what the server does with a hash. On disk an item is saved as a patch (ItemStack.MAP_CODEC’s “components”, with transient types silently dropped), but a block entity as a full DataComponentMap — the two are not symmetric.

The prototype, and why it is built at reload

A prototype is recorded once and built many times, and the two happen far apart in the program’s life. The recording is at class-init, long before a world exists. Bootstrap.bootStrap has loaded the Items class before BuiltInRegistries.bootStrap runs — the dispenser and cauldron bootstraps name items — and its static initialisers each build an Item, so every item in the game exists, and no item has components. An Item constructor registers its initializer in BuiltInRegistries.DATA_COMPONENT_INITIALIZERS and nothing more. Item.Properties.component and its convenience methods (Item.Properties.durability, Item.Properties.food, Item.Properties.equippable, Item.Properties.sword, …) only record a DataComponentInitializers.Initializer; nothing is a map yet. Item.Properties.delayedComponent and Item.Properties.delayedHolderComponent are the reason it must be deferred: they name registry entries that do not exist until a world’s registries do — an item’s DataComponents.JUKEBOX_PLAYABLE or DataComponents.DAMAGE_RESISTANT names an entry a data pack can change.

They are also rare. Between them the two methods have twenty-six call sites in the entire game, all inside Item and Items: fire resistance resolving DamageTypeTags.IS_FIRE, the banner patterns, the goat horn, the jukebox songs, the egg variants, the spear’s damage type, the axe, shovel and hoe’s block transformers, the pottery sherds, the trim materials and a handful more. Everything else is baked literal. Item.Properties.repairable is the near miss that shows where the line runs — it takes a tag, which sounds like context, and is still eager, because it asks BuiltInRegistries.acquireBootstrapRegistrationLookup at class-init and stores an unresolved HolderSet instead of waiting for one. So the deferral exists for twenty-six call sites, and everything else is deferred along with them because the map is built in one pass.

The maps are built with full registry context — tags, damage types, jukebox songs resolvable — by DataComponentInitializers.build, reached from ReloadableServerResources.loadResources on the reload worker on the server, and installed with Holder.Reference.bindComponents on the owning thread: on the server in ReloadableServerResources.updateComponentsAndStaticRegistryTags (by the thread driving a world load, and by the server thread on /reload), on the client in RegistryDataCollector at the end of configuration. A /reload therefore rebinds every item’s prototype. Every registry element gets a component map (DataComponentMap.EMPTY if it had no initializer), so EntityType holders have one too — with one asymmetry, and it runs the way round you would not guess. ClientConfigurationPacketListenerImpl passes Connection.isMemoryConnection into RegistryDataCollector.collectGameRegistries, which negates it, so a singleplayer client binds only the registries RegistrySynchronization.isNetworkable accepts and a multiplayer client binds every one. Singleplayer can skip the rest because it shares BuiltInRegistries with the integrated server, whose own apply has already bound them.

Before binding, reading a registry element’s components throws, and the failure is a null-check, not a friendly error: Item.components merely delegates, the throw comes from Holder.Reference.components, and the non-throwing question is Holder.areComponentsBound. Item.CODEC_WITH_BOUND_COMPONENTS guards on it and refuses to decode a stack until then.

Eleven entries in DataComponents.COMMON_ITEM_COMPONENTS make the map every item’s prototype starts from. Notably it puts an empty ItemEnchantments on every item, which is what ItemStack.isEnchantable depends on: it gates first on DataComponents.ENCHANTABLE being present, and then on DataComponents.ENCHANTMENTS being present and empty.

A prototype can carry a validator. DataComponentMap.Builder.addValidator is where a structural rule is installed, and Item.Properties installs one: an item may not be both damageable and stackable. The stack-time twin of that rule, and the rest of what ItemStack.validateStrict refuses, belongs to items and stacks.

The reverse index: DataComponentLookup

Every frozen MappedRegistry builds one (Registry.componentLookup): a lazily-populated reverse index answering “which elements carry this component value?”, which is how the game finds the spawn egg for an entity type or the item for a dye colour. It scans the bound prototypes the holders carry the first time a component type is asked for and keeps what it found, so before the first reload it throws, and a /reload that changes a prototype leaves its old answer in place.

The readers and the predicates

DataComponentGetter reads one component; DataComponentHolder reads one and has a map, and is implemented only by ItemStack, which is where DataComponentHolder.get, DataComponentHolder.getOrDefault and DataComponentHolder.has come from. ItemInstance is the read-only face over ItemStack and ItemStackTemplate that predicates take (items and stacks). Beyond item behaviour, the callers are loot functions (CopyComponentsFunction) and the /give, /item and /loot commands — which is where the square brackets this page opened on are finally turned into a patch: ItemParser reads the text component by component through each DataComponentType’s own codec (codecs, NBT and JSON), and what it produces is the same DataComponentPatch the wire and the disk carry.

The predicates come in two strengths. DataComponentExactPredicate requires every listed component to equal. The partial DataComponentPredicate family under core/component/predicates — DataComponentPredicates, 15 kinds, in their own BuiltInRegistries.DATA_COMPONENT_PREDICATE_TYPE registry — matches a shape rather than a value, and DataComponentMatchers joins the two for ItemPredicate.

The values, by package

packagevalue types
world/item/componentConsumable, Tool, Weapon, BlocksAttacks, ItemLore, CustomData, TooltipDisplay, ItemContainerContents, BundleContents, TypedEntityData …
world/item/equipmentEquippable
world/item/enchantmentItemEnchantments, Enchantable, Repairable

Most combat has left the Item subclasses. Weapon, BlocksAttacks, KineticWeapon, PiercingWeapon, AttackRange, SwingAnimation and Tool are components; Item.Properties.sword, Item.Properties.spear and Item.Properties.humanoidArmor build whole kits, and SwordItem is gone; MaceItem, BowItem, CrossbowItem and TridentItem are among the few that still carry their own. The tools that act on a block went the same way: stripping, path-making and tilling are DataComponents.BLOCK_TRANSFORMER, which Item.Properties.axe, Item.Properties.shovel and Item.Properties.hoe point at BlockTransformers.AXE, BlockTransformers.SHOVEL and BlockTransformers.HOE, and which Item.useOn runs for an item that carries it.

Sharpness onto a sword, and how the client is told

sequenceDiagram
    box transparent the server
    participant EM as EnchantmentMenu
    participant IStack as ItemStack
    participant PDM as PatchedData<br/>ComponentMap
    end
    box transparent the client
    participant CPL as ClientPacketListener
    end

    Note over EM: the server thread, the button-click packet has arrived
    EM->>IStack: enchant, once per EnchantmentInstance
    Note over EM,IStack: a book goes through ItemStack.transmuteCopy first: the same patch, a new prototype
    IStack->>IStack: EnchantmentHelper.<br/>updateEnchantments, then set
    IStack->>PDM: set: ensureMapOwnership clones the shared map, then patch.put
    Note over PDM: the patch is now one entry: enchantments to sharpness 3
    EM->>EM: broadcastChanges, still inside the handler — this slot does not match
    EM->>CPL: ClientboundContainerSetSlotPacket: count, item id, the patch
    Note over CPL: decoded on Netty, the patch laid over the client's own prototype
    CPL->>CPL: handleContainerSetSlot, then the menu's setItem

One write on the server and one slot packet for the sword, and what crosses is the diff alone. The client never receives the prototype: it rebuilds the stack by laying the patch over the copy it already has, which is why a component the server never changed costs nothing to send.

The menu owns the mutation. EnchantmentMenu.clickMenuButton runs under ContainerLevelAccess.execute on the server thread and calls ItemStack.enchant once per chosen EnchantmentInstance. (A book takes one step more first: ItemStack.transmuteCopy builds a new stack carrying the same patch over the enchanted book’s prototype.) ItemStack.enchant hands the edit to EnchantmentHelper.updateEnchantments, which reads the current ItemEnchantments, edits a mutable copy and writes the immutable result back with ItemStack.set — under DataComponents.STORED_ENCHANTMENTS for an Items.ENCHANTED_BOOK and DataComponents.ENCHANTMENTS for everything else, which is the one place the book branch reaches past the transmute; the enchanting rules, the lapis, the seed and /enchant are Part VII’s. What matters here is that enchantments are a value, not a list on the stack: one component, one write.

One write, one patch entry. ItemStack.set is PatchedDataComponentMap.set. The map the sword owned was shared with whatever it was copied from, so PatchedDataComponentMap.ensureMapOwnership clones it now; the new ItemEnchantments differs from the prototype’s empty one, so the patch gains its single entry. A book that was transmuted a moment earlier carries the same patch over a different prototype, which is the whole meaning of ItemStack.transmuteCopy.

The menu compares, and here it does not wait for the tick. A menu-button click, like an ordinary click, is answered inside the handler that accepted it (containers and menus), so the correction leaves at once. The sword’s slot no longer matches what the client was last told, so ClientboundContainerSetSlotPacket goes out, and only the patch crosses: ItemStack.OPTIONAL_STREAM_CODEC writes the count, Item.STREAM_CODEC (a registry id) and the patch. The client answers later clicks with a HashedStack of CRC32C checksums rather than stacks (codecs, NBT and JSON for the hashing, containers and menus for the click protocol).

The client rebuilds against its own prototype. The decoder constructs ItemStack from holder, count and patch, and that constructor is PatchedDataComponentMap.fromPatch against the client’s own bound prototype — the map RegistryDataCollector bound at the end of configuration, or in singleplayer the server’s own. That is the reason components must be bound on the client before the play phase: an unbound prototype would throw in a packet decoder on a Netty thread. ClientPacketListener.handleContainerSetSlot then hands the stack to AbstractContainerMenu.setItem, and the Sharpness line in the tooltip, when the sword is hovered, is ItemEnchantments.addToTooltip reading the same component.

Components on things that are not items

Block entities, both directions. Placing runs BlockEntity.applyComponentsFromItemStack, which hands subclasses a recording DataComponentGetter: whatever BlockEntity.applyImplicitComponents reads is forgotten from the patch (DataComponentPatch.forget) and only the leftovers persist as opaque BlockEntity.components. Two types are pre-seeded into that forget set regardless of whether anything reads them — DataComponents.BLOCK_ENTITY_DATA and DataComponents.BLOCK_STATE — and only the added half of the resulting patch is kept, so removals are discarded. Breaking or picking runs the reverse, BlockEntity.collectComponents over BlockEntity.collectImplicitComponents, with BlockEntity.removeComponentsFromTag de-duplicating what was promoted; BlockItem.setBlockEntityData is the write path for the opaque blob.

Entities, a view with a narrow write path. Entity implements DataComponentGetter with no patch of its own: Entity.get answers DataComponents.CUSTOM_NAME and DataComponents.CUSTOM_DATA by hand, lets subclass overrides (Sheep, Wolf, Villager …) answer the variant-shaped types, and otherwise falls through to its EntityType holder’s bound prototype — the same map every registry element gets. Entity.applyComponentsFromItemStack is the write path, and it is not only spawn eggs: any item-to-entity spawn, an arrow picking up its stack and a lingering potion’s cloud all take it.

Where to look

DataComponentType · DataComponents · DataComponentMap · PatchedDataComponentMap · DataComponentPatch · DataComponentInitializers · Holder.Reference · DataComponentLookup · Item.Properties · ItemStack · ItemInstance · ItemStackTemplate · EnchantmentMenu · AbstractContainerMenu · BlockEntity · Entity (the getter half)


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Text components

Verified against Minecraft 26.3 · Part II · A player dies: the message that names the killer is built on the server, crosses the wire as a translation key, and is worded on the client.

An arrow lands and a player drops. A line appears in everyone’s chat, and on the victim’s screen the death screen says who did it. The server built that message on its own thread, inside CombatTracker.getDeathMessage, and sent it twice: once in ClientboundPlayerCombatKillPacket to the victim, once as system chat to everybody. But the server never wrote the sentence. What it built was a Component whose contents are a translation key — death.attack.arrow — and two arguments, the victim’s name and the killer’s, each of them another Component carrying a hover card, and a player’s a click action too. The packet crosses the wire as NBT. On the client the packet is decoded, handed to a DeathScreen, and still nobody has read the key. The client receives the death message before it knows what it says. The death screen’s words are chosen on the first frame that draws it, when TranslatableContents asks the client’s own Language for the template behind the key, so two players watching the same death read two different sentences from one packet — and a dedicated server, which logs the message too, only ever reads it in English.

The cast

classwhat it decidesthread
Componentthe interface: contents, style, siblings, and a walk over them in logical orderbuilt wherever text is made — a server tick, a command, the Render thread
MutableComponentthe only implementation; MutableComponent.append and MutableComponent.withStyle are how a tree is builtas above
ComponentContents · TranslatableContentswhat a node says: seven kinds, of which the translatable kind is the one that waits for a Languageworded on whichever thread first visits it
Stylethe eleven inheritable fields, immutable, Style.EMPTY the shared blankany
ComponentSerializationthe one recursive codec, and the NBT stream codecs built over itNetty, in PacketEncoder and PacketDecoder; Server or Render for data on disk
ComponentUtilsComponentUtils.resolve: selectors, scores and NBT paths into text, against a ResolutionContextServer, for commands and for the text of a sign, a text display, a written book or a set_name loot function; the client, for a server list’s MOTD
Languagea key to a template; the client swaps the instance on every resource reloadwhichever thread words a message reads it; Language.inject swaps it
ClickEventwhat a click on styled text may do, and the one action a server may not sendRender thread, when the click lands

A component is three things

classDiagram
    class Component {
        <<interface>>
        getStyle
        getContents
        getSiblings
        getVisualOrderText
    }
    class MutableComponent {
        ComponentContents contents
        Style style
        List siblings
    }
    class Style {
        eleven nullable fields
        null means inherit
    }
    Component <|.. MutableComponent : the only implementation
    MutableComponent *-- ComponentContents : one of seven kinds
    MutableComponent *-- Style
    MutableComponent o-- Component : each sibling is one

The triple, and the loop that makes it a tree: a component holds one ComponentContents, one Style and a list whose members are components again, the hollow diamond where the recursion is. Nothing here holds an inherited style — Style.applyTo merges a child’s over its parent’s during the walk, and the child records nothing.

Component is an interface with four abstract members — Component.getStyle, Component.getContents, Component.getSiblings and Component.getVisualOrderText, the last of which is why MutableComponent caches a laid-out form — and exactly one implementation, MutableComponent. A component is a triple: one ComponentContents, one Style, and an ordered list of sibling components. Style inheritance happens during traversal, not in storage: Component.visit applies a node’s own style over its parent’s with Style.applyTo and hands the result down to the contents and then to each sibling in turn, so a red parent with a plain child draws the child red, and nothing in the child records it. The mutability is in the name. Component.literal, Component.translatable and the other factories return a MutableComponent, and a tree is built by MutableComponent.append and MutableComponent.withStyle, which replace the style field with the Style the change produces. Component.copy is a shallow copy — the sibling list is new, the siblings are shared.

The walk is the only way to read one. Component.getString visits every node and concatenates what it says; Component.getString with a limit stops at that many characters. Everything from the walk onwards — the codepoint stream, bidirectional reordering, glyphs — belongs to text and fonts.

The seven kinds of contents

Each kind has a static factory on Component:

kindclassmade by
textPlainTextContents, with PlainTextContents.LiteralContentsComponent.literal
translatableTranslatableContentsComponent.translatable
keybindKeybindContentsComponent.keybind
scoreScoreContentsComponent.score
selectorSelectorContentsComponent.selector
nbtNbtContentsComponent.nbt
objectObjectContentsComponent.object

What each kind says, and when

Text says its string and nothing more; the empty string is the shared PlainTextContents.EMPTY, which is what Component.empty and CommonComponents.EMPTY hold.

Translatable is a key, an optional fallback and an array of arguments. An argument is a number, a boolean, a string or another Component — TranslatableContents.isAllowedPrimitiveArgument is the test, and Component.translatableEscape turns anything else into its string form before it can reach the codec. The template the key names is read by TranslatableContents.decompose, which accepts only %s, %n$s and %%: any other specifier is a TranslatableFormatException, and the component then shows the raw template string instead. The decomposition is cached against the identity of the Language that produced it (TranslatableContents.decomposedWith), so a language switch re-words every component the next time it is visited, without anything being re-sent.

Keybind names a key binding and asks KeybindResolver.keyResolver for its current name; the default resolver answers with the name itself, and the client installs KeyMapping.createNameSupplier in the Minecraft constructor, so key.jump reads as whatever key is bound on a client and as key.jump anywhere else.

Score, selector and nbt are the three kinds a server has to resolve. A score names a holder — an entity selector, a literal name, or ScoreHolder.WILDCARD for the context’s own entity — and an objective. A selector holds a compiled EntitySelector and an optional separator; visited unresolved, it shows the selector’s source text. An nbt component holds an NbtPathArgument.NbtPath, a separator, and one of three DataSources — EntityDataSource, BlockDataSource, StorageDataSource — and either prints what it finds through TextComponentTagVisitor or, with interpret, parses each match as a component; interpret and plain are refused together. Visited without resolution, score and nbt contents say nothing at all.

Object puts a picture inside text. Its ObjectInfo is an AtlasSprite (an atlas and a sprite, the block atlas by default) or a PlayerSprite (a ResolvableProfile and whether to draw the hat). When visited with a style it emits a single placeholder character, U+FFFC, with the style’s font set to the info’s FontDescription; the font system draws the sprite where a glyph would go. Visited without style — Component.getString, a log line — it says its fallback, or ObjectInfo.defaultFallback: [sprite] for an atlas sprite, [name head] for a player.

Style, and the click that never crosses

Style holds eleven nullable fields: Style.color (a TextColor), Style.shadowColor, the five booleans — Style.bold, Style.italic, Style.underlined, Style.strikethrough, Style.obfuscated — Style.clickEvent, Style.hoverEvent, Style.insertion and Style.font (a FontDescription). Null means inherit, which is what makes Style.applyTo a merge: a field the child sets wins, a field it leaves null falls through to the parent. A setter returns a new Style only when the value changes — set bold to what it already is and the same object comes back — and the codec and the setters collapse a style with nothing left set to the shared Style.EMPTY, so Style.isEmpty is an identity check. ChatFormatting is the legacy vocabulary — Style.applyFormat sets one boolean or, for a colour code, TextColor.fromLegacyFormat, and ChatFormatting.RESET returns Style.EMPTY outright.

A TextColor is 24 bits, masked on construction, and serialises as one of its sixteen names or as #RRGGBB; TextColor.parseColor accepts either. The shadow is different: Style.shadowColor is a 32-bit ARGB integer, and a null one inherits like every other field; Style.withoutShadow sets it to Style.NO_SHADOW, zero, which is how a component asks to be drawn flat. Style.font is a FontDescription, which may be a FontDescription.Resource naming a font file, or one of the two sprite shapes that ObjectContents uses — it need not name a font at all.

Eight clicks, and the one the server may not send

ClickEvent and HoverEvent are interfaces implemented by nested records — closed by convention and by their Action dispatch codec, not by the language. There are eight click actions and three hover actions:

ClickEvent.Actionrecordcarriesa server may send it
ClickEvent.Action.OPEN_URLClickEvent.OpenUrla URI, through ExtraCodecs.UNTRUSTED_URI✓
ClickEvent.Action.OPEN_FILEClickEvent.OpenFilea path on the viewer’s diskno
ClickEvent.Action.RUN_COMMANDClickEvent.RunCommanda command string✓
ClickEvent.Action.SUGGEST_COMMANDClickEvent.SuggestCommanda string for the chat box✓
ClickEvent.Action.SHOW_DIALOGClickEvent.ShowDialoga Holder of a Dialog✓
ClickEvent.Action.CHANGE_PAGEClickEvent.ChangePagea positive page number✓
ClickEvent.Action.COPY_TO_CLIPBOARDClickEvent.CopyToClipboarda string✓
ClickEvent.Action.CUSTOMClickEvent.Customan Identifier and an optional NBT payload, for servers to define✓

What keeps ClickEvent.Action.OPEN_FILE out of a server’s hands is ClickEvent.Action.filterForSerialization, applied as a validation on ClickEvent.Action.CODEC and therefore biting in both directions and in every format: a data pack cannot write one either, and the client cannot encode one it built itself. The private flag behind it is ClickEvent.Action.allowFromServer. The unfiltered ClickEvent.Action.UNSAFE_CODEC exists and nothing outside the enum reads it; an open-file click is something only client code constructs in memory — Screenshot’s notice, a debug dump’s path in KeyboardHandler, a profiler result in Minecraft — for its own chat. HoverEvent.Action has the identical machinery and nothing to filter — all three of its values are allowed: HoverEvent.ShowText wraps a component, HoverEvent.ShowItem an ItemStackTemplate, HoverEvent.ShowEntity an HoverEvent.EntityTooltipInfo of type, UUID and optional name.

Serialisation: one codec, three shapes

Serialisation is one recursive codec, ComponentSerialization.CODEC, whose shape is a three-way choice: a bare string becomes a literal, a list becomes its first element with the rest appended, and an object is the full record. Contents are matched by an explicit type field if one is present and otherwise by trying each contents codec in turn — which is why an untyped component still round-trips. Component.tryCollapseToString is what lets a plain unstyled literal encode as a bare string.

The full record is flat. Style.Serializer.MAP_CODEC is inlined, so the style’s keys — color, shadow_color, bold, italic, underlined, strikethrough, obfuscated, click_event, hover_event, insertion, font — sit beside the contents’ own keys, and the siblings are a non-empty list under extra. The seven contents codecs are registered in an ExtraCodecs.LateBoundIdMapper under the names in the table above, and ComponentSerialization.createLegacyComponentMatcher builds the matcher that the object and data-source kinds reuse for their own object and source fields. In JSON and NBT, encoding never writes a type: each kind is written with its own distinguishing key (text, translate, keybind, score, selector, nbt, and a sprite’s sprite or player), and a decoder recognises it by that key. A translatable argument that decodes to a plain unstyled literal is collapsed back to a string argument, so the argument list round-trips by meaning rather than by shape.

On the wire: NBT, and two budgets

On the wire in the configuration and play phases, components travel as NBT, not JSON: ComponentSerialization.STREAM_CODEC is built over the NBT ops — it is ByteBufCodecs.fromCodecWithRegistries, which encodes through NbtOps with the buffer’s RegistryOps and writes the resulting Tag, and on decode reads a Tag under an NbtAccounter and parses it back. The accounter is the difference between the two families: NbtAccounter.defaultQuota allows two mebibytes at depth 512, and the trusted variants — ComponentSerialization.TRUSTED_STREAM_CODEC and its siblings, built with ByteBufCodecs.fromCodecWithRegistriesTrusted over NbtAccounter.unlimitedHeap — lift the NBT size budget (the depth cap stays), and every clientbound chat packet uses them (packets and stream codecs). So do other texts a server authors: the death packet, entity custom names through EntityDataSerializers.OPTIONAL_COMPONENT, score displays, painting titles, command-suggestion tooltips. The budgeted codec is what the two component-typed data components use — DataComponents.CUSTOM_NAME and DataComponents.ITEM_NAME are synchronised with it — and those are the components a creative player’s stack carries serverbound. A third shape, ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC, needs no registries and carries texts sent where no registry is bound or needed: ClientboundDisconnectPacket, the MOTD in ClientboundServerDataPacket, a resource-pack prompt, server links. And ComponentSerialization.flatRestrictedCodec caps a component by the length of its JSON form; WrittenBookContent.CONTENT_CODEC uses it at 32,767 for a page.

Resolution: what a server does to a component before it sends it

Resolution — turning selectors, scores and NBT paths into text — is ComponentUtils.resolve against a ResolutionContext, which carries the command source, a depth limit and a ResolutionContext.LimitBehavior. The walk copies the tree: ComponentContents.resolve returns a copy by default and is overridden by the score, selector, nbt and object kinds, and by the translatable kind, which resolves each component argument; siblings are resolved after the contents, and a HoverEvent.ShowText inside a style is resolved too. Past the depth limit — 100 by default — ResolutionContext.LimitBehavior.STOP_PROCESSING_AND_COPY_REMAINING copies the rest untouched and ResolutionContext.LimitBehavior.DISCARD_REMAINING puts CommonComponents.ELLIPSIS in its place. A context with no command source resolves score, selector and nbt contents to empty. The context also carries an ObjectInfo validator: ObjectContents.resolve swaps a sprite ResolutionContext.validate rejects for its fallback, which is how ServerStatusPinger sanitises a server-list MOTD on the client — depth 16, discard past it, and no player heads.

Ordinary chat never resolves anything: the content of a ServerboundChatPacket is a plain string all the way to Component.literal. Commands are the exception — MessageArgument.Message.toComponent expands entity selectors inside a message argument, behind a permission, which is why /say @a names people and a chat line saying the same thing does not. The resolved text becomes the message’s unsigned content. The mechanics: MessageArgument.Message.parseText refuses more than 256 characters and, if the source may use selectors at all (EntitySelectorParser.allowSelectors), tries every @ as a selector and keeps the ones that parse as a MessageArgument.Part — an @ that fails on a missing or unknown selector type is left as ordinary text; at execution the permission is Permissions.COMMANDS_ENTITY_SELECTORS, and each part becomes EntitySelector.joinNames of what it finds. A raw component argument — /tellraw’s — is a ComponentArgument, parsed with the full codec from SNBT and resolved by ComponentArgument.getResolvedComponent with the target player as the scoreboard entity.

The death message

sequenceDiagram
    box transparent the server
    participant SP as ServerPlayer
    participant CT as CombatTracker
    end
    box transparent the client
    participant CPL as ClientPacketListener
    participant DScr as DeathScreen
    participant TrC as Translatable<br/>Contents
    participant Language as Language
    end

    Note over SP,CT: the server tick in which the player dies
    SP->>CT: getDeathMessage, if SHOW_DEATH_MESSAGES
    CT->>CT: the last CombatEntry's deathMessageType, then<br/>DamageSource.getLocalized<br/>DeathMessage
    CT-->>SP: a translatable death.attack.arrow, the two names as arguments
    Note over SP: the only wording on this side is Component.getString for the console log
    SP->>CPL: ClientboundPlayerCombatKillPacket, encoded to NBT on Netty
    Note over CPL: a MutableComponent nobody has read
    CPL->>DScr: new DeathScreen with the packet's message
    Note over DScr: the next frame, on the Render thread
    DScr->>TrC: visit reaches the contents
    TrC->>Language: getInstance, then getOrDefault of the key
    Language-->>TrC: the template from the client's language stack
    TrC->>Language: and again for the killer, if it is a mob
    TrC-->>DScr: the victim's name, " was shot by ", the killer's name

Four boundaries in one trace: the server tick that builds it, the Netty thread that encodes it, the Render thread that stores it, and the frame that finally words it. The component crosses the wire as a key and two arguments, and before that frame a Language is asked only for the server’s console log and for the chat line, which a second packet carries.

Built on the server, in no language

ServerPlayer.die runs on the server thread. If GameRules.SHOW_DEATH_MESSAGES is on it asks the CombatTracker for the message; CombatTracker.getDeathMessage takes the last CombatEntry, reads its DamageType.deathMessageType, and for the default kind hands to DamageSource.getLocalizedDeathMessage, which picks a key from the damage type’s DamageType.msgId — death.attack. plus the id, with .player when the source names no entity but LivingEntity.getKillCredit still finds one (the last player, or failing that the last mob, to hurt the victim), with .item when the killer’s held item has a DataComponents.CUSTOM_NAME — and builds Component.translatable with the victim’s and the killer’s display names as arguments. DeathMessageType.FALL_VARIANTS and DeathMessageType.INTENTIONAL_GAME_DESIGN take two more branches, the second of them attaching a ClickEvent.OpenUrl to a bracketed link — and the fall branch only when CombatTracker.getMostSignificantFall found one, so a fall-typed source with no recorded fall drops back to the ordinary message. Which entry counts as the significant fall, and the five-block threshold under it, belongs to damage and death. A fourth branch comes first: an empty combat log is death.attack.generic. The key is a line in en_us.json; the tracker never sees the line.

The killer’s name is itself a component, and often a translatable one. Entity.getDisplayName is PlayerTeam.formatNameForTeam over Entity.getName — the custom name if there is one, otherwise Entity.getTypeName, which is EntityType.getDescription, a Component.translatable of entity.minecraft.zombie — with a HoverEvent.ShowEntity and the UUID as insertion. Player.getDisplayName adds a ClickEvent.SuggestCommand of /tell name and the name as insertion. A team wraps the name in its prefix, suffix and colour. So the argument list carries components inside a component, styles inside styles, and a mob killer’s name is a second key the client will look up after the first.

The packet is sent twice over. ClientboundPlayerCombatKillPacket goes to the victim, with a PacketSendListener.exceptionallySend fallback for a send that fails — the case its keys have in mind is a message too large to encode: the replacement carries death.attack.even_more_magic and hangs the first 256 characters of the real message off it as a death.attack.message_too_long hover, so a failed send does not leave the death screen blank. The same component goes to everyone as system chat — PlayerList.broadcastSystemMessage, or the team-scoped PlayerList.broadcastSystemToTeam / PlayerList.broadcastSystemToAllExceptTeam when the team’s Team.Visibility says so — each as a ClientboundSystemChatPacket. With the game rule off, the kill packet still goes, carrying CommonComponents.EMPTY, and nothing is broadcast. Both packets use ComponentSerialization.TRUSTED_STREAM_CODEC, so what crosses is a compound with translate and with, the arguments compounds of their own.

On the ordinary path the server reads the message once, for its log: PlayerList.broadcastSystemMessage starts with MinecraftServer.sendSystemMessage, which logs Component.getString, and the walk visits the translatable contents and asks Language.getInstance. That is the ordinary path only. A victim on a team whose death-message visibility is not Team.Visibility.ALWAYS goes through PlayerList.broadcastSystemToTeam or PlayerList.broadcastSystemToAllExceptTeam instead, neither of which logs — and Team.Visibility.NEVER matches neither branch, so only the victim is told, on the death screen, and the server never reads it. On a dedicated server that is Language.DEFAULT_INSTANCE, loaded once from the en_us.json on the classpath, and nothing on the server ever calls Language.inject; a singleplayer server shares the JVM’s one Language with the client, so its log line is in the player’s language. A named mob’s death is logged the same way from LivingEntity.die. A dedicated server’s console is in English whatever the players speak.

Worded on the client, on the first frame

On the client, ClientPacketListener.handlePlayerCombatKill hops to the Render thread and constructs a DeathScreen with ClientboundPlayerCombatKillPacket.message, or respawns at once if LocalPlayer.shouldShowDeathScreen is off. Nothing has read the key. The next frame’s DeathScreen.visitText hands the component to the text collector, and the walk reaches TranslatableContents.decompose, which asks the current Language for the template. That Language is a ClientLanguage, built by LanguageManager.onResourceManagerReload from ClientLanguage.loadFrom over a stack of two codes — en_us first, then the selected language if its pack declares it — reading lang/code.json from every namespace of every enabled pack, merged in stack order. A key missing from the chosen language falls to English; a key missing from both is shown as itself, by Language.getOrDefault, unless the component carried a fallback from Component.translatableWithFallback. Language.loadFromJson rewrites a %d or %f specifier to %s as it loads, which is why translators’ number specifiers do not leave the decomposer showing the raw template. The template’s arguments are visited in their turn — the killer’s name, if it is entity.minecraft.zombie, goes back to the same Language — and the sentence exists, in this client’s language, for the first time. The chat line takes the same lookup through ChatListener.handleSystemMessage, worded when the packet is handled rather than on a frame.

What this page does not own

Signing and chat delivery — the PlayerChatMessage, the session, the Not Secure tag — is chat and signing. Everything after Component.visit — StringDecomposer, FormattedCharSequence, wrapping, reordering, glyphs — is text and fonts. What a Codec is, and how NBT and JSON differ under one, is codecs, NBT and JSON.

Questions players ask

Why did my friend’s death message say something different from mine? Because neither client received a sentence. Both received death.attack.arrow and two name components, and each client’s Language supplied its own template when it first worded the message.

Why does a message sometimes show as a raw key like death.attack.foo? The key is in neither the selected language nor en_us, and the component has no fallback, so Language.getOrDefault returned the key. A server or data pack that invents a key without shipping a resource pack for it shows the key on every client.

Why can a server show me a link but not open a file? ClickEvent.Action.OPEN_FILE fails ClickEvent.Action.filterForSerialization, so it cannot be encoded into any packet, data pack or book by either side. Only client code that constructs the ClickEvent.OpenFile in memory — the screenshot notice, a debug dump’s path — can present one.

Why does a dedicated server’s console show death messages in English? Its Language is Language.DEFAULT_INSTANCE, the bundled en_us.json; Language.inject is called only by the client’s LanguageManager, which is also why a singleplayer world’s log follows the player’s language.

Where to look

Component · MutableComponent · ComponentContents · PlainTextContents · TranslatableContents.decompose · KeybindContents · ScoreContents · SelectorContents · NbtContents · ObjectContents · Style.applyTo · TextColor · ClickEvent.Action.filterForSerialization · HoverEvent · ComponentSerialization.CODEC · ComponentSerialization.STREAM_CODEC · ByteBufCodecs.fromCodecWithRegistries · ComponentUtils.resolve · ResolutionContext · MessageArgument.Message.toComponent · CombatTracker.getDeathMessage · DamageSource.getLocalizedDeathMessage · ServerPlayer.die · ClientboundPlayerCombatKillPacket · ClientPacketListener.handlePlayerCombatKill · DeathScreen.visitText · Language.getOrDefault · ClientLanguage.loadFrom · LanguageManager.onResourceManagerReload


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The data-driven type pattern

Verified against Minecraft 26.3 · Part II · A data pack’s loot table says “type”: “minecraft:set_count”, and the game turns that string into an object the file never names.

A data-pack author writes a chest loot table, gives one entry a modifier whose type field says minecraft:set_count and whose count is a range, and drops the file into data/mypack/loot_table/chests/. Nothing in the pack says SetItemCountFunction. Nothing in the jar says mypack. When the server reloads, ReloadableServerRegistries.reload has RegistryDataLoader scan the directory and hand the file to LootTable.DIRECT_CODEC, and somewhere inside that codec the string minecraft:set_count is looked up in BuiltInRegistries.LOOT_FUNCTION_TYPE — a registry that BuiltInRegistries.bootStrap filled and froze before any world existed — and the MapCodec it finds there reads the rest of the object. The result is a SetItemCountFunction, and the first time a player opens that chest, SetItemCountFunction.run calls ItemStack.setCount on every stack that entry emits. That field is spelled type here and almost everywhere else the pattern appears, and the move behind it is the same one: fifty-six registries in BuiltInRegistries are read this way. It is why type is the most important key in a data pack — every type the pattern reads is a lookup in a registry data packs cannot add to, so a pack can compose the game’s behaviours endlessly and never add a new one.

The cast

classwhat it decidesthread
MapCodec (DataFixerUpper)how to read one kind’s fields out of a JSON object that also carries a type key; the registry element in the bare spelling—
RegistryRegistry.byNameCodec: a string to an element, with the error Unknown registry key and the element’s registration lifecycle attached—
BuiltInRegistriesthe registries of kinds — filled during Bootstrap.bootStrap, frozen, identical on client and servermain thread, at Bootstrap
ReloadableServerRegistriesthe eight registries of RegistryDataLoader.RELOADABLE_REGISTRIES, loot tables among them, rebuilt on every reload, and the six with a LootDataType validatedthe background executor
RegistryOpsthe ops that let a codec resolve a Holder to another data-pack element while it decodeswherever the codec runs
LootItemFunctionsLootItemFunctions.TYPED_CODEC, the dispatch codec for one instance of the pattern; LootItemFunctions.CODEC, which takes an item modifier’s id, one function or a list of them—
SetItemCountFunctionthe kind traced below: a condition, a ContextIntProvider, an add flagServer, when the loot rolls
Holderhow everything else refers to the loaded element — by key, bound later—

The idea, stated once

A codec built by Codec.dispatch reads one field of a JSON object — type unless the caller names another — decodes it with a codec for the kind, and asks that kind for a MapCodec to read the remaining fields. The class that does it is DataFixerUpper’s KeyDispatchCodec — a Codec and a DynamicOps being the two halves of every read in this part (codecs, NBT and JSON) — which is why the MapCodec a kind supplies is a map codec: it reads a set of fields from the same object the type key came from, so the file looks flat. When the kind codec is Registry.byNameCodec over a registry in BuiltInRegistries, the set of kinds is whatever the jar registered at Bootstrap, and a pack can reach every one of them by name and none it did not ship. The element that comes out is then either registered — in a RegistryDataLoader registry, read with the world like Registries.STRUCTURE or on every reload like Registries.LOOT_TABLE — or inline, a value inside a larger element that has no id of its own, such as the PlacementModifier list in a PlacedFeature. A registered element is referred to everywhere else by Holder: a RegistryFileCodec reads either an id or the inline object, and the identifiers page explains how the reference is handed out before the entry exists (identifiers and registries).

flowchart TD
    F["a data-pack file with a type key"] --> D["Codec.dispatch over Registry.byNameCodec"]
    D --> K["a built-in registry of kinds, frozen at Bootstrap"]
    K --> M["that kind's MapCodec reads the remaining fields"]
    M --> O["an object of a class the file never named"]
    O --> R["registered by RegistryDataLoader, or inline in another element"]
    R --> H["referred to by Holder from other files and from the wire"]

The pattern in one line, and the whole of it turns on the third box: the registry of kinds is built in and frozen, so a pack may write any number of elements and no number of kinds. Only the first box is data; every box after it is the jar’s code.

The pattern has two spellings, and they differ only in what the registry holds. In the bare spelling the element is the MapCodec: BuiltInRegistries.LOOT_FUNCTION_TYPE is a Registry of MapCodec, LootItemFunctions.bootstrap registers SetItemCountFunction.MAP_CODEC under set_count, and LootItemFunction.codec is how a live object names its own kind for encoding. In the type-object spelling the element is a small interface or record that wraps the codec: BlockPredicateType is an interface with one method, BlockPredicateType.codec, its constants such as BlockPredicateType.MATCHING_BLOCKS are registered into BuiltInRegistries.BLOCK_PREDICATE_TYPE, and BlockPredicate.CODEC dispatches on BlockPredicate.type. A type object lets a kind carry something beside its codec — RecipeSerializer, ConsumeEffect.Type and RecipeDisplay.Type are records of a MapCodec and a StreamCodec, one for the file and one for the wire — though most carry the codec alone. Feature and WorldCarver take the bare spelling: BuiltInRegistries.FEATURE_TYPE and BuiltInRegistries.CARVER_TYPE hold each kind’s MapCodec, and the element the file describes carries its own parameters and does the work itself, in Feature.place or WorldCarver.carve.

Eleven of the instances accept a bare value in place of the object, each through a Codec.either in front of the dispatch, or a Codec.xor in one case: IntProviders.CODEC, FloatProviders.CODEC, ContextIntProviders.DIRECT_CODEC, ContextFloatProviders.DIRECT_CODEC and LevelBasedValue.CODEC read a plain number as a constant, DensityFunctions.DIRECT_CODEC reads one as a ConstantFunction, a height provider reads a bare anchor, BlockStateProvider.DIRECT_CODEC a bare block state, NbtProviders.CODEC and ScoreboardNameProviders.CODEC a bare string as the context form, and Permission.CODEC a bare identifier as a Permission.Atom. Two accept a bare list: LootItemFunctions.DIRECT_CODEC tries LootItemFunctions.TYPED_CODEC and falls back to SequenceFunction.INLINE_CODEC, so a JSON array where one function was expected is a sequence of them, and SlotSources.DIRECT_CODEC does the same through GroupSlotSource.INLINE_CODEC.

Fifty-six of them

Fifty-six registries in BuiltInRegistries hold a kind’s codec, or a type that carries one, for a codec to dispatch to from the value of a field: thirty-six bare and twenty type-object. The dispatch key is type unless the row says otherwise. The criterion is the codec in the registry, not Registry.byNameCodec itself: more registries dispatch through it and are not here. BuiltInRegistries.GAME_RULE spells the registry name as the key of a map rather than the value of a field, and so does BuiltInRegistries.STAT_TYPE in a player’s statistics file, though a player predicate dispatches on it by type. BuiltInRegistries.ENVIRONMENT_ATTRIBUTE and BuiltInRegistries.DATA_COMPONENT_TYPE are keys nearly everywhere a data pack meets them, and each also backs a field dispatch or two — EnvironmentAttributeCheck.MAP_CODEC on attribute, and two client item-model properties. All four are among the exceptions below.

The bare spelling: the registry holds a MapCodec

registryelementkeywhere the elements livetaught in
BuiltInRegistries.LOOT_POOL_ENTRY_TYPELootPoolEntryContainerinline in loot tablesloot tables
BuiltInRegistries.LOOT_FUNCTION_TYPELootItemFunctionRegistries.ITEM_MODIFIER (reloadable), and inline in tables, pools and entriesloot tables
BuiltInRegistries.LOOT_CONDITION_TYPELootItemConditionRegistries.PREDICATE (reloadable), and inlinecontexts and predicates
BuiltInRegistries.CONTEXT_INT_PROVIDER_TYPEContextIntProviderRegistries.CONTEXT_INT_PROVIDER (reloadable), and inline; a bare integer is a constantcontexts and predicates
BuiltInRegistries.CONTEXT_FLOAT_PROVIDER_TYPEContextFloatProviderRegistries.CONTEXT_FLOAT_PROVIDER (reloadable), and inline; a bare number is a constantcontexts and predicates
BuiltInRegistries.LOOT_NBT_PROVIDER_TYPENbtProviderinline in loot functionscontexts and predicates
BuiltInRegistries.LOOT_SCORE_PROVIDER_TYPEScoreboardNameProviderinline in the score int provider, ScoreboardValuecontexts and predicates
BuiltInRegistries.SLOT_SOURCE_TYPESlotSourceRegistries.SLOT_SOURCE (reloadable), and inline in SlotLoot entriescontexts and predicates
BuiltInRegistries.INT_PROVIDER_TYPEIntProviderinline in features and elsewhere; a bare integer is a constantfeatures and placement
BuiltInRegistries.FLOAT_PROVIDER_TYPEFloatProviderinline; a bare float is a constantfeatures and placement
BuiltInRegistries.DENSITY_FUNCTION_TYPEDensityFunctionRegistries.DENSITY_FUNCTION, and inline; a bare number is a constantdensity functions
BuiltInRegistries.MATERIAL_CONDITION_TYPEMaterialConditionRegistries.MATERIAL_CONDITION, and inline in material rulesterrain
BuiltInRegistries.MATERIAL_RULE_TYPEMaterialRuleRegistries.MATERIAL_RULE, and inlineterrain
BuiltInRegistries.BIOME_SOURCEBiomeSourceinline in Registries.LEVEL_STEMbiomes
BuiltInRegistries.CHUNK_GENERATORChunkGeneratorinline in Registries.LEVEL_STEMterrain
BuiltInRegistries.FEATURE_TYPEFeatureRegistries.FEATURE, and inlinefeatures and placement
BuiltInRegistries.PLACEMENT_MODIFIER_TYPEPlacementModifierinline in Registries.PLACED_FEATUREfeatures and placement
BuiltInRegistries.BLOCK_STATE_PROVIDER_TYPEBlockStateProviderRegistries.BLOCK_STATE_PROVIDER, and inline in features; a bare block state is a SimpleStateProviderfeatures and placement
BuiltInRegistries.CARVER_TYPEWorldCarverRegistries.CARVERterrain
BuiltInRegistries.STRUCTURE_PLACEMENTStructurePlacementinline in Registries.STRUCTURE_SETstructure placement
BuiltInRegistries.STRUCTURE_PROCESSORStructureProcessorprocessor_typeRegistries.PROCESSOR_LISTjigsaw and templates
BuiltInRegistries.POOL_ALIAS_BINDING_TYPEPoolAliasBindinginline in jigsaw structuresjigsaw and templates
BuiltInRegistries.ENCHANTMENT_LEVEL_BASED_VALUE_TYPELevelBasedValueinline in Registries.ENCHANTMENTenchantments
BuiltInRegistries.ENCHANTMENT_ENTITY_EFFECT_TYPEEnchantmentEntityEffectinline in Registries.ENCHANTMENTenchantments
BuiltInRegistries.ENCHANTMENT_LOCATION_BASED_EFFECT_TYPEEnchantmentLocationBasedEffectinline in Registries.ENCHANTMENTenchantments
BuiltInRegistries.ENCHANTMENT_VALUE_EFFECT_TYPEEnchantmentValueEffectinline in Registries.ENCHANTMENTenchantments
BuiltInRegistries.ENCHANTMENT_PROVIDER_TYPEEnchantmentProviderRegistries.ENCHANTMENT_PROVIDERenchanting
BuiltInRegistries.SPAWN_CONDITION_TYPESpawnConditioninline in entity variants, through SpawnPrioritySelectors.CODECentity lifecycle
BuiltInRegistries.TEST_ENVIRONMENT_DEFINITION_TYPETestEnvironmentDefinitionRegistries.TEST_ENVIRONMENTgame tests
BuiltInRegistries.TEST_INSTANCE_TYPEGameTestInstanceRegistries.TEST_INSTANCEgame tests
BuiltInRegistries.DIALOG_TYPEDialogRegistries.DIALOGdialogs
BuiltInRegistries.DIALOG_ACTION_TYPEActioninline in dialogsdialogs
BuiltInRegistries.DIALOG_BODY_TYPEDialogBodyinline in dialogsdialogs
BuiltInRegistries.INPUT_CONTROL_TYPEInputControlinline in dialogs, as a MapCodec (Codec.dispatchMap)dialogs
BuiltInRegistries.PERMISSION_TYPEPermissioninline in a permission checkpermissions
BuiltInRegistries.PERMISSION_CHECK_TYPEPermissionCheckonly written, by ArgumentUtils into the command-tree reportpermissions

The type-object spelling: the registry holds a type that carries a MapCodec

registrytype objectelementkeywhere the elements livetaught in
BuiltInRegistries.HEIGHT_PROVIDER_TYPEHeightProviderTypeHeightProviderinline in placements; a bare anchor is a constantfeatures and placement
BuiltInRegistries.BLOCK_PREDICATE_TYPEBlockPredicateTypeBlockPredicateinline in features and placementsfeatures and placement
BuiltInRegistries.TRUNK_PLACER_TYPETrunkPlacerTypeTrunkPlacerinline in tree featurestrees
BuiltInRegistries.FOLIAGE_PLACER_TYPEFoliagePlacerTypeFoliagePlacerinline in tree featurestrees
BuiltInRegistries.ROOT_PLACER_TYPERootPlacerTypeRootPlacerinline in tree featurestrees
BuiltInRegistries.TREE_DECORATOR_TYPETreeDecoratorTypeTreeDecoratorinline in tree featurestrees
BuiltInRegistries.FEATURE_SIZE_TYPEFeatureSizeTypeFeatureSizeinline in tree featurestrees
BuiltInRegistries.STRUCTURE_TYPEStructureTypeStructureRegistries.STRUCTUREstructure placement
BuiltInRegistries.STRUCTURE_POOL_ELEMENTStructurePoolElementTypeStructurePoolElementelement_typeinline in Registries.TEMPLATE_POOLjigsaw and templates
BuiltInRegistries.RULE_TESTRuleTestTypeRuleTestpredicate_typeinline in processor listsjigsaw and templates
BuiltInRegistries.POS_RULE_TESTPosRuleTestTypePosRuleTestpredicate_typeinline in processor listsjigsaw and templates
BuiltInRegistries.RULE_BLOCK_ENTITY_MODIFIERRuleBlockEntityModifierTypeRuleBlockEntityModifierinline in processor rulesjigsaw and templates
BuiltInRegistries.RECIPE_SERIALIZERRecipeSerializerRecipeRegistries.RECIPE (reloadable), which RecipeManager indexes into a RecipeMaprecipes
BuiltInRegistries.RECIPE_DISPLAYRecipeDisplay.TypeRecipeDisplayinline, mostly on the wire to the recipe bookrecipes
BuiltInRegistries.SLOT_DISPLAYSlotDisplay.TypeSlotDisplayinline in recipe displaysrecipes
BuiltInRegistries.CONSUME_EFFECT_TYPEConsumeEffect.TypeConsumeEffectinline in the Consumable and DeathProtection componentsusing an item
BuiltInRegistries.TRIGGER_TYPESCriterionTriggerCriteriontrigger, with the fields under conditions (ExtraCodecs.dispatchOptionalValue)inline in Registries.ADVANCEMENT (reloadable)advancements
BuiltInRegistries.PARTICLE_TYPEParticleTypeParticleOptionsinline in biome ambient particles (AmbientParticle) and area effect cloudsparticles
BuiltInRegistries.NUMBER_FORMAT_TYPENumberFormatTypeNumberFormatinline in Objective and Score — save data and commands, no packscoreboard and data
BuiltInRegistries.POSITION_SOURCE_TYPEPositionSourceTypePositionSourceinline in VibrationParticleOption, the vibration particle’s options, which a pack can write wherever it writes a particlegame events and vibrations

The first seven rows are the sub-objects of a feature or a placed feature, and they split between two pages: the first two are the placement layer, on features and placement, and the five tree slots are trees, which is one feature’s inside seen whole.

Every registered destination is a RegistryDataLoader registry. Most are read once, when the world loads; the eight of RegistryDataLoader.RELOADABLE_REGISTRIES — Registries.LOOT_TABLE, Registries.ITEM_MODIFIER, Registries.PREDICATE, Registries.RECIPE, Registries.ADVANCEMENT and three more — are read again on every reload by ReloadableServerRegistries, and DatapackStructureReport calls them stable dynamic registries. The elements that reach the client are the ones in RegistryDataLoader.SYNCHRONIZED_REGISTRIES, re-encoded with the direct codec or, for thirteen of them, a smaller network codec, which is why BuiltInRegistries must be identical on both sides: the client runs the same dispatch on the same kinds (protocol phases).

One instance traced: set_count

sequenceDiagram
    participant RSReg as Reloadable<br/>ServerRegistries
    participant LT as LootTable
    participant LIF as LootItemFunctions
    participant BIR as BuiltInRegistries
    participant SICF as SetItemCountFunction
    participant CBE as ChestBlockEntity

    Note over RSReg: a reload, on the background executor
    RSReg->>RSReg: reload: RegistryDataLoader.load over the reloadable registries
    RSReg->>LT: DIRECT_CODEC parses one loot_table file
    LT->>LIF: an entry's modifier: CODEC, DIRECT_CODEC, then TYPED_CODEC reads the type key
    LIF->>BIR: LOOT_FUNCTION_TYPE.<br/>byNameCodec looks up minecraft:set_count
    BIR-->>LIF: SetItemCountFunction.<br/>MAP_CODEC, out of a frozen registry
    LIF->>SICF: MAP_CODEC reads condition, count and add
    SICF-->>LT: the function object, held as the entry's modifier
    LT-->>RSReg: into a fresh MappedRegistry, the RELOADABLE layer replaced
    Note over CBE: a later tick, on the server thread, a player opens the chest
    CBE->>RSReg: the key, from RandomizableContainer.unpackLootTable
    RSReg-->>CBE: the LootTable, or LootTable.EMPTY for an unknown key
    CBE->>LT: fill, then getRandomItems with a CHEST context
    LT->>SICF: the entry's decorate wraps its output, every stack passes through apply
    SICF->>SICF: the condition passes, run calls ItemStack.setCount
    LT-->>CBE: the stacks, shuffled into the chest's slots

One loot function’s whole life, in two stretches: parsed once at reload, where the registered type supplies the codec and the file only the config, and run much later, when a chest is filled and every stack its entry emits passes through SetItemCountFunction.

The reload half. MinecraftServer.reloadResources — and WorldLoader.load on first start — calls ReloadableServerResources.loadResources, whose first act is ReloadableServerRegistries.reload on the background executor. It builds lookups that already carry the updated tags, so a loot condition can name an item tag while it decodes (tags), and hands them to RegistryDataLoader.load with RegistryDataLoader.RELOADABLE_REGISTRIES — loot tables, predicates, item modifiers and five more, read the way the world’s registries are rather than by a listener of the every-JSON-file-in-a-directory shape (the resource system). For each, RegistryDataLoader creates a new MappedRegistry, lists the directory with FileToIdConverter.registry over Registries.elementsDirPath — the directory is the registry’s path, loot_table — and parses every file with the registry’s codec through a RegistryOps over JsonOps.INSTANCE. It collects every error by key and fails the whole load, so one bad table costs the whole reload: a loot table naming minecraft:set_cuont makes a /reload fail and keep the data it had, and stops the world from opening on first start, as a typo in a biome does.

Inside LootTable.DIRECT_CODEC the entry’s modifier is read by LootItemFunctions.CODEC, which takes an item modifier’s id or the function itself; the value in question is an object, so LootItemFunctions.DIRECT_CODEC takes its first branch, LootItemFunctions.TYPED_CODEC: Registry.byNameCodec on BuiltInRegistries.LOOT_FUNCTION_TYPE reads minecraft:set_count, finds SetItemCountFunction.MAP_CODEC, and that codec reads condition (the LootItemConditionalFunction.commonFields every conditional function shares, itself dispatched on BuiltInRegistries.LOOT_CONDITION_TYPE), count through ContextIntProviders.CODEC — a third dispatch, on BuiltInRegistries.CONTEXT_INT_PROVIDER_TYPE — and the optional add. At least three built-in registries are consulted to build this one function, and the file names none of them. A misspelt kind fails at the first of them with an Unknown registry key error that names the registry of kinds, and the whole load fails with it. An entry, a pool and a table each hold at most one modifier, and several functions in one place are one SequenceFunction, which is what a JSON array in that field reads as. That same LootItemFunctions.CODEC decodes the modifier field of a pool and of the table as well as of an entry, which is why one kind name is accepted in all three places — the three modifiers wrap the output consumer one inside the other, and a modifier on the table runs last.

After all eight registries are built and frozen, ReloadableServerRegistries.createAndValidateFullContext replaces the RegistryLayer.RELOADABLE layer through LayeredRegistryAccess.replaceFrom and ReloadableServerRegistries.validateLootRegistries runs LootDataType.runValidation over every element of the six registries that have a LootDataType: SetItemCountFunction.validate walks into its count provider, entering any Registries.CONTEXT_INT_PROVIDER entry it names by id. Validation warns — problems are logged, the element stays registered — except for a reference cycle, which is fatal and fails the reload. An element from one of the game’s own packs registers as Lifecycle.stable, one from any other pack as Lifecycle.experimental.

The run half. The pack described a function with a string, and the string is now an object hanging off a pool’s entry. What runs it is the ordinary machinery of a draw — RandomizableContainer.unpackLootTable asking MinecraftServer.reloadableRegistries for the table, the pools rolling, the tiers of modifiers wrapping the output consumer one inside the other, and SetItemCountFunction.run finally calling ItemStack.setCount with a number a ContextIntProvider produced (loot tables). None of that is the pattern; the pattern ended the moment the object existed.

What does not follow the pattern

Not every registry in BuiltInRegistries whose name ends in type is a registry of kinds. Two groups account for most of the ones that are not. A few fall outside them altogether — BuiltInRegistries.TICKET_TYPE, BuiltInRegistries.MAP_DECORATION_TYPE, BuiltInRegistries.POINT_OF_INTEREST_TYPE and BuiltInRegistries.VILLAGER_TYPE are registries of ordinary things whose names happen to end in type, dispatching nothing, and BuiltInRegistries.ATTRIBUTE_TYPE has a Registry.byNameCodec, AttributeTypes.CODEC, that nothing in the tree reads; the attribute name a file actually uses as a key belongs to BuiltInRegistries.ENVIRONMENT_ATTRIBUTE.

A key, not a kind. BuiltInRegistries.DATA_COMPONENT_TYPE, BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE, BuiltInRegistries.GAME_RULE and BuiltInRegistries.ENVIRONMENT_ATTRIBUTE each hold objects that carry a codec for their value, and a file uses them as JSON keys: GameRuleMap.CODEC and DataComponentPredicate.CODEC are Codec.dispatchedMap, a map whose key codec is Registry.byNameCodec and whose value codec depends on the key. There is no type field because the name of the field is the type. BuiltInRegistries.ENTITY_SUB_PREDICATE_TYPE is the same shape and holds a plain Codec rather than a MapCodec, and EntityPredicate reads it as a dispatched map too. BuiltInRegistries.STAT_TYPE is a key in a player’s statistics file and the type a PlayerPredicate.StatMatcher dispatches on, and either way its value codec is derived from StatType.getRegistry rather than stored. BuiltInRegistries.MEMORY_MODULE_TYPE is a key in a brain’s saved memories, MemoryMap.CODEC, the same way.

A type object with no codec. BuiltInRegistries.RECIPE_TYPE is the one a modder reaches for and the wrong one: RecipeType.CRAFTING groups recipes for lookup, and the kind a recipe file names — the field Recipe.DIRECT_CODEC dispatches on through Recipe.getSerializer — is a RecipeSerializer, in BuiltInRegistries.RECIPE_SERIALIZER. BuiltInRegistries.STRUCTURE_PIECE holds StructurePieceType, a loader from NBT with a StructurePieceSerializationContext, for the pieces of a started structure saved in the chunk — save data, not a pack. BuiltInRegistries.ENTITY_TYPE, BuiltInRegistries.BLOCK_ENTITY_TYPE, BuiltInRegistries.MENU, BuiltInRegistries.SENSOR_TYPE and BuiltInRegistries.COMMAND_ARGUMENT_TYPE are registries of type objects that no codec dispatches on: they are looked up by name and construct or describe things in Java.

For a 1.21-era reader. The loot registries of kinds hold the MapCodec itself — there is no LootItemFunctionType — and a functions list is one modifier, keyed type; NumberProvider is ContextIntProvider and ContextFloatProvider. Int and float providers, structure processors, features, carvers, placement modifiers, state providers and structure placements went the same way, and ConfiguredFeature is gone: a Feature carries its own configuration. Surface rules are MaterialRules. The rest kept their type objects, and both spellings dispatch identically.

Where to look

Codec.dispatch · KeyDispatchCodec · Registry.byNameCodec · BuiltInRegistries · LootItemFunctions.TYPED_CODEC · LootItemFunctions.bootstrap · SetItemCountFunction.MAP_CODEC · BlockPredicate.CODEC · BlockPredicateType · Feature.DIRECT_CODEC · FeatureTypes.bootstrap · LootDataType · ReloadableServerRegistries.reload · RegistryDataLoader.RELOADABLE_REGISTRIES · RegistryFileCodec · RegistryDataLoader.WORLD_REGISTRIES · LootTable.fill · LootItemFunction.decorate · SetItemCountFunction.run


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

III · The server

Verified against Minecraft 26.3 · Part III · The program that owns the world: one thread, one loop, twenty times a second, from the command line that starts it to the three different ways it stops.

Everything a player thinks of as the world — the blocks, the mobs, the weather, the hunger bar — is state owned by one object on one thread, and this part is that thread doing a full lap. Part I named the Server thread (anatomy); this is the first place you watch it work. A player recognises this part by its clock: nothing in the world happens continuously, and a furnace advances in steps of a twentieth of a second. The claim the part makes is that almost everything surprising about a server’s timing is the order of one method — which step of MinecraftServer.tickChildren runs before which — and a reader who finishes it can answer when for anything the server does: when a block change reaches a screen, when a mob stops being ticked, when a chest is on disk, and what is lost if the process is killed between two of those moments.

That is a small system to know so much about. Counting the four packages the atlas lists for this part — net/minecraft/server itself, and server/level, server/players and server/dedicated under it — the way it counts everything else, it is 95 files and 21,900 lines. Over half of those lines are server/level’s forty-two files at nearly three hundred lines apiece, because the objects that own a world are few and enormous. The other landmark is one file: MinecraftServer, the largest of the ninety-five and most of this part’s first page and its last two. Part I counts it too, because it is where the Server thread is made — the atlas lets a class belong to two parts, and this is the part that watches it work.

The shape of the part

Part III is a line into a loop and out again. Two pages are the loop itself — the server tick and the one step of it that is a whole lecture — and the other three are the beginning, the population and the end.

flowchart TD
    Tick["1 · The server tick"]
    Level["2 · The level tick"]
    Players["3 · Players and sessions"]
    Start["4 · Starting a server"]
    Death["5 · How a server dies"]
    Start -- "the thread, the levels" --> Tick
    Tick -- "MinecraftServer.tickChildren" --> Level
    Level -- "the packets it wrote" --> Tick
    Tick -- "the packet drain, the connection phase" --> Players
    Players -- "a join, a disconnect" --> Tick
    Tick -- "the loop's finally" --> Death

The five lectures of Part III, numbered to the watch order and wired in the order the program runs them; an arrow is a hand-off at run time, not a dependency between lectures.

The two numbered first are the loop itself. Seven later parts (IV, V, VI, VII, VIII, IX and XIII: every later part on the server’s side of the game except world generation) assume one of those two or the other, which makes them the most load-bearing pair in the book after Anatomy. The beginning and the end are numbered last because there has to be something for them to start and stop.

Before you start

Anatomy, and specifically its two loops figure. This part assumes you know that the Server thread is an event loop as well as a game loop, that a packet is decoded on a Netty thread and handled on this one, and that the client’s frame loop is a separate clock. Part II’s codecs and registries are assumed wherever something is written to disk or sent on the wire, and the resource system is assumed once, by starting a server, which runs its staged load for server data.

Two pages from a later part are assumed, and only one of them has to be read first. Tickets and loading owns what entity-ticking and block-ticking range mean, but the level tick defines both before it uses them, so it keeps until Part IV. Environment attributes and timelines does not keep: the level tick’s first act is to throw that system’s cache away, and that step does not mean much to a reader who has never met it. Watch that page out of order; everything else in Part IV can wait.

Watch in this order

  1. The server tick — one 50 ms lap: the deadline that moves before the work starts, every packet since last time handled at once, every dimension advanced, and the two writes per client that carry the tick’s own packets. “Can’t keep up!” is not a warning that the server is about to skip ticks — it is the skip.
  2. The level tick — one step of that lap, which is the whole world changing: weather, scheduled ticks, mob spawns, random ticks, every entity, every block entity. The block changes are broadcast before the entities move, so a change a piston makes reaches you a tick later than one a player’s command makes — with falling sand the exception that sends its own packets, and a console command as late as the piston.
  3. Players and sessions — a join from the end of the login handshake to a player standing in a world with chunks on the way, and then the four ways that session changes: death, a dimension, a disconnect, and a debug command that sends the player back through the join handshake without dropping the socket. Dying replaces your player object; the Nether does not.
  4. Starting a server — java -jar server.jar to the word Done: the EULA, the lock on session.lock, the packs and registries, the thread, the levels. The step that loads the world’s chunks loads none of them on an ordinary world.
  5. How a server dies — three endings compared: /stop, a crash in the tick loop, and the watchdog thread that kills a server whose tick runs past its limit. A crash in the tick saves your world. The watchdog does not.

Where the part stops

Almost nothing: 2% of the part’s lines are named on no page in the book, which after Parts I and VIII is as close to complete as the book gets. What the five lectures leave out they mostly leave to another part, because a package is not a subject; the rest is a handful of small classes no lecture names, ServerEntityGetter the largest. Chunk generation — half of ChunkMap and everything around it — is Part IV’s, where a chunk rather than a tick is the thing being followed (chunk generation). The server half of a click is Parts V, VII and VIII’s. The machinery beside the loop that commands and data packs drive — the scoreboard, the function library, the advancement manager, all living in net/minecraft/server — is Part XIII’s, and the reload beside it is Part II’s. What is left, and it is the part’s whole subject, is the loop, the levels it drives, the players in them, and the two ends of the process.

Reference this part uses

Threads — the Server thread, the worker pool and the dedicated server’s five side threads, with who makes each. Game rules — the fourteen these five pages name, out of fifty-nine. Level data and rules — which file remembers what, and how level.dat is written. Packets — everything the tick sends. Diagram lanes — what each lane in the part’s figures stands for.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The server tick

Verified against Minecraft 26.3 · Part III · One 50 ms tick on the Server thread, from the moment the clock says now to the moment the thread parks again.

Twenty times a second the Server thread wakes, hands every packet that arrived since it last looked to the code that answers it, advances every dimension by one step, pushes what each player needs to know onto the wire, and then spends whatever is left of its 50 ms on work other threads posted back before parking until the next beat. Everything a player calls the world — blocks, mobs, weather, the contents of a chest — is computed in plenty of places and committed in this one, on this one thread. When a lap runs long the console eventually prints Can’t keep up! Is the server overloaded?, and that line is not a warning that the server is about to start missing ticks. MinecraftServer.runServer logs it and advances the deadline past the whole backlog in the same branch, so the message is the skip; and because that branch will not fire again for a further ten seconds and a hundred ticks of the server’s own scheduled time, a server that complained recently keeps running behind rather than skipping at all. And the ticks it does skip are simply gone — nothing runs them later, so game time ends up that many ticks younger than the wall clock and stays there.

The cast

classwhat it decidesthread
MinecraftServerthe loop, the thread and the event loop in one object: the deadline, the budget, and the order everything happens inServer
ServerTickRateManagerhow long this tick is, whether game elements advance at all, and whether the loop is sprintingServer
TickTaskone queued runnable plus the MinecraftServer.tickCount it was submitted on — the age the budget readsany thread submits, Server runs
PacketProcessorwhich serverbound packets are waiting, and that all of them are handled at the top of a tickNetty fills it, Server empties it
ServerLevelone dimension’s step — a lecture of its ownServer
ServerConnectionListenerwhich connections are still alive, and when each one’s player takes its own tickServer
ServerCommonPacketListenerImplwhether a packet handed to a player is written, or written and flushedServer, mostly
ServerChunkCache.MainThreadExecutorwhat the leftover milliseconds buy, one queue per levelServer

One lap, ending on the wire

sequenceDiagram
    participant PP as PacketProcessor
    participant MS as MinecraftServer
    participant SL as ServerLevel
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Conn as Connection
    participant Wire as the network

    Note over MS,SGPL: MinecraftServer.runServer moves MinecraftServer.nextTickTimeNanos past the backlog if it warns
    MS->>PP: processQueuedPackets
    PP->>SGPL: each packet Netty queued since the last drain, handled on its listener
    rect rgba(0, 0, 0, 0.04)
        Note over PP,Wire: MinecraftServer.tickChildren, one pass
        MS->>SGPL: suspendFlushing, on every player's listener
        MS->>SL: tick(haveTime), each dimension in turn, overworld first
        SL->>SGPL: send, per player
        SGPL->>Conn: send, with no flush while the flag holds
        MS->>Conn: Connection.tick on each, from ServerConnectionListener.tick
        Conn->>Conn: flushQueue drains pendingActions
        Conn->>SGPL: tick, the player's own tick
        Conn->>Wire: flush one, the levels and the player tick
        MS->>SGPL: the next chunk batch, through its PlayerChunkSender
        MS->>SGPL: resumeFlushing, per player, right after that batch
        SGPL->>Conn: flushChannel
        Conn->>Wire: flush two, the chunks and the rest of the phase
    end
    Note over MS,SGPL: MinecraftServer.waitUntilNextTick runs all tasks, then BlockableEventLoop.managedBlock parks

One tick, and the two moments the tick’s own packets reach the socket; the shaded band is MinecraftServer.tickChildren, the stretch in which a playing client’s packets wait to be flushed.

Follow the two arrows into the network: they carry what the tick itself sends a playing client, and the second one is the one the chunks ride.

The deadline moves before the work starts

MinecraftServer.runServer re-reads this tick’s length every iteration from TickRateManager.nanosecondsPerTick and adds it to MinecraftServer.nextTickTimeNanos before calling anything. Being behind means the wall clock has already passed that field. The overload branch fires when it has passed by more than MinecraftServer.OVERLOADED_THRESHOLD_NANOS (one second) plus MinecraftServer.OVERLOADED_TICKS_THRESHOLD ticks’ worth — two seconds in all at the default rate — and the last warning is at least MinecraftServer.OVERLOADED_WARNING_INTERVAL_NANOS (ten seconds) plus MinecraftServer.OVERLOADED_TICKS_WARNING_INTERVAL ticks’ worth behind it, fifteen seconds in all at the default rate. Both effects — the log line, and MinecraftServer.nextTickTimeNanos jumping over the missed ticks — are statements of that one branch. Lateness therefore has to accumulate before either happens: a server running every lap ten percent long says nothing while the shortfall piles up, then warns and drops the pile in one step. And because MinecraftServer.lastOverloadWarningNanos is set to the new deadline, the second gate is fifteen seconds of the server’s own scheduled time — which on an overloaded server is rather more than fifteen seconds of yours. In between, the backlog is real and every tick of it is run.

Sprinting takes the other arm of the loop’s opening if, the one the overload check sits inside: when the server is not paused, ServerTickRateManager.isSprinting is true and ServerTickRateManager.checkShouldSprintThisTick consents, this tick is declared zero nanoseconds long and MinecraftServer.nextTickTimeNanos is set to now. TickRateManager.nanosecondsPerTick still reads 50 ms throughout — a sprint changes the length of this tick, gives the overload check nothing to measure, and sends the loop straight back for another. Sprinting also unfreezes the game — ServerTickRateManager.requestGameToSprint remembers the old state in ServerTickRateManager.previousIsFrozen — and restores it when ServerTickRateManager.finishTickSprint reports the measured rate.

Every packet since last time, in one drain

MinecraftServer.processPacketsAndTick opens the frame for the Tracy profiler, then calls PacketProcessor.processQueuedPackets — before MinecraftServer.tickServer, so the frame a profiler shows includes the packets. Each entry in that ConcurrentLinkedQueue is a PacketProcessor.ListenerAndPacket: a Netty thread decoded the packet, the handler called PacketUtils.ensureRunningOnSameThread, and that queued the pair here and aborted the Netty-side call by throwing RunningOnDifferentThreadException (the connection has the crossing, in both directions). This is where most player input enters the world, but not all of it: the handlers that never call PacketUtils.ensureRunningOnSameThread reach the Server thread another way, when they reach it at all. Fifty-one of ServerGamePacketListenerImpl’s sixty-one game handlers open on that call, and the ten that do not divide into four kinds (the handlers that never hop lists them). Two really touch nothing — the ping reply and an empty custom-payload hook. Four — ServerGamePacketListenerImpl.handleChat, both command packets and the command-suggestion request — run their work through MinecraftServer.execute (the suggestions by way of a ServerCommandSuggestionsProvider, which keeps only the latest request, answers at most one a tick, and leaves one it has no budget for to the player’s own tick), and two more, filtered sign and book text, come back on a CompletableFuture completed against the server: so chat and commands arrive as tasks, drained by the event loop below, and not with the packets. The last two write listener state on Netty outright, because there is nothing to hand it to — a chat acknowledgement advances LastSeenMessagesValidator under a lock, and the acknowledgement that ends the play phase installs the configuration listener. And the chat family does a little real work before it hands over: ServerGamePacketListenerImpl.tryHandleChat reads the sender’s chat visibility and resets its last-action time on the Netty thread, which is the one place player state is written off the main thread by design.

Two gates sit on each queued pair. PacketListener.shouldHandleMessage is asked again at handling time, so a player who disconnected between arrival and now is dropped with a debug line rather than handled into a dead session. And a handler that throws does not end the tick: ServerPacketListener overrides PacketListener.onPacketError to log “suppressing error” and return, and ServerCommonPacketListenerImpl.onPacketError additionally files the throwable through MinecraftServer.reportPacketHandlingException into the SuppressedExceptionCollector that the next crash report dumps. Two things escape: a ReportedException wrapping an OutOfMemoryError, which PacketUtils.makeReportedException builds for the processor to throw, and any Error, which the catch does not cover.

An empty server stops ticking

Before anything else MinecraftServer.tickServer compares MinecraftServer.emptyTicks against MinecraftServer.pauseWhenEmptySeconds times twenty — the pause-when-empty-seconds property, default 60 in DedicatedServerProperties, and zero on the base class, which disables the feature. The counter advances only while nobody is online and the loop is not sprinting; on the tick it first reaches the threshold the server logs, autosaves once, and from then on runs MinecraftServer.tickConnection and returns before the rest of the tick. Only the dedicated server pauses this way, and its override still ticks the JSON-RPC ManagementServer after that return. MinecraftServer.tickCount does not advance, so a paused server is stopped in every sense that matters and still answers pings.

The integrated server pauses on a different signal. IntegratedServer.tickServer sets IntegratedServer.paused from Minecraft.isPaused or an empty player list, saves once on the way in, runs IntegratedServer.tickPaused — connections plus one statistic — instead of the real tick, and re-syncs the world time on the way out. It also copies the client’s render and simulation distance into the PlayerList on every unpaused tick, which is why singleplayer has no separate view-distance setting.

That pause is the pattern for every difference between the two servers on this page. The loop itself is written once: IntegratedServer and DedicatedServer override pieces of the tick and never the loop, so DedicatedServer.tickServer adds the JSON-RPC ManagementServer.tick, DedicatedServer.tickConnection the console, and IntegratedServer.tickServer the pause above. Starting a server is how the thread that runs MinecraftServer.runServer comes to exist.

What MinecraftServer.tickChildren runs, and in what order

MinecraftServer.tickChildren is the tick, and the rows below are its order. All but the first are a profiler section of their own; suspending the flush has none, and the debug row is three:

in orderwhat it doesskipped when
suspend flushingServerCommonPacketListenerImpl.suspendFlushing on every player’s connectionnever
command functionsServerFunctionManager.tick runs ServerFunctionManager.LOAD_FUNCTION_TAG once after a reload, then ServerFunctionManager.TICK_FUNCTION_TAGfrozen
clocksServerClockManager.tick advances the world clocks, a SavedData kept in the world_clocks filefrozen, or GameRules.ADVANCE_TIME is off
time syncMinecraftServer.forceGameTimeSynchronization broadcasts a ClientboundSetTimePacketnot a multiple of 20 ticks
levelsMinecraftServer.updateEffectiveRespawnData, then ServerLevel.tick for each dimension in MinecraftServer.getAllLevels order, overworld firstnever
connectionMinecraftServer.tickConnection — every Connection, and each playing client’s own ticknever
playersPlayerList.tick broadcasts a latency-only ClientboundPlayerInfoUpdatePacketits own counter has not passed 600, so it fires on every 601st call
debug, game tests, tickablesServerDebugSubscribers.tick, GameTestTicker.tick, the dedicated server GUI’s refresh through MinecraftServer.addTickablegame tests alone, when frozen
send chunksPlayerChunkSender.sendNextChunks, then ServerCommonPacketListenerImpl.resumeFlushing, per playernever

Three rows say frozen, and that is TickRateManager.runsNormally answering false. /tick freeze sets TickRateManager.isFrozen, and TickRateManager.tick turns that into this tick’s TickRateManager.runGameElements — unless /tick step left TickRateManager.frozenTicksToRun above zero, which it also decrements. Everything that consults TickRateManager.runsNormally stops: the functions, the clocks and the game tests in the table above, and inside a level every step the level tick marks running, every entity but a player or what carries one, and a player’s pushing of entities and the movement packets’ rate check.

A throwable out of ServerLevel.tick is caught, filled with the level’s details as “Exception ticking world” and rethrown as a ReportedException — which is how one bad dimension ends the whole server. Nothing else in the list is wrapped. Two things a reader looks for here are elsewhere: the /schedule queue is a TimerQueue the server owns and persists (MinecraftServer.getScheduledEvents) but ticks from inside ServerLevel.tickTime, which runs in the overworld alone and off the overworld’s gameTime (the level tick), and the last statement of MinecraftServer.tickChildren is ServerActivityMonitor.tick, a rate-limited nudge to the NotificationManager rather than anything the world can see.

Where a player’s own tick actually happens

The connection phase runs after every level. ServerConnectionListener.tick walks its synchronized list and, for each live Connection, calls Connection.tick: drain Connection.pendingActions, the one queue a Connection owns and which holds closures rather than packets; tick the TickablePacketListener; drop the connection if it has died; flush the channel; and every twentieth tick recompute the packet-rate averages. For a playing client that listener is ServerGamePacketListenerImpl, whose ServerGamePacketListenerImpl.tick acknowledges pending block changes, runs ServerPlayer.doTick through ServerGamePacketListenerImpl.tickPlayer, and then, only if that returns without having kicked anyone (or at once, with no player tick, while the server is paused), four more things in this order: the fifteen-second keep-alive (ServerCommonPacketListenerImpl.LATENCY_CHECK_INTERVAL), the three spam throttles, any command-suggestion request still waiting, and the idle-timeout check.

So a movement packet is applied to the player before any level ticks, and the player entity takes its step after all of them. Entities see the player where the packets put her; the player then ticks against a world that has already moved. An exception out of Connection.tick ends the tick only on singleplayer’s in-memory channel; elsewhere what it ends depends on the channel rather than on the fault (the connection). On a dedicated server DedicatedServer.tickConnection adds DedicatedServer.handleConsoleInputs, which is how a command typed at the console reaches the Server thread. RCON does not come this way: DedicatedServer.runCommand puts the command on the task queue with BlockableEventLoop.executeBlocking and waits for the answer, so it runs wherever the queue next drains. Players and sessions is what happens inside that phase; the connection is the channel underneath it.

The two writes each client gets

ServerCommonPacketListenerImpl.suspendFlushing at the top of MinecraftServer.tickChildren sets a flag that turns ServerCommonPacketListenerImpl.send into a channel write with no flush — but only for sends made on the Server thread, because the flag is tested together with BlockableEventLoop.isSameThread, so anything sent from another thread flushes on its own as before. Two things then empty the buffer. Connection.tick flushes the channel unconditionally, in the middle of its own body and so inside the connection phase, carrying everything the levels and the player’s own tick produced. ServerCommonPacketListenerImpl.resumeFlushing, after the chunk batch, both clears the flag and calls Connection.flushChannel itself, carrying the player-list update, the debug subscribers, the game-test ticker, the server’s own tickables and last the chunks.

Two writes to the socket per client come out of MinecraftServer.tickChildren: one after the levels, one after the chunks. A reply sent while the packets are drained before it, or from a task after it, is flushed on its own.

The pacing of that second write belongs to what the client is told; which chunks are eligible to be in it, to tickets and loading. PlayerChunkSender answers to the client’s own acknowledgements, so a slow client throttles its own chunks without slowing the tick.

The bookkeeping at the bottom

MinecraftServer.tickServer closes with three pieces of bookkeeping. The cached ServerStatus is rebuilt when the old one is more than MinecraftServer.STATUS_EXPIRE_TIME_NANOS (five seconds) old, so a ping never costs a walk of the player list. MinecraftServer.ticksUntilAutosave counts down to MinecraftServer.autoSave, and it counts ticks while promising minutes: it starts at MinecraftServer.AUTOSAVE_INTERVAL (6000) and is thereafter MinecraftServer.computeNextAutosaveInterval, the tick rate times 300, floored at MinecraftServer.MIMINUM_AUTOSAVE_TICKS (100 — the typo is Mojang’s). So an autosave is five minutes of the server’s scheduled time at any steady /tick rate, and at any set rate the floor never binds, since the rate cannot go below 1; MinecraftServer.onTickRateChanged re-derives the figure whenever that changes, but only ever shortens the pending countdown, and a sprint uses the measured rate from MinecraftServer.getAverageTickTimeNanos so that it saves at the speed it is really running. And the tick’s own duration replaces its slot in the hundred-entry MinecraftServer.tickTimesNanos ring, updates MinecraftServer.aggregatedTickTimesNanos, and folds into MinecraftServer.smoothedTickTimeMillis at MinecraftServer.AVERAGE_TICK_TIME_SMOOTHING. That ring is what /tick query reads, through MinecraftServer.getAverageTickTimeNanos and MinecraftServer.getTickTimesNanos.

The debug screen’s TPS chart is a different pipe: a SampleLogger with one slot per TpsDebugDimensions value, written from three points of the loop. MinecraftServer.logTickMethodTime records the tick method, MinecraftServer.finishMeasuringTaskExecutionTime the scheduled-task and idle slots after the wait, and MinecraftServer.logFullTickTime at the very bottom both flushes the four-slot sample and measures the whole iteration, tick plus wait. IntegratedServer logs it always; a dedicated server only while a client is subscribed to DebugSubscriptions.DEDICATED_SERVER_TICK_TIME.

The loop is instrumented in three more places, none of them that ring. Every iteration gets a fresh ProfilerFiller from MinecraftServer.createProfiler, composing the MetricsRecorder’s profiler with a SingleTickProfiler, and /debug start arms MinecraftServer.TimeProfiler at the top of an iteration through MinecraftServer.debugCommandProfilerDelayStart. MinecraftServer.tickFrame is a DiscontinuousFrame from TracyClient, opened before the packet drain and closed after the tick. JvmProfiler is fed MinecraftServer.smoothedTickTimeMillis after the wait, on the last line of the iteration — where MinecraftServer.isReady is also set, every lap rather than once.

The event loop, and what a tick’s spare time buys

MinecraftServer extends ReentrantBlockableEventLoop of TickTask: it is an Executor whose queue drains on the Server thread, and submitting a task to it is how most other threads touch server state — a packet takes the processor’s queue instead, and a console line the console’s list. This section is where the rest of the book sends you for that machinery.

flowchart TD
    P["BlockableEventLoop.pollTask peeks the head"] --> E{"anything queued"}
    E -- "a task" --> B{"blocking depth above zero"}
    B -- "inside BlockableEventLoop.managedBlock" --> RUN["run it, report true"]
    B -- "not blocked" --> S{"MinecraftServer.shouldRun"}
    S -- "older than three ticks" --> RUN
    S -- "otherwise" --> H{"MinecraftServer.haveTime"}
    H -- "time left" --> RUN
    H -- "out of time, left queued" --> OFF["nothing ran from the server's own queue"]
    E -- "nothing queued" --> OFF

The one question the head of the queue is asked, and the three independent reasons the answer is yes; the two edges into the bottom box are the ones the next figure picks up.

Read the bottom box twice. An empty queue and a queue whose head may not run yet are the same answer to MinecraftServer.pollTaskInternal, and that answer is the first of the two things a level’s chunk source waits for; the guard in the next figure is the second.

Every runnable becomes a TickTask

MinecraftServer.wrapRunnable stamps the current MinecraftServer.tickCount onto every task queued for the server, from whichever thread. That stamp is the whole of a task’s identity to the scheduler: MinecraftServer.shouldRun lets a task run when there is time left, or when it is older than MinecraftServer.MAX_TICK_LATENCY (three) ticks — so a saturated server still drains its queue, late but in order and without unbounded growth. Submitting from the Server thread runs the work inline unless a task is already running: ReentrantBlockableEventLoop.scheduleExecutables reports true then, so re-entrant work queues instead of nesting. MinecraftServer.scheduleExecutables and MinecraftServer.executeIfPossible, the two doors into the queue, both answer differently once the server has stopped, which is the first move of how a server dies.

A task that throws an exception is not the loop’s problem either. BlockableEventLoop.doRunTask logs the failure under the fatal marker and returns, rethrowing only what BlockableEventLoop.isNonRecoverable calls unrecoverable — a ReportedException wrapping an OutOfMemoryError or a StackOverflowError — while an Error thrown bare passes its catch untouched.

On a dedicated server a worker thread that dies surfaces here too, as a throw out of BlockableEventLoop.pollTask rather than out of anything the tick called: that is the relay how a server dies describes, and what it becomes — the crash report, the shutdown the loop’s finally performs, and the watchdog that reads MinecraftServer.getNextTickTime from outside the loop and halts the JVM without saving — is that page’s whole subject.

The budget, and where it stops applying

MinecraftServer.haveTime is the BooleanSupplier the whole tick is handed. It is true unconditionally while a task is running (ReentrantBlockableEventLoop.runningTask), and otherwise compares the clock against MinecraftServer.delayedTasksMaxNextTickTimeNanos or MinecraftServer.nextTickTimeNanos depending on MinecraftServer.mayHaveDelayedTasks — which MinecraftServer.runServer sets true, together with the slack deadline, immediately after the tick, and which every subsequent MinecraftServer.pollTask overwrites with was there more.

It stops applying the moment the thread blocks. Inside BlockableEventLoop.managedBlock the blocking depth is non-zero, so BlockableEventLoop.shouldRunAllTasks is true and BlockableEventLoop.pollTask never consults MinecraftServer.shouldRun: every queued task runs, budget and age irrelevant. A level that blocks on a chunk mid-tick does the same on its own ServerChunkCache.MainThreadExecutor, whose tasks always run, and does not deadlock for the same reason — the wait is the drain, and the thread doing the waiting is the thread that completes the thing it waits for. MinecraftServer.waitUntilNextTick is the mechanism used deliberately: BlockableEventLoop.runAllTasks, then BlockableEventLoop.managedBlock on no time left. MinecraftServer.waitForTasks parks with LockSupport until MinecraftServer.nextTickTimeNanos — or 100 µs at a time when the loop is not waiting on a tick — and BlockableEventLoop.schedule unparks it, so a submitted task cuts the park short. PacketProcessor.scheduleIfPossible pointedly does not: a packet landing in the slack waits for the next drain.

What the budget actually gates

Three things are all MinecraftServer.haveTime decides inside the tick, once it has travelled from MinecraftServer.tickServer through MinecraftServer.tickChildren, ServerLevel.tick, ServerChunkCache.tick and ChunkMap.tick.

They are ChunkMap.processUnloads (the unload queue, which drains anyway while it holds more than two thousand entries), ChunkMap.saveChunksEagerly (at most twenty chunks a tick, and only under 128 outstanding writes) and SectionStorage.tick by way of PoiManager.tick (the village-point data of dirty chunks being written out). Loading a chunk, generating one, propagating tickets and ticking chunks take no supplier and are not gated at all. A late server does not load fewer chunks, then; it postpones unloading and saving them, and its memory grows while it is behind.

The slack, and the sprint that inverts it

After the tick, MinecraftServer.waitUntilNextTick spends the remaining milliseconds. MinecraftServer.pollTaskInternal polls the server’s own queue first and, only if that queue had nothing to run, offers the levels’ ServerChunkCache.MainThreadExecutor.pollTask a turn, one after another until one runs something — when the loop is sprinting, or blocked, or still in time. The levels get the leftovers of the leftovers.

flowchart TD
    N["nothing ran from the server's own queue"] --> G{"sprinting, blocked or in time"}
    G -- "no" --> W["report false, and MinecraftServer.waitForTasks parks"]
    G -- "yes" --> C{"any level's chunk source with work"}
    C -- "one had" --> CRUN["a chunk source ran, report true"]
    C -- "none did" --> W

What the leftover milliseconds buy: the guard that has to pass before the levels are asked at all, and the two ways the poll still ends in a park.

That executor keeps a policy of its own: its ServerChunkCache.MainThreadExecutor.shouldRun is unconditionally true, with no age rule, and its poll runs ServerChunkCache.runDistanceManagerUpdates first and returns at once if that did any work. The first thing leftover milliseconds buy is chunks changing status; the light schedule and the one queued chunk task only happen on a poll where the graphs were already quiet.

Sprinting inverts the arithmetic. MinecraftServer.processPacketsAndTick hands MinecraftServer.tickServer a constant false instead of MinecraftServer.haveTime, so unloading, eager saving and section flushing stop for the length of the sprint — and yet ServerTickRateManager.isSprinting is the first term of the condition guarding the chunk-source poll, so the levels are offered the turn on every poll regardless. A sprint therefore does more chunk work per wall-clock second than an ordinary server, not less, while doing little of the housekeeping that would let the results reach the disk — the exceptions being the unload queue, which the two-thousand rider drains whether there is time or not, and the autosave, which keeps counting.

Questions players ask

Does freezing stop the server? It stops the world, not the loop. MinecraftServer.tickCount still increments, connections still tick and packets are still answered, so a frozen server is one you can log into, walk about in and type commands at — TickRateManager.isEntityFrozen exempts players and anything carrying one — while nothing around you moves.

Is the tick rate settable to anything? Between TickRateManager.MIN_TICKRATE (1.0) and TickCommand.MAX_TICKRATE (10000). Clients are told: ServerTickRateManager.setTickRate and ServerTickRateManager.setFrozen broadcast a ClientboundTickingStatePacket, ServerTickRateManager.stepGameIfPaused a ClientboundTickingStepPacket, and ServerTickRateManager.updateJoiningPlayer sends both to anyone arriving. A sprint is not announced as such — a client is sent an unfrozen state as it starts and, if the game was frozen, the refreeze after.

Why is an empty Nether nearly free? Because a dimension that no player’s simulation ticket, forced chunk, portal or ender pearl keeps active stops ticking entities, block entities and any dragon fight after 300 ticks of ServerLevel.emptyTime, while still running its chunk-source work. That rule belongs to the level tick, and tickets and loading owns which ticket resets it.

Where to look

MinecraftServer.runServer · MinecraftServer.processPacketsAndTick · MinecraftServer.tickServer · MinecraftServer.tickChildren · MinecraftServer.waitUntilNextTick · MinecraftServer.haveTime · BlockableEventLoop · PacketProcessor · PacketUtils · TickRateManager · ServerTickRateManager · ServerConnectionListener.tick · Connection.tick · ServerCommonPacketListenerImpl.send · ServerChunkCache.MainThreadExecutor · IntegratedServer · DedicatedServer


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The level tick

Verified against Minecraft 26.3 · Part III · A player stands still for one twentieth of a second while a whole dimension advances — weather, scheduled ticks, spawns, every entity, every block entity.

Nobody moves. A wheat crop is one random tick short of ripe, a creeper is walking a fence line, rain has been falling for four minutes and a piston somewhere is halfway through extending. Then ServerLevel.tick runs, once, for this dimension, and all of it advances: the border interpolates, the weather counts down, the scheduled block ticks fire, mobs are counted and spawned, chunks near players get their random ticks, every entity in range runs its Entity.tick, block entities run their tickers, and the block changes that all of that produced are handed to the clients. It is one method on the Server thread, called once per dimension from MinecraftServer.tickChildren (the server tick), overworld first. The order inside it is the whole lecture, because one of the steps is in a place nobody expects: the block-change broadcast runs before the entities tick. ServerChunkCache.broadcastChangedChunks comes before ChunkMap.tick and long before EntityTickList.forEach, so a block a command changed between two ticks reaches your screen in the tick that follows, and a block a piston changed during that tick reaches it a tick later.

Three ranges, before we need them

Three phrases run through everything below and they are all one number line, which counts outwards: a low level is a chunk somebody is standing in and a high one is a chunk at the edge of what the server bothers with, up to ChunkLevel.MAX_LEVEL. A chunk is loaded when it has a ChunkHolder at all; it is block-ticking at level 32 or below (ChunkLevel.BLOCK_TICKING_LEVEL) and entity-ticking at 31 or below (ChunkLevel.ENTITY_TICKING_LEVEL), and those last two answers come from the simulation graph, through DistanceManager.inBlockTickingRange and DistanceManager.inEntityTickingRange. How a chunk gets its level — which tickets put it there and which graph they feed — is Part IV’s tickets and loading; for this page it is enough that block-ticking reaches one chunk further out than entity-ticking, and that both are brought up to date inside this tick, before anything but the scheduled ticks and the raids reads them.

The cast

classwhat it decidesthread
ServerLevelthe order of the tick, and the gates on its own steps — one instance per dimensionServer
ServerChunkCachethe chunk half of the tick: ticket purging, distance updates, spawning, random ticks, the broadcastServer
ChunkMapwhich chunks are candidates for spawning, which are entity-ticking, and every player’s view of every entityServer
ChunkHolderone chunk’s pending block and light changes, and which packet shape they becomeServer
LevelTicksthe two scheduled-tick queues, one LevelChunkTicks per chunk, drained by priorityServer
EntityTickListwhich entities are ticked, and a stable view of that set while it is being walkedServer
PersistentEntitySectionManagerwhich entities exist and which of them are ticking — the tick list’s only editorServer; its inbox is filled by IO threads
TickRateManagerwhether this is a normal tick at all, through TickRateManager.runsNormallyServer

ServerLevel is one of the two subclasses of the abstract Level, and the split shows in this page’s own names. Level holds what a world has on either side — the block-entity ticker list, the CollectingNeighborUpdater, the two weather floats, Level.random and the level data — and leaves the questions only an authority can answer abstract, Level.sendBlockUpdated and Level.tickRateManager among them. ServerLevel answers those and adds the authority itself: the chunk source, the two scheduled-tick queues, the entity manager, the list of players. So where a step below is spelled with Level rather than ServerLevel the client inherits it too, and the client level is the list of the ones it has hollowed out.

The whole tick, and its three gates

The tick is one method calling its steps one after another, so its shape is not a conversation — it is a column with guards down the side. There are three guards, and every step is behind one of them, a combination of them, or nothing: running (TickRateManager.runsNormally, false while /tick freeze holds and no step is pending), not a debug world (Level.isDebug, true only in the Debug Mode world type), and the dimension is not empty (ServerLevel.emptyTime below ServerLevel.EMPTY_TIME_NO_TICK, 300).

flowchart TD
    START["MinecraftServer.tickChildren calls ServerLevel.tick, overworld first"]
    ENV["ServerLevel.handlingTick goes true, the attribute cache dropped"]
    WB["WorldBorder.tick, then ServerLevel.advanceWeatherCycle"]
    SLEEP["the sleep check: the clock, the players, the weather"]
    SKY["Level.updateSkyBrightness"]
    TIME["ServerLevel.tickTime, overworld only"]
    SCHED["ServerLevel.blockTicks, then ServerLevel.fluidTicks"]
    RAID["Raids.tick"]
    subgraph SCC["ServerChunkCache.tick"]
        direction TB
        PURGE["TicketStorage.purgeStaleTickets"]
        DIST["ServerChunkCache.runDistanceManagerUpdates"]
        CHUNKS["spawns, random ticks, thunder, custom spawners"]
        CAST["ServerChunkCache.broadcastChangedChunks"]
        TRACK["ChunkMap.tick: entity tracking"]
        UNLOAD["ChunkMap.tick with the budget: POI saves and unloads"]
        PURGE --> DIST --> CHUNKS --> CAST --> TRACK --> UNLOAD
    end
    EVENTS["ServerLevel.runBlockEvents, then ServerLevel.handlingTick false"]
    EMPTY["ServerLevel.emptyTime: reset by a ticket, otherwise rising"]
    DRAGON["EnderDragonFight.tick, the End only"]
    ENT["EntityTickList.forEach: each entity, then its riders"]
    BE["Level.tickBlockEntities"]
    EM["PersistentEntitySectionManager.tick: the inbox, then the unload set"]
    DBG["LevelDebugSynchronizers.tick"]
    GC["RandomState.garbageCollect"]
    START --> ENV --> WB --> SLEEP --> SKY --> TIME --> SCHED --> RAID --> PURGE
    UNLOAD --> EVENTS --> EMPTY --> DRAGON --> ENT --> BE --> EM --> DBG --> GC

The order, and the one containment in it: the six steps in the box are not siblings of the steps around them, they are the insides of a single call, and a later section counts them as five things.

The gates are the other half of the tick and they do not fit in the boxes. Read this table down the middle instead: a step with three blank cells runs on a frozen, empty, debug world.

the steprunningnot a debug worldthe dimension is not empty
ServerLevel.handlingTick true, the attribute cache dropped
WorldBorder.tick, ServerLevel.advanceWeatherCycle•
the sleep check
Level.updateSkyBrightness
ServerLevel.tickTime•
ServerLevel.blockTicks, ServerLevel.fluidTicks••
Raids.tick•
TicketStorage.purgeStaleTickets•
ServerChunkCache.runDistanceManagerUpdates
spawns, random ticks, thunder, custom spawners••
ServerChunkCache.broadcastChangedChunks•
ChunkMap.tick, entity tracking
ChunkMap.tick with the budget
ServerLevel.runBlockEvents•
ServerLevel.handlingTick false
ServerLevel.emptyTime rises•
EnderDragonFight.tick••
EntityTickList.forEach•
Level.tickBlockEntitiesper ticker•
PersistentEntitySectionManager.tick
LevelDebugSynchronizers.tick
RandomState.garbageCollect

All but the last of those steps have names, and they are the names a profiler reports: world border and weather, tickPending (holding blockTicks and fluidTicks), raid, chunkSource, blockEvents, entities (holding dragonFight, checkDespawn and the per-entity tick), blockEntities, entityManagement and debugSynchronizers. Half a dozen pages elsewhere in the book place a mechanism by saying it runs after tickPending or in the chunk source phase, and they mean these; so does a /debug profiling report.

Read the gate columns and most of the page’s surprises fall out of the table. Sleeping through the night works with the game frozen. A frozen world still loads, sends and unloads chunks — and stops expiring its tickets. A debug world keeps its entities and drops its block updates. And the last three steps run on a dimension with nobody in it.

What the tick does before anything can move

The cache that is dropped before the border

The first thing the tick does to the world, before the border and before the weather, is EnvironmentAttributeSystem.invalidateTickCache: last tick’s resolved environment attributes are thrown away. That system (environment attributes) is where the old per-dimension and per-biome constants went, and Level.updateSkyBrightness later in this same tick reads EnvironmentAttributes.SKY_LIGHT_LEVEL out of it rather than deriving sky light from the time of day. ServerClockManager invalidates the same cache on every level whenever a clock is set, moved, paused or given a new rate, so the level is not its only owner — it is the first reader of the tick, and it starts clean.

The weather is the server’s; only the fade is the level’s

Then WorldBorder.tick advances the interpolated extent, and ServerLevel.advanceWeatherCycle counts the clear, rain and thunder timers down under GameRules.ADVANCE_WEATHER, resampling the rain and thunder timers from ServerLevel.RAIN_DELAY, ServerLevel.RAIN_DURATION, ServerLevel.THUNDER_DELAY and ServerLevel.THUNDER_DURATION as each expires, and fading Level.rainLevel and Level.thunderLevel by 0.01 a tick, which is why a downpour arrives as a five-second ramp. The countdowns belong to the server: ServerLevel.getWeatherData delegates to MinecraftServer.getWeatherData, one WeatherData shared by every dimension, and the only per-level parts are those two floats and the Level.canHaveWeather test that decides whether this dimension acts on any of it. Every move of a float is a ClientboundGameEventPacket (ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) to this dimension’s players, and a start or a stop goes, with both levels, to every player in every dimension.

A freeze stops the clock and not the sleep check

SleepStatus.areEnoughSleeping and SleepStatus.areEnoughDeepSleeping (against GameRules.PLAYERS_SLEEPING_PERCENTAGE) decide the night skip, and they are checked outside every gate. If the dimension type has a default clock and GameRules.ADVANCE_TIME is on, ServerClockManager.moveToTimeMarker jumps that clock to ClockTimeMarkers.WAKE_UP_FROM_SLEEP; ServerLevel.wakeUpAllPlayers gets everyone out of bed, and, if it is raining and GameRules.ADVANCE_WEATHER is on, ServerLevel.resetWeatherCycle clears the storm. The ClientboundSetTimePacket that follows is sent by the clock manager, not by the level — day time is not the level’s state at all.

What the level does own is gameTime, and only in the overworld: ServerLevel.tickTime advances it when the level was built with its ServerLevel.tickTime flag set, which MinecraftServer does for the overworld alone, and every other dimension reads that number. The same call advances the server-wide TimerQueue behind /schedule (MinecraftServer.getScheduledEvents), so a scheduled function is timed off overworld gameTime and stands still while the world is frozen.

Scheduled ticks, twice, and a promise to the same block

LevelTicks.tick is called twice — ServerLevel.blockTicks, then ServerLevel.fluidTicks — each with the current gameTime and a budget of ServerLevel.MAX_SCHEDULED_TICKS_PER_TICK, 65536, and both skipped entirely in a debug world. Each call collects every LevelChunkTicks container whose head is due and whose chunk passes ServerLevel.isPositionTickingWithEntitiesLoaded, drains what the budget allows, and hands each drained tick to ServerLevel.tickBlock or ServerLevel.tickFluid — both of which check that what is at the position is still what the tick named before running it. The order the drain uses, the dedup rule that decides what was ever queued, and why that re-check is the whole of cancellation belong to scheduled ticks; what belongs to this page is that the two calls happen here, in this order, under this budget.

The chunk source does five things in one call

ServerChunkCache.tick receives the server’s MinecraftServer.haveTime supplier (or, while sprinting, a constant false in its place) — the level never looks at it, it only passes it on — and does five things in order.

  1. Purge stale tickets — while running only, so a frozen world holds on to expired portal and pearl tickets indefinitely.
  2. Run ServerChunkCache.runDistanceManagerUpdates — ungated, and the tick’s own run of the updates that change a chunk’s ticking state. The reason an entity starts or stops ticking this tick is decided here, several steps before the entity loop reads it.
  3. Tick the chunks, then broadcast — one call holding two halves, both skipped in a debug world: the spawning and random-ticking work below, and then the block changes turned into packets. Each half has a section of its own.
  4. Update entity tracking — ChunkMap.tick, ungated, where last tick’s movement becomes packets.
  5. Spend whatever time is left on POI saving and chunk unloads.

That last step is the only part of the whole level tick that yields to the clock, and even it does not yield completely: ChunkMap.processUnloads force-drains anything over two thousand queued unloads whatever the budget says.

Two chunk sets, and two different mob caps

NaturalSpawner.createState walks ServerLevel.getAllEntities — every entity in the dimension except the ones in MobCategory.MISC (items, projectiles and armour stands, but villagers, golems and minecarts too), and except mobs that require persistence — and counts the rest per MobCategory, using the chunk each one stands in to charge a spawn-potential field and to feed a LocalMobCapCalculator. Walking every entity in the dimension is what this step costs, once a tick, before any spawn is attempted; the two caps that census feeds — a server-wide one scaled by the spawnable-chunk count and a per-player one that is not — belong to entity lifecycle. The categories marked persistent — the animals — are considered only on a tick where gameTime divides by 400, and GameRules.SPAWN_MOBS stops the spawns themselves, not the census before them or the thunder beside them.

Two chunk sets are then walked, and they are not the same set. ChunkMap.collectSpawningChunks gathers the spawning chunks: the chunks within eight of a player (the radius DistanceManager.naturalSpawnChunkCounter counts) that have a ticking chunk and at least one non-spectating player within 128 blocks (ChunkMap.playerIsCloseEnoughForSpawning). They are shuffled, and each then gets ChunkAccess.incrementInhabitedTime, then ServerLevel.tickThunder if it is also entity-ticking, and then NaturalSpawner.spawnForChunk if ServerLevel.canSpawnEntitiesInChunk. The thunder is a 1-in-100000 roll per chunk per tick while raining and thundering, whose bolt prefers a lightning rod, then a mob that can see the sky, then the heightmap, and which brings a trap skeleton horse along at effective difficulty × 1 % when GameRules.SPAWN_MOBS is on and no lightning rod is under the strike.

The second set is ChunkMap.forEachBlockTickingChunk, which despite its name walks DistanceManager.forEachEntityTickingChunk — the entity-ticking set, level 31 and below — and keeps only those whose ChunkHolder has a live ChunkHolder.getTickingChunk. Each survivor gets ServerLevel.tickChunk.

Random ticks are counted per section, and empty sections are free

ServerChunkCache.tickChunks reads GameRules.RANDOM_TICK_SPEED (default 3, minimum 0) once per level tick and hands it down, and ServerLevel.tickChunk does two things with it. It rolls that many 1-in-48 chances at ServerLevel.tickPrecipitation — the ice and snow layer, using Biome.shouldFreeze, Biome.shouldSnow and GameRules.MAX_SNOW_ACCUMULATION_HEIGHT — and then, for every LevelChunkSection in the chunk that reports LevelChunkSection.isRandomlyTicking, picks that many random positions and rolls both BlockBehaviour.BlockStateBase.randomTick and the fluid’s at each. LevelChunkSection.isRandomlyTicking is a counter maintained on every block change, so a section of solid stone costs nothing at all: the loop over sections is a loop over the chunk’s interesting height. Set the rule to zero and both halves stop, ice and snow included.

The position itself does not come from Level.random. Level.getBlockRandomPos advances Level.randValue, a plain linear congruential generator, and unpacks x, y and z out of one integer. Everything the block then does with that position — the crop-growth roll, fire spreading, every implementation of BlockBehaviour.BlockStateBase.randomTick — takes Level.random, which is also what the ice and snow rolls, the lightning roll and the spawning-chunk shuffle use.

The custom spawners come last inside this step. ServerLevel.tickCustomSpawners runs the five a level was constructed with, which in practice means the overworld’s (the other ways in names them). Three of the five carry a game rule of their own (GameRules.SPAWN_PHANTOMS, GameRules.SPAWN_PATROLS, GameRules.SPAWN_WANDERING_TRADERS); the other two carry none, and GameRules.SPAWN_MOBS gates the whole call — though VillageSiege, like the phantom and patrol spawners, also stops when GameRules.SPAWN_MONSTERS is off.

The broadcast, which is why entities are a tick behind

Almost nothing in the game sends a block update at the moment a block changes — a landing FallingBlockEntity is the exception worth knowing, and it is dealt with at the end of this section. ServerLevel.sendBlockUpdated calls ServerChunkCache.blockChanged, which finds the ChunkHolder, records the position in that holder’s per-section set, and — the first time a holder gains a changed section — adds it to ServerChunkCache.chunkHoldersToBroadcast. Then ServerChunkCache.blockChanged returns. The packets are built once, later, in the chunk source’s step.

sequenceDiagram
    participant SL as ServerLevel
    participant SCC as ServerChunkCache
    participant CH as ChunkHolder
    participant Wire as the network

    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: before the tick — a command changed a hundred blocks
        SL->>SCC: blockChanged, once per block
        SCC->>CH: blockChanged, one entry in that section's set
        CH-->>SCC: nothing leaves, the holder is only marked
    end
    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: this tick, inside ServerChunkCache.tick
        SCC->>CH: broadcastChanges, on every marked holder
        CH->>Wire: ClientboundLightUpdatePacket, if either light filter has anything
        CH->>Wire: one ClientboundBlockUpdatePacket per single-change section
        CH->>Wire: one ClientboundSectionBlocks<br/>UpdatePacket per busier section
        CH->>Wire: BlockEntity.getUpdatePacket beside any changed block entity
        Note over SL,Wire: ChunkMap.tick then turns last tick's movement into packets
    end
    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: still this tick, several steps later
        SL->>SL: tickBlockEntities: a piston head finishes its push
        SL->>SCC: blockChanged, marked, and it waits for the next tick
    end

Three bands: the write before the tick, the send inside it, and a write that arrives too late for the send. The dotted arrow in the first band is the hook — a block change leaves nothing on the wire at the moment it happens.

ChunkHolder.broadcastChanges asks two different questions of two different audiences, and it asks the light one first: if either light filter has anything in it, ChunkHolder.PlayerProvider.getPlayers is called with the border-only flag set and one ClientboundLightUpdatePacket goes to the players on the edge of their tracked area, before a single block packet is built. The blocks are then emitted per 16³ section, to everyone tracking the chunk, and the shape depends on how many blocks in that section changed: one ClientboundBlockUpdatePacket for a single change, one ClientboundSectionBlocksUpdatePacket for several, plus BlockEntity.getUpdatePacket for any changed position that carries a block entity. A hundred blocks changed by one command are therefore a handful of packets, one per affected section — not a hundred.

Then ChunkMap.tick runs — chunk tracking for each player, and ChunkMap.TrackedEntity for each entity, which is where ServerEntity.sendChanges turns last tick’s movement into packets. Blocks first, entities second, and the entity loop only after both. The ordering is visible from a client: a player’s /setblock lands in the tick right after it runs, because a player’s command runs as a task between two ticks, before MinecraftServer.tickChildren starts, while a piston head, moved inside a tick after that tick’s broadcast, lands in the tick after the one that moved it. A command typed at the console is as late as the piston, because it too changes blocks after the broadcast: it is drained in the connection phase, which MinecraftServer.tickChildren runs after every level has already broadcast.

Falling sand is the exception that proves the rule, and it is deliberate. FallingBlockEntity calls LevelWriter.setBlockAndUpdate and then, straight after, ChunkMap.sendToTrackingPlayers twice: a ClientboundBlockUpdatePacket of its own, and a ClientboundAddTransientBlockPacket the client hands its renderer as a TransientBlock — so the block the client sees appear is sent in the same tick the entity that placed it vanished, rather than a tick behind it. A handful of other places do the same thing to one player rather than to everyone tracking the chunk. What the client does with all of this is what the client is told.

Block events close the handlingTick window

ServerLevel.runBlockEvents drains ServerLevel.blockEvents — the note-block plays, piston pushes and chest-lid counts queued since the last drain, and any the drain itself raises — running every event inside the block-ticking range and putting the rest back for the next one, and this is the only point in the tick at which it happens. What the queue promises the blocks in it, and why a piston told about a change by a packet handler still moves in the same tick this phase belongs to, is pistons and block events.

ServerLevel.handlingTick went true at the very top of the tick and goes false here, so the whole entity half runs outside that window. Its one reader in the game is PistonBaseBlock, which uses it to tell a piston update raised inside the tick from one raised outside it.

An empty dimension skips exactly three things

ServerChunkCache.hasActiveTickets — really TicketStorage.shouldKeepDimensionActive, which is the players’ simulation tickets and the forced, portal and ender-pearl tickets — resets ServerLevel.emptyTime at this point in the tick, and two other calls reset it: Entity.teleportCrossDimension for the level a non-player entity arrives in, and ServerPlayer.registerAndUpdateEnderPearlTicket, from a thrown pearl’s own tick in the entity loop, for the pearl’s level. Otherwise the counter rises, and it rises only while running, so a frozen dimension never falls asleep. Past ServerLevel.EMPTY_TIME_NO_TICK, 300 ticks, the level skips the dragon fight, the entity loop and the block entities. That is the entire skip: the weather, the scheduled ticks, the chunk source, the block events, the entity manager’s load and unload drain and the debug feed all keep running on a dimension nobody has visited for fifteen seconds.

Every entity, and then its riders

EntityTickList.forEach walks the tick list. An entity is skipped if it has been removed, or if TickRateManager.isEntityFrozen says so — frozen, and neither a player nor something carrying one. Otherwise it gets Entity.checkDespawn, and then ticks only if it is a ServerPlayer or its chunk answers DistanceManager.inEntityTickingRange. A passenger whose vehicle is alive and still lists it is passed over here and ticked by the vehicle instead; a stale link is broken with Entity.stopRiding. ServerLevel.tickNonPassenger calls Entity.commonTick, which counts Entity.invulnerableTime down, records the old position and bumps Entity.tickCount, and then Entity.tick; ServerLevel.tickPassenger runs Entity.rideTick for each rider that is a Player or in the tick list, recursively down the stack. Level.guardEntityTick wraps each one in a crash report titled Ticking entity, so a mob that throws names itself.

The list stays stable under all of that by construction. Membership changes during the loop — a bred baby, a fired arrow, a bolt a trident calls down — arrive through ServerLevel.EntityCallbacks.onTickingStart immediately. EntityTickList allows exactly one EntityTickList.forEach at a time and, on the first add or remove while it is iterating, copies into its EntityTickList.passive map and swaps that with EntityTickList.active: the running loop keeps walking the view it started with, and the new entity waits for the next tick. Which entities are in the list at all is PersistentEntitySectionManager’s answer, through Visibility.fromFullChunkStatus — only FullChunkStatus.ENTITY_TICKING maps to a ticking visibility — with one override, Player.isAlwaysTicking, which keeps a player in the list whatever its chunk is doing; the ServerPlayer test inside the loop is the other half of that exemption, letting the player past the range check. Entities are Part VI’s subject: entity lifecycle.

Block entities reach one chunk further than mobs

Level.tickBlockEntities walks Level.blockEntityTickers, dropping removed tickers as it goes and running the rest — but only those whose position passes ServerLevel.shouldTickBlocksAt, which is the block-ticking range, and only while TickRateManager.runsNormally. That single condition is why a furnace keeps smelting one chunk further out than a zombie keeps walking. A block entity created during the walk — a sculk sensor a catalyst’s spread has just grown — lands on Level.pendingBlockEntityTickers instead, because Level.tickingBlockEntities is true, and is merged in when the next tick’s walk begins. Block entities has the rest.

After the entities: the manager’s drain and the debug feed

PersistentEntitySectionManager.tick drains PersistentEntitySectionManager.loadingInbox, a concurrent queue that chunk storage fills on the Server thread once an IO read returns — entities from freshly loaded chunks join the world here — and then processes PersistentEntitySectionManager.chunksToUnload. The ServerLevel.EntityCallbacks it fires (ServerLevel.EntityCallbacks.onTickingStart, ServerLevel.EntityCallbacks.onTickingEnd) are exactly what add and remove entries in the tick list the previous step walked.

Then LevelDebugSynchronizers.tick pushes this tick’s changed brains, paths and raids to any client subscribed through DebugSubscriptions — and, just before it, arms or clears the neighbour listener on CollectingNeighborUpdater, which sends each neighbour update the moment it happens, depending on whether anyone is subscribed to DebugSubscriptions.NEIGHBOR_UPDATES at all. It is the one step of the level tick whose entire output is diagnostic, and it is outside every gate. The last step is RandomState.garbageCollect, which drops the density buffers world generation pooled and no longer uses.

What leaves the level, and when

the packetthe step that sends itto whom
ClientboundGameEventPacket, rain and thunder levelsServerLevel.advanceWeatherCyclethis dimension’s players, and everyone on a start or a stop
ClientboundGameEventPacket, start and stop rainingthe same step, on a transitionevery player in every dimension
ClientboundSetTimePacketServerClockManager.moveToTimeMarker, from the sleep skipeveryone, sent by the clock manager
ClientboundBlockUpdatePacket · ClientboundSectionBlocksUpdatePacketChunkHolder.broadcastChanges, per 16³ sectioneveryone tracking the chunk
BlockEntity.getUpdatePacketthe same walk, beside a changed position that has oneeveryone tracking the chunk
ClientboundLightUpdatePacketthe same walk, before the blocksthe players at the edge of their tracked area
entity moves, and the adds and removes a move causesChunkMap.TrackedEntity, inside ChunkMap.tickwhoever tracks that entity
ClientboundBlockEventPacketServerLevel.runBlockEventsplayers within 64 blocks

None of them go out when they are written. Every one is queued behind the suspended flush that MinecraftServer.tickChildren opens around all the levels, and leaves on the wire when Connection.tick flushes the channel in the connection phase, after every level has ticked (the server tick).

Questions players ask

Does /tick freeze stop the server doing work? Barely, on the chunk side. Distance updates, the block-change broadcast, entity tracking and chunk unloads all run every tick regardless. Two things inside the chunk source are frozen and are easy to miss: the spawning and random-ticking step, and TicketStorage.purgeStaleTickets — so a frozen world accumulates expired tickets and never releases the chunks they hold.

Why did replacing a block cancel its scheduled tick? Because a scheduled tick is a promise to that block: ServerLevel.tickBlock compares the Block at the position with the one scheduled, and a mismatch runs nothing.

Why does /weather rain in the Nether change the overworld? There is one WeatherData on the MinecraftServer, and the command writes it from whatever dimension it runs in; only a dimension that Level.canHaveWeather counts it down. The fade of Level.rainLevel and Level.thunderLevel is per level, and only there.

Why does an arrow a skeleton fires not move until the next tick? EntityTickList swapped its maps the moment the arrow was added, so the loop that is running kept the view it started with. The arrow is in the list — it is just not in this walk of it. A mob that spawned naturally is ticked at once: the chunk source added it before the walk began.

For a 1.21-era reader. The day–night cycle is out of the level. Time is a set of WorldClocks owned by ServerClockManager and ticked by the server, and the ClientboundSetTimePacket after a night skip is sent by the clock manager rather than by the level. What ServerLevel still owns is gameTime, advanced by ServerLevel.tickTime in the overworld alone, which every other dimension reads. The weather countdowns went the same way, into one WeatherData on the MinecraftServer.

Where to look

In the order the tick runs them. ServerLevel.tick · EnvironmentAttributeSystem.invalidateTickCache · ServerLevel.advanceWeatherCycle · SleepStatus.areEnoughSleeping · ServerClockManager.moveToTimeMarker · ServerLevel.tickTime · LevelTicks.tick · ServerChunkCache.tick · ServerChunkCache.tickChunks · NaturalSpawner.createState · ServerLevel.tickChunk · ServerChunkCache.broadcastChangedChunks · ChunkHolder.broadcastChanges · ChunkMap.tick · ServerLevel.runBlockEvents · EntityTickList.forEach · Level.tickBlockEntities · PersistentEntitySectionManager.tick · LevelDebugSynchronizers.tick · RandomState.garbageCollect


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Players and sessions

Verified against Minecraft 26.3 · Part III · One connection, four events: a player joins, dies, walks into a Nether portal, and logs out.

A player clicks a server in the list, watches a progress bar, and is standing in a world. An hour later they fall in lava, press Respawn, walk through a portal and log off for the night. On the server those are four events on one object graph, and they disagree about what survives. Dying destroys the ServerPlayer and builds a new one; a trip to the Nether keeps the same object and moves it. Both keep the player’s entity id, and both keep the very same ServerGamePacketListenerImpl — a respawn does not rebuild the connection, it repoints it, in one field assignment, at the new player. Which is why anyone still holding the old ServerPlayer after a respawn is holding a corpse: an object flagged removed, out of every list, that will keep answering questions all day.

The cast

classwhat it decidesthread
PlayerListwho is admitted, who is in the tab list, and the exact order of the join burstServer
ServerPlayerone player’s world state, and what a new one inherits from an old oneServer
ServerGamePacketListenerImplthe session: the player it points at, the flush suspension, the load gate, the kicksNetty to decode, Server to handle
ServerConfigurationPacketListenerImplthe strictly serial task queue a join is prepared inNetty, with four handlers hopping to Server
PrepareSpawnTaskwhere the player will stand, and when the ServerPlayer is finally constructedServer, or Netty when accepting a code of conduct starts it
PlayerDataStoragethe .dat file, its .dat_old twin and its corrupt-copy rescuewhichever thread starts the task, then Server
PlayerChunkSenderhow many chunks a client is trusted with, starting at one unacknowledged batchServer
CommonListenerCookiethe four facts that survive a phase change: profile, latency, client options, transferred—

The phases this trace passes through — login, configuration, play, and the terminal packets between them — belong to protocol phases. This page starts where that one hands over: with PlayerList deciding whether the login is allowed at all.

Admission is a Component or nothing

Identity here is a NameAndId: a record of a UUID and a name, made from the authenticated GameProfile, and the key that the ban list, the whitelist, every op lookup and every save file are addressed by (the IP ban list keys on the address). UserNameToIdResolver, reached through MinecraftServer.services, is the cache that maps between the two halves — a CachedUserNameToIdResolver over usercache.json, with a GameProfileRepository behind it for the lookups the file cannot answer — and comparing its remembered name against the profile’s is how the server notices a player has been renamed since their last visit.

Four questions, and the two ways past them

PlayerList.canPlayerLogin returns a Component — the reason to refuse, rendered on the disconnect screen — or null for let them in. It asks four questions in order — the ban list, the whitelist, the IP ban list, then capacity — and the first three ask the same kind of object. A StoredUserList is a JSON file of StoredUserEntry records keyed by identity (the IP bans by address), rewritten whole on every change, and there are four of them: UserBanList, IpBanList, UserWhiteList and the ServerOpList that both surprises below turn on. The two ban lists share a BanListEntry carrying a source, a reason and an expiry, and nothing sweeps that expiry on a schedule: StoredUserList.get drops what has lapsed before it answers, but the login gate asks UserBanList.isBanned or IpBanList.isBanned first, which do not. So the first login after a temporary ban lapses finds the ban, fetches an entry that is no longer there, and throws; the server logs Internal server error and closes the connection, and the sweep that caused it lets the next attempt in. The two ways past the gate are not the ones a reader expects. The whitelist is bypassed by being an op: PlayerList.isWhiteListed is satisfied by presence in the op list, and DedicatedPlayerList.isWhiteListed overrides it to route through PlayerList.isOp, which also asks whether the identity is the singleplayer owner — a branch that never fires here, because DedicatedServer.isSingleplayerOwner returns false for everyone. The bypassesPlayerLimit flag in ops.json is a separate thing entirely and reaches exactly one of the four questions: of them, only the capacity test reads ServerOpListEntry.bypassesPlayerLimit, through DedicatedPlayerList.canBypassPlayerLimit. A banned op is still banned.

The gate runs twice, and disagrees with itself

The gate runs twice, and the second run is not a repeat of the first. ServerLoginPacketListenerImpl.verifyLoginAndFinishConnectionSetup runs it during login; ServerConfigurationPacketListenerImpl.handleConfigurationFinished runs it again when the client says configuration is over, because a ban or a newly full server can land in the seconds a configuration takes. Duplicate logins differ between the two. At login the newcomer wins: PlayerList.disconnectAllPlayersWithProfile kicks every player holding that UUID with PlayerList.DUPLICATE_LOGIN_DISCONNECT_MESSAGE, and the login parks until the old player has left the player list. At the second check the newcomer loses — an existing player with that id is a flat rejection, since by then there is a prepared spawn to throw away rather than a session to evict.

Preparing a place to stand

sequenceDiagram
    participant SLPL as ServerLoginPacket<br/>ListenerImpl
    participant PL as PlayerList
    participant SCPL as ServerConfiguration<br/>PacketListenerImpl
    participant PST as PrepareSpawnTask
    participant PDS as PlayerDataStorage
    participant SL as ServerLevel

    SLPL->>PL: canPlayerLogin, then disconnectAllPlayersWithProfile
    SLPL->>SCPL: startConfiguration, on a listener just built
    Note over SCPL: one task at a time, never two at once
    SCPL->>SCPL: Synchronize<br/>RegistriesTask<br/>first, then the<br/>code of conduct<br/>or a resource pack
    SCPL->>PST: start, appended by returnToWorld with a JoinWorldTask behind it
    PST->>PL: loadPlayerData, for the saved position
    PL->>PDS: load, the whole file read and datafixed
    PST->>SL: a PLAYER_SPAWN ticket at radius 3, through its chunk source
    rect rgba(0, 0, 0, 0.04)
        Note over SCPL,SL: every tick until the chunks land, the client still in configuration
        SL-->>PST: the load future completes, Preparing becomes Ready
    end
    SCPL->>SCPL: JoinWorldTask sends the finish packet
    SCPL->>PL: the duplicate check again, then canPlayerLogin
    SCPL->>PST: spawnPlayer

A join assembled while the client waits: the queue above the band runs one task at a time and never overlaps, and the band is where the waiting happens — a world being built for a client that has no idea.

Nothing in that queue overlaps. ServerConfigurationPacketListenerImpl.startNextTask throws rather than start a task while another is unfinished, so the registry transfer completes before PrepareSpawnTask reads a byte, and the two optional tasks sit between them. What does run alongside it is the chunk load and the client’s wait: once the ticket is placed the task simply reports not finished from ConfigurationTask.tick each tick, and the client spends that time in configuration with no idea a world is being assembled for it.

The ticket needs re-arming, but not yet. TicketType.PLAYER_SPAWN is registered with a timeout of twenty ticks and with TicketType.FLAG_LOADING as its only flag, so TicketType.canExpireIfUnloaded is false and the timeout does not begin while the chunks it asked for are still on their way — which is what lets ServerChunkCache.addTicketAndLoadWithRadius accept this type at all, since it throws for any type that could expire before it loads. From the moment the task reaches PrepareSpawnTask.Ready the clock does run, and PrepareSpawnTask.keepAlive — called from ServerConfigurationPacketListenerImpl.tick — re-adds the ticket at radius 3, the radius the figure shows, every tick. Without that, a client slow to acknowledge the finish packet would arrive to find its spawn chunks expired underneath it.

A player with no save file gets a search instead of a position, except on a world whose game mode is Adventure, where the world spawn is used without a search, only its height corrected. PlayerSpawnFinder.findSpawn walks up to PlayerSpawnFinder.ABSOLUTE_MAX_ATTEMPTS candidates — a thousand and twenty-four, or fewer if GameRules.RESPAWN_RADIUS or the world border says so — in a strided order from a random offset, loading each candidate’s chunk under a one-tick TicketType.SPAWN_SEARCH ticket and returning a future. The ChunkLoadCounter beside it feeds only the server’s LevelLoadListener, through LevelLoadListener.Stage.LOAD_PLAYER_CHUNKS — the client’s progress bar on an integrated server, and nothing a remote player ever sees on a dedicated one, where the listener is a LoggingLevelLoadListener that boot already closed.

The save file is read twice, and both reads are the whole file

PrepareSpawnTask.start reads the .dat and decodes ServerPlayer.SavedPosition from it — three optional fields, dimension, position and rotation, and nothing else. That is all it decodes. It is not all it does: PlayerDataStorage.load reads the compressed tag with an unlimited accounter and runs DataFixTypes.PLAYER over the entire document before any codec sees it (codecs, NBT and JSON). So the narrow read saves decoding, not I/O, and a join pays the migration cost twice — once here, and once in PrepareSpawnTask.Ready, where the file is loaded again for the full Entity.load.

Between the two reads, the player is built

ServerLevel.waitForEntities blocks the Server thread until the entities in the spawn chunks have finished loading, and then the ServerPlayer constructor runs, pulling its ServerStatsCounter and PlayerAdvancements out of PlayerList on the way past and applying the ClientInformation the client sent in configuration. The second read fills the object in, the player is snapped to the prepared position, PlayerList.placeNewPlayer runs, and only afterwards do ServerPlayer.loadAndSpawnEnderPearls and ServerPlayer.loadAndSpawnParentVehicle put back the ender pearls the player had in flight and what they were sitting on when they left, both re-created from the file.

Two rescues wired into the read

Two rescues are wired into the read, and the first is the file’s own. PlayerDataStorage.load tries <uuid>.dat; if that comes back with nothing — the file is missing, or NbtIo.readCompressed threw and logged Failed to load player data — it copies whatever is there aside as <uuid>corrupted<timestamp>.dat and then tries <uuid>.dat_old, the twin the previous write rotated out. So a corrupted file is kept — under the corrupted name, for whoever wants to look at it — and the player loses what changed since the save before last rather than everything; a player with no readable file and no twin is built from nothing, which is a new spawn rather than an error. Only what survives that is datafixed.

The second rescue is about identity, and only an integrated server has it. PlayerList.loadPlayerData checks whether the joining identity is the singleplayer owner and, if the world records a singleplayer_uuid in its level data, loads that file rather than the one named for the joining id. It works for one session: the save writes under the current id and MinecraftServer.saveAllChunks stamps the current owner’s id into level.dat, so the next time the world is opened the old file is orphaned.

PlayerList.placeNewPlayer sends a world in one write

sequenceDiagram
    participant PL as PlayerList
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SL as ServerLevel
    participant CM as ChunkMap
    participant Wire as the network

    Note over PL,Wire: in the packet drain at the top of a tick
    PL->>SGPL: suspendFlushing, on a listener already set to play
    PL->>SGPL: ClientboundLoginPacket, difficulty, abilities, held slot, recipes
    PL->>SGPL: the permission level as an entity event, then the command tree
    PL->>SGPL: recipe book, scoreboard, teleport, status
    PL->>SGPL: the tab list as it stands, everyone already here
    PL->>PL: the joiner is added to PlayerList.players
    PL->>SGPL: the joiner, to everyone, themselves included
    PL->>SGPL: the border, the clocks, the spawn, the rain, LEVEL_CHUNKS_LOAD_START, the tick rate
    PL->>SL: addNewPlayer
    SL->>CM: addEntity, then updatePlayerStatus and the first ChunkTrackingView
    CM->>SGPL: every ready chunk in that view marked pending on PlayerChunkSender
    PL->>SGPL: the boss bars, the effects, the inventory menu, then resumeFlushing
    SGPL->>Wire: one write
    Note over SL,Wire: later in the same tick, the first chunk batch, and only one

Everything PlayerList.placeNewPlayer sends the joiner, and the single write at the foot that carries it; the self-message in the middle is the step the tab-list order turns on.

The whole method sits inside one suspension, so everything above — a login packet, a command tree, a scoreboard, a tab list, a world border — leaves as a single write; what it tells the players already there goes out on their own connections, one packet at a time. It is the same ServerCommonPacketListenerImpl.suspendFlushing bracket the server tick puts around every client every tick, and the one place in the game it is opened by hand: a join is handled in the packet drain that runs before MinecraftServer.tickChildren opens the tick’s own.

ClientboundLoginPacket is where the client learns the entity id it is about to be given, the hardcore flag, every dimension key the server has, the view and simulation distances, whether the server authenticates, and a CommonPlayerSpawnInfo from ServerPlayer.createCommonSpawnInfo carrying the dimension type, the obfuscated seed, both game modes, the last death location, the portal cooldown and the sea level. Three game rules are pinned into it as plain booleans and never re-sent by this path: GameRules.REDUCED_DEBUG_INFO, GameRules.LIMITED_CRAFTING, and GameRules.IMMEDIATE_RESPAWN inverted into show the death screen.

The permission level arrives twice over, as a ClientboundEntityEventPacket carrying one of five event ids and as the whole command tree from Commands.sendCommands, both from PlayerList.sendPlayerPermissionLevel resolving a LevelBasedPermissionSet out of MinecraftServer.getProfilePermissions. Where such a set comes from is permissions; all a join needs to know is that both halves are re-sent whenever PlayerList.op or PlayerList.deop changes it.

The tab list goes out in a deliberate order, and it is the middle step that makes it one: the joiner is sent everyone already present, then added to PlayerList.players, then everyone — themselves included — is sent the joiner. Near the end of PlayerList.sendLevelInfo, with only the tick rate after it, comes ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, the client’s signal to stop waiting for the server and start waiting for its own chunk. And the chat join message, which goes to the players already there and not to the joiner, uses multiplayer.player.joined.renamed when the name in the profile differs from the one the name cache remembered, which is why that name is read at the top of the method, before the cache is overwritten.

Entering the level is the step that starts the terrain, though it is not the last: the boss bars, the active effects, the inventory menu and NotificationManager.playerJoined — which is not the chat message, sent before the player entered the level — all follow it, and ServerCommonPacketListenerImpl.resumeFlushing closes the single write. ServerLevel.addNewPlayer hands the player to PersistentEntitySectionManager.addNewEntity, whose callback adds it to ServerLevel.players and to the chunk source; ChunkMap.updatePlayerStatus registers the player with DistanceManager, resets its chunk tracking to ChunkTrackingView.EMPTY and computes the real one, and every chunk inside that view that is ready to send is marked pending on the connection’s PlayerChunkSender. So there are two player lists, written by different systems: PlayerList.players is who is on the server and PlayerList alone writes it, while ServerLevel.players is whose entity is in this level and only the entity manager’s tracking callbacks write it. A player halfway through a dimension change is in the first and in neither copy of the second.

A joining client is trusted with one batch, which is the only thing about the chunk-sending loop that is a fact about joining: PlayerChunkSender starts with a budget of a single unacknowledged batch, so the first batch a player ever receives is a hard round trip before the second can leave. The loop it then settles into — the client’s measured rate, the clamps and the nearest-first order — belongs to what the client is told, and which chunks are in the player’s set at all, to tickets and loading.

Loaded is something the client says

The player is in the world before the client can play. ServerGamePacketListenerImpl.hasClientLoaded gates movement, block breaking, item use, sprinting and sneaking, and it answers false until either the client sends ServerboundPlayerLoadedPacket or a sixty-tick timer runs out — ServerGamePacketListenerImpl.CLIENT_LOADED_TIMEOUT_TIME, started in the listener’s constructor and counted down from ServerPlayer.tick, which is to say from the level’s entity loop rather than from the connection.

Death re-arms the same gate by a different mechanism. ServerGamePacketListenerImpl.markClientUnloadedAfterDeath, called at the end of ServerPlayer.die, sets a flag that no timer clears: a dead player’s client counts as unloaded indefinitely, and only ServerGamePacketListenerImpl.restartClientLoadTimerAfterRespawn — reached from the respawn, or from a brand-new listener — clears the flag and starts the sixty ticks again. The death screen is held open by the same gate as the Joining world screen, through a different field.

That the countdown ticks from ServerPlayer.tick while the food, health and stat sync tick from ServerPlayer.doTick is the whole reason a player is ticked from two places every tick; the two-phase tick is the lecture on that. The fact worth carrying out of here is that the level’s entity loop ticks players regardless of entity-ticking range.

Four ways the session changes

The join is one story; what happens afterwards is a comparison. Three of these a player will meet tonight and the fourth exists for one debug command, and they disagree about almost everything.

respawndimension changedisconnectServerGamePacketListenerImpl.switchToConfig
who runs itServerGamePacketListenerImpl.handleClientCommandServerPlayer.teleportConnection.handleDisconnectionDebugConfigCommand
same ServerPlayer?no — PlayerList.respawn constructs a new oneyesyes, brieflyyes, then a new one on the way back
same entity id?yes, copied over by Entity.setIdyes—no, the rebuilt player gets a fresh one
same connection listener?yes, ServerPlayer.connection is reassignedyes—no, a new one from PlayerList.placeNewPlayer
what carries overwhatever ServerPlayer.restoreFrom copieseverything, because nothing is copied—whatever the .dat holds
is the .dat written?nonoyes, inside PlayerList.removeyes, the same call
the client is toldClientboundRespawnPacket, keeping nothing, or attribute modifiersClientboundRespawnPacket, ClientboundRespawnPacket.KEEP_ALL_DATAnothing, the channel is already goneClientboundStartConfigurationPacket
removal reasonEntity.RemovalReason.KILLEDEntity.RemovalReason.CHANGED_DIMENSIONEntity.RemovalReason.UNLOADED_WITH_PLAYERthe same

The object, and the reference that outlives it

PlayerList.respawn builds the new ServerPlayer from the old one’s profile and client information, then performs three assignments that keep the difference out of every tab list: ServerPlayer.connection is set to the old player’s listener, Entity.setId copies the entity id, and the listener’s own player field is reassigned by its caller. To every other client’s tab list nothing has happened — same id, same UUID, same row — though entity tracking removes the old body and adds the new one, and a hardcore respawn sends the row’s new game mode. To anything inside the server holding the old object, everything has happened: it was removed from its level before the new one existed, and it is not in PlayerList.players any more.

Only two of the four paths take a TeleportTransition, and they are the two that move you. ServerPlayer.findRespawnPositionAndUseSpawnBlock turns the saved ServerPlayer.RespawnConfig — respawn data plus a forced flag — into one: a respawn anchor is found and a charge spent unless the point is forced, a bed is found and costs nothing, TeleportTransition.missingRespawnBlock says the block is gone, and TeleportTransition.createDefault falls back to the world spawn. Both of those last two are the expensive branch, not just the fallback: each builds its position with TeleportTransition.findAdjustedSharedSpawnPos, so each runs ServerPlayer.adjustSpawnLocation — the same PlayerSpawnFinder search a join runs asynchronously — and blocks the Server thread on the future with BlockableEventLoop.managedBlock.

What comes across when you die

ServerPlayer.restoreFrom has two branches, and the interesting one is not the one people name it for. Its restore everything branch — permanent attribute modifiers, health, hunger, every active effect, the inventory, the portal state — is reached only when PlayerList.respawn is called with Entity.RemovalReason.CHANGED_DIMENSION, which happens in exactly one place: a player leaving the end credits, with ServerPlayer.wonGame set. That is the End-portal return, not keepInventory.

Which makes the end credits a fifth way out of a ServerLevel, and the reason the table above has four columns rather than five: ServerPlayer.showEndCredits removes the player with Entity.RemovalReason.CHANGED_DIMENSION like a dimension change, and the client, when the credits end or are closed, sends the respawn that hands it to PlayerList.respawn like a death. It is not a way a session changes so much as the one place two of them meet.

An ordinary death takes the other branch. Health is reset to maximum, effects are gone, and GameRules.KEEP_INVENTORY — or having died as a spectator — decides only whether ServerPlayer.restoreFrom runs ServerPlayer.transferInventoryXpAndScore, which moves the inventory, the experience and the score and nothing else. What the same rule decided on the way out, when the old player died, belongs to damage and death. Outside the branch, and so true of every death, a long tail is copied unconditionally: the ender chest, the enchantment seed, both game modes, base attribute values, the recipe book, the warden spawn tracker, the chat session, the skin customisation, the last death location. The ender chest surviving death is not a game rule; it is a field assignment.

Why the Nether keeps your potion effects

Because nothing is copied. ServerPlayer.teleport across a dimension boundary sets ServerPlayer.isChangingDimension, sends the client a ClientboundRespawnPacket marked ClientboundRespawnPacket.KEEP_ALL_DATA, removes the entity from the old level with Entity.RemovalReason.CHANGED_DIMENSION — one of the two reasons that neither destroy an entity nor write it into a chunk (entity lifecycle has the five) — immediately calls Entity.unsetRemoved, points it at the new level with ServerPlayer.setServerLevel, and adds it back with ServerLevel.addDuringTeleport. It is one object the whole way. The effects survive because they were never touched, and what the server then re-sends — level info, the inventory through PlayerList.sendAllPlayerInfo, the active effects, the abilities, the permission level — is not a restoration but a re-sync, because the client threw its own world away the moment the respawn packet arrived.

A same-dimension teleport is not this path at all. It is ServerGamePacketListenerImpl.teleport followed by ServerGamePacketListenerImpl.resetPosition, with no client-side world discarded and no respawn packet sent.

Where your llama goes when you log out

The channel closes on a Netty thread and nothing happens until ServerConnectionListener.tick, in the connection step of the server tick, notices and calls Connection.handleDisconnection (the connection is the channel’s own account of dying). That reaches ServerGamePacketListenerImpl.removePlayerFromWorld: the leave message, ServerPlayer.disconnect to eject passengers and stop sleeping, and PlayerList.remove.

The order inside PlayerList.remove is the answer to the question. The .dat is written first, with the stats and advancements JSON beside it, and ServerPlayer.saveParentVehicle writes the entire root vehicle into it under RootVehicle — but only if that vehicle has exactly one player passenger. The same test then decides whether the vehicle chain is removed from the world with Entity.RemovalReason.UNLOADED_WITH_PLAYER — the other of those two non-destroying reasons, which is exactly what you want for something already written into a different file. In-flight ender pearls go the same way, saved into the file and removed from the world. So the llama leaves the world with you, rides in your save, and is put back by ServerPlayer.loadAndSpawnParentVehicle after your next PlayerList.placeNewPlayer — unless somebody else was riding it too, in which case it stays where it is and you land beside it. On an integrated server one further thing happens: ServerCommonPacketListenerImpl.onDisconnect sees that the leaver is the singleplayer owner and halts the server (how a server dies, which owns PlayerList.saveAll and PlayerList.removeAll at shutdown).

ServerGamePacketListenerImpl.switchToConfig is that same removal with the connection left alive. It runs ServerGamePacketListenerImpl.removePlayerFromWorld — leave message, saved file, tab-list removal and all — which is the half that belongs here: as far as this page’s four ways are concerned, a reconfigure is a leave that keeps the socket. What the connection then does with the phase, and why no registry is re-sent on the way back, belong to protocol phases. CommonListenerCookie carries the client’s options and the transferred flag across that return, as it does across every phase change.

What everyone else is told

Very little, and nearly all of it on the way out. A respawn and a dimension change send other clients nothing for the tab list, a hardcore respawn’s game mode aside, because the identity a tab list is keyed on never changed. A disconnect sends one ClientboundPlayerInfoRemovePacket to everybody, and otherwise a tab list hears a row’s game mode, hat or chat session when one changes and the latency sweep PlayerList.tick broadcasts on its own slow counter (the server tick). The rest of what other players see is entity tracking in ChunkMap, and the removal reasons in the table above have already told it what to do.

The three kicks that come from the tick

ServerGamePacketListenerImpl.tick ends sessions of its own accord. An idle player is disconnected after MinecraftServer.playerIdleTimeout minutes with no action, unless they are sitting on the end credits. A player whose client reports itself airborne for too long is kicked for flying, on a budget that stretches as gravity falls and against a list of exemptions the movement page owns (input to movement). And keep-alive disconnects a client that has stopped answering — a mechanism that belongs to the connection rather than to the session, because it runs in the configuration phase too (the connection).

The asymmetry is what this section is about. The singleplayer owner is exempt from the keep-alive, the one kick of the three that could reach it: the keep-alive alone asks ServerCommonPacketListenerImpl.isSingleplayerOwner, and the host exists only on an integrated server, which allows flight and sets no idle timeout, so nobody there is kicked for either of the others.

Where to look

PlayerList.canPlayerLogin · PlayerList.placeNewPlayer · PlayerList.respawn · PlayerList.remove · PlayerList.loadPlayerData · PlayerList.sendLevelInfo · PrepareSpawnTask · JoinWorldTask · PlayerSpawnFinder · PlayerDataStorage · ServerPlayer.restoreFrom · ServerPlayer.teleport · ServerPlayer.findRespawnPositionAndUseSpawnBlock · ServerPlayer.die · ServerPlayer.doTick · ServerGamePacketListenerImpl.removePlayerFromWorld · ServerGamePacketListenerImpl.switchToConfig · ServerGamePacketListenerImpl.hasClientLoaded · ServerCommonPacketListenerImpl · PlayerChunkSender · ChunkMap.updatePlayerStatus · TeleportTransition


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Starting a server

Verified against Minecraft 26.3 · Part III · Dropping the jar in an empty folder, typing java -jar server.jar, and waiting for the line that says Done.

The first run unpacks the libraries the jar carries, writes two files and exits: you have not agreed to the EULA. The second gets as far as Preparing level “world” and, a second or two later, Done (1.284s)! For help, type “help”. By the time the second of those lines prints, the server has opened a data pack stack, built every registry the world is made of, taken an operating-system lock on the save directory, rewritten level.dat, constructed a ServerLevel for every dimension the packs declare and started listening on 25565 — and only the levels were built between the two lines. Every other item on that list was over before the first one printed, which is why the elapsed time Done reports is so short. It is not a claim about the whole boot: query, RCON, the watchdog and JMX are all started after that line, and the last of the server’s own start-up work happens later still. And the step in the middle whose name promises the most — MinecraftServer.prepareLevels, the one whose progress line still reads Preparing spawn area — does the least: on an ordinary world it loads no chunks at all. All it does is re-arm the chunk-loading tickets the last shutdown wrote down (tickets and loading has the nine kinds), and exactly two of the nine are ever written down.

The cast

classwhat it decidesthread
server/Maineverything decided before the Server thread exists: the flags, the properties, the EULA, which directory is the world, and when to hand offServerMain, which the jar’s bundler starts
DedicatedServerSettingsthe live copy of server.properties — and the rewrite that happens every time a setting changesServerMain, then Server
LevelStorageSource.LevelStorageAccessone open world: its directory layout, its session.lock, and every read and write of level.dat while the world is openwhoever holds it
WorldLoaderthe packs, the registries and the datapack-driven resources, assembled into a WorldStemServerMain and Util.backgroundExecutor
DedicatedServerwhat a dedicated server has and singleplayer does not: the console, the port, the legacy conversions, RCON, query, the watchdogServer, but constructed on ServerMain
MinecraftServerthe levels, the saves and the loop — and MinecraftServer.spin, the line where the Server thread beginsServer
ServerLevelone dimension: its chunk source, its saved data, its ticket storeServer
LevelLoadListenerwhat boot progress looks like: log lines on a dedicated server, a progress bar on a clientthe caller’s

From the command line to the Server thread

sequenceDiagram
    participant Main as Main
    participant LSA as LevelStorageSource.<br/>LevelStorageAccess
    participant WL as WorldLoader
    participant Worker as Worker
    participant DS as DedicatedServer

    Main->>Main: the flags, Bootstrap.bootStrap, server.properties, the EULA gate
    Main->>Main: JsonRpc.create, if the management server is enabled at all
    Main->>LSA: validateAndCreateAccess, taking session.lock in the constructor
    Main->>LSA: getUnfixedDataTagWithFallback, if there is a world: level.dat, or level.dat_old restored
    Main->>WL: load, wrapped in Util.blockUntilDone, so this thread is an executor
    WL->>Main: the pack-opening stage, the reload's apply steps and the final assembly, single-threaded
    WL->>Worker: static tags, world then dimension registries, the resources
    Worker-->>WL: the background stages, finished
    WL-->>Main: a WorldStem
    Main->>LSA: saveDataTag rewrites level.dat now, upgrade or no upgrade
    Main->>DS: MinecraftServer.spin runs the factory here, on this thread
    Note over Main,DS: the thread starts only now, and main returns

The first half of the boot, and the wall at the foot of it: one thread drives every line above that note, with the worker pool’s help, and the server object does not exist for most of them.

Read the note bar as a wall. Above it one thread drives the work, handing the load’s background stages to the worker pool; below it main has returned, and everything that remains is the Server thread and the daemons arranged around it — the half drawn under The Server thread wakes up below. Anatomy draws the same hand-off from the client’s side, where the thread that spins the server is the one drawing frames.

Everything main does before there is a second thread

Main.main does not run on the thread the JVM started with: the jar opens with a bundler, which unpacks the libraries and the game’s own jar into the working directory and starts Main.main from that copy on a thread named ServerMain, and that thread stays with it for all of what follows. SharedConstants.tryDetectVersion reads version.json out of the jar first, so everything downstream knows what version it is. Then the flags are parsed — –nogui, –port, –universe, –world, –forceUpgrade, –recreateRegionFiles, –safeMode, –initSettings, –pidFile and the rest — and CrashReport.preload runs: MemoryReserve sets a block of heap aside and one throwaway report is formatted and discarded, so that the crash-report path is warm and has memory of its own even when the reason for the crash is that there is none left. Bootstrap.bootStrap and Bootstrap.validate build and freeze the static registries (identifiers and registries), and Util.startTimerHackThread starts a daemon that sleeps forever and touches nothing. The last thing Bootstrap.bootStrap does is quieter and matters later: it puts a LoggedPrintStream over System.out and System.err — a DebugLoggedPrintStream when debug logging is on — so that anything printed from here on goes through the logger, and the original stream survives only as Bootstrap.STDOUT. That is the stream a crash report uses when the logger cannot be trusted (how a server dies).

Only then does the server read its own configuration. DedicatedServerSettings parses server.properties, and DedicatedServerSettings.forceSave writes it straight back — which is why a properties file carried over from an older jar comes back with the new keys filled in. RegionFileVersion.configure takes the region compression out of it before any chunk file is ever opened (chunk storage). And Eula reads eula.txt, or writes the default and reports it missing. That is the gate: Eula.hasAgreedToEULA false means one log line and main returns. No world has been opened, no server exists, and everything main has started so far is a daemon — which does not hold the JVM open, so the process ends with the return. –initSettings returns one step earlier still, having written both files on purpose.

The management port opens before the world does

With the EULA agreed, Services.create builds the authentication services and the name cache in the universe directory, and JsonRpc.create starts a ManagementServer if management-server-enabled is set — and throws, ending the boot, if the secret it finds is not forty alphanumeric characters. A missing secret is not that failure: DedicatedServerProperties resolves management-server-secret to a freshly generated key when the file has none, and the properties rewrite above puts that key in the file — so what stops a boot is a malformed secret, not an absent one. TLS stops it too: it is on by default and wants a keystore, which by default is not configured, so enabling the management server and nothing else ends the boot. What starts is a Netty WebSocket listener with its own event-loop group named Management server IO, TLS on by default, and a JsonRpcNotificationService registered on the NotificationManager that everything later in the boot reports through. It binds before session.lock is taken, and DedicatedServer.onServerExit stops it last, so the management protocol is reachable at both ends of the server’s life — including while there is no world to ask it about (how a server dies).

Taking the lock, and fixing level.dat twice

LevelStorageSource.createDefault points at the universe directory — the working directory unless –universe says otherwise — and LevelStorageSource.validateAndCreateAccess opens the world named by –world or by level-name. The validation is a symlink check; the lock is neither optional nor deferred. DirectoryLock.create, called from the LevelStorageSource.LevelStorageAccess constructor, opens DirectoryLock.LOCK_FILE, writes a snowman into it and takes a FileChannel.tryLock on it. That is an operating-system advisory lock, not a value in the file: a second server on the same directory fails at once with DirectoryLock.LockException, a crashed JVM releases it with nothing to clean up, and a world folder copied out from under a running server carries a session.lock that means nothing. The client’s world list greys out a world that DirectoryLock.isLocked reports held.

If the directory already holds world data, main parses level.dat once and runs the result through the datafixers twice. LevelStorageSource.LevelStorageAccess.getUnfixedDataTagWithFallback reads the file and, on a parse failure, falls back to level.dat_old and restores it over the original. That raw tag goes through LevelStorageSource.LevelStorageAccess.fixAndGetSummaryFromTag for a LevelSummary, which is enough to answer LevelSummary.requiresManualConversion and LevelSummary.isCompatible — two gates that each end the boot with one explanatory line — and separately through DataFixers.getFileFixer for the fully upgraded tag the world is built from.

The world load turns the main thread into an executor

ServerPacksSource.createPackRepository builds the repository over the world’s datapacks/ folder and WorldLoader.load does the rest: the staged load that the resource system describes, run here for server data. WorldLoader.PackConfig.createResourceManager selects and opens the packs, TagLoader.loadTagsForExistingRegistries collects tags for the static registries, RegistryDataLoader loads RegistryDataLoader.WORLD_REGISTRIES and then RegistryDataLoader.DIMENSION_REGISTRIES, the WorldLoader.WorldDataSupplier turns the fixed level.dat into a PrimaryLevelData through LevelStorageSource.getLevelDataAndDimensions — or, with no world data at all, Main.createNewWorldData builds one out of server.properties. Last, ReloadableServerResources.loadResources loads the rest: RegistryDataLoader.RELOADABLE_REGISTRIES, the loot tables, recipes and advancements among them, through ReloadableServerRegistries.reload, and then the functions. What comes back is a WorldStem.

The threading is the part worth noticing. WorldLoader.load takes two executors: Util.backgroundExecutor for the work, and a main-thread executor for the stages that must be single-threaded. Util.blockUntilDone supplies the second by handing the loader a queue’s add method and then draining that queue until the future completes. For the length of the world load ServerMain is an event loop, running the pack-opening stage, the reload’s apply steps and the final assembly itself between bouts of waiting on the workers.

Two things then happen before the server object exists. With –forceUpgrade or –recreateRegionFiles, a WorldUpgrader rewrites the chunks that need it (an older version’s, those holding a cache –eraseCache clears, or every chunk with –recreateRegionFiles) while main polls it once a second and logs a percentage. And either way LevelStorageSource.LevelStorageAccess.saveDataTag writes level.dat back out, by the same replace every later save uses (level data and rules). That is unconditional: a server started and killed one second later has already rewritten its world data.

MinecraftServer.spin, and the last thing main does

MinecraftServer.spin takes a factory rather than a server, and its order is the point. It builds the Thread object first, sets priority 8 on a machine with more than four processors, calls the factory on the calling thread, and only then starts the thread — so the whole of the DedicatedServer constructor runs on ServerMain, and the new thread cannot begin before there is a server for it to run.

That constructor is real work. MinecraftServer’s own opens the server-wide SavedDataStorage, takes the WorldData and WorldGenSettings out of the stem (creating a world is where the stem was built, by a screen or by server.properties), builds the ServerConnectionListener, the PlayerDataStorage, the GameRules, the StructureTemplateManager and the PacketProcessor, finalises recipe loading, and refuses outright a stem whose LevelStem registry has no overworld. DedicatedServer’s adds the ServerTextFilter, the ServerLinks built from the bug-report property, and — when enable-code-of-conduct is set — every .txt file under the codeofconduct folder, which throws if that folder is missing. A misconfigured code of conduct kills the server on ServerMain, before a Server thread exists to be killed.

Back in main, the factory has already applied –port, –demo and –serverId and, unless –nogui or a headless JVM says otherwise, opened the Swing window through DedicatedServer.showGui. A Server Shutdown Thread hook goes on the runtime — its whole body is one MinecraftServer.halt call — and main returns.

The Server thread wakes up, and can still fail twice

sequenceDiagram
    participant DS as DedicatedServer
    participant MS as MinecraftServer
    participant SL as ServerLevel

    Note over DS,SL: the other side of the wall: the Server thread
    MS->>DS: runServer calls initServer
    DS->>DS: the console daemon thread, the properties into fields, the key pair
    DS->>DS: startTcpServerListener binds the port, then convertOldUsers
    DS->>MS: loadLevel, which is createLevels, forceDifficulty and prepareLevels
    MS->>SL: the overworld first, then one level per LevelStem on DerivedLevelData
    MS->>SL: the persisted tickets re-armed, then a wait in 10 ms slices
    SL-->>MS: on an ordinary world, nothing pending: nothing was asked for
    MS-->>DS: loadLevel returns
    DS->>DS: Done is logged here, before the loop is ever entered
    DS->>DS: the optional listeners, each if its property is set
    DS-->>MS: initServer returns true
    MS->>MS: the icon and the status response, then the tick loop

The second half: the play port is bound two messages before the levels exist, and the word Done is logged three messages before the loop it claims is running.

MinecraftServer.runServer is the Server thread’s body and its first act is DedicatedServer.initServer, which begins with the console: a daemon thread named Server console handler reading System.in line by line. It runs nothing itself. Each line becomes a ConsoleInput — the text plus a CommandSourceStack built there on the console thread — appended to a synchronized list that DedicatedServer.handleConsoleInputs drains from DedicatedServer.tickConnection, so a typed command executes inside a tick, in its connection phase.

Then the properties become fields: online mode, the local IP, the default game type, the port. MinecraftServer.initializeKeyPair generates the RSA pair that login encryption uses (protocol phases), and ServerConnectionListener.startTcpServerListener binds the port with a Netty server bootstrap. Two things can end the boot at this point, and they end it identically.

the failurewhat the console saysthe check
the port is already takenFAILED TO BIND TO PORT! and the exceptionServerConnectionListener.startTcpServerListener throws, and DedicatedServer.initServer returns false
a legacy user list survived conversionFAILED TO START THE SERVER AFTER ACCOUNT CONVERSION! and the files to delete by handOldUsersConverter.areOldUserlistsRemoved looks for banned-players.txt, banned-ips.txt, ops.txt and white-list.txt, and is false if any of the four is still there

The conversion itself is DedicatedServer.convertOldUsers, which attempts five migrations — the two ban lists, the op list, the whitelist and the player save files — retrying a failed one up to twice more, five seconds apart, and reporting true unless all five failed; a missing legacy file counts as a success, so an ordinary server reports true on every boot. The gate is the separate check above, so what stops the boot is the file nobody could convert. Either failure returns false out of DedicatedServer.initServer, and a false there is not a quiet exit: MinecraftServer.runServer calls it inside its own try and throws an IllegalStateException on false, so the boot failure lands in the loop’s own catch, writes a crash report and falls into the same finally that /stop reaches — a server that failed to bind still walks the whole shutdown path and releases session.lock on the way out (how a server dies).

Past them, a DedicatedPlayerList is built, offline names are resolved in the cache, and the log says Preparing level “world”.

Building the levels

MinecraftServer.loadLevel is three calls: MinecraftServer.createLevels, MinecraftServer.forceDifficulty — empty in the base class, and overridden only by DedicatedServer.forceDifficulty, which pushes server.properties’ difficulty onto the world on every boot — and MinecraftServer.prepareLevels.

MinecraftServer.createLevels builds the overworld first and by name, and it is the only level that is special. It gets the custom spawners — the PhantomSpawner, the PatrolSpawner, the CatSpawner, the VillageSiege and the WanderingTraderSpawner — and its ServerLevelData is the real PrimaryLevelData out of level.dat. With it in MinecraftServer.levels (a LinkedHashMap, so it stays first for every later walk over the levels), the scoreboard, the CommandStorage and the Stopwatches come out of the server-wide saved data. Every other LevelStem in the registry then gets a ServerLevel over a DerivedLevelData, which forwards the overworld’s game time, difficulty, game mode and hardcore flag and swallows every setter but the spawn. That is why the difficulty is one number every dimension shares. The other world-wide numbers a reader would expect to find here are not its doing: the time of day belongs to ServerClockManager, the weather to one server-wide WeatherData, and the spawn a level reports to MinecraftServer.effectiveRespawnData (level data and rules).

A brand-new world takes one detour, as soon as the overworld exists and before the other dimensions do, and it is the detour that actually generates terrain at boot. When ServerLevelData.isInitialized is false, MinecraftServer.setInitialSpawn asks the generator for its origin chunk (ChunkGenerator.getOrigin), reads the generator’s spawn height, and walks a spiral over the chunks five in each direction — the value MinecraftServer.SPAWN_POSITION_SEARCH_RADIUS names — calling PlayerSpawnFinder.getSpawnPosInChunk until one of them offers a standable block. The bonus chest is placed here if –bonusChest asked for one. Then the flag is set, and no later boot of that world looks for a spawn again.

Preparing the levels, which prepares nothing

MinecraftServer.prepareLevels replays a list. For each level a ChunkLoadCounter records which chunks are already ChunkStatus.FULL, calls TicketStorage.activateAllDeactivatedTickets, runs the distance manager again and counts what is new. The tickets it replays are the persisting ones the last shutdown parked rather than dropped, and two of the nine TicketTypes carry TicketType.FLAG_PERSIST — TicketType.FORCED, from /forceload, and TicketType.PORTAL — so those two are the only entries a chunk_tickets file contains (tickets and loading has the nine). The world spawn is kept by nothing.

Zero chunks are loaded by MinecraftServer.prepareLevels on a world with no forceloads and no live portal ticket.

What follows is MinecraftServer.waitUntilNextTick with the deadline set MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS — 10 ms — out, repeated while ChunkLoadCounter.pendingChunks is above zero: the Server thread pumping its own queue in slices, the same way a tick waits for a chunk (the server tick). With a total of zero it runs once and leaves. MinecraftServer.updateMobSpawningFlags and the effective respawn data are recomputed, and the step is over.

What the console prints while nothing is prepared

The output is worth reading literally. LoggingLevelLoadListener logs Loading N persistent chunks… when LevelLoadListener.Stage.LOAD_INITIAL_CHUNKS starts and Time elapsed: N ms when it finishes, with Preparing spawn area: N% — still the menu.preparingSpawn string, its percentage computed by LevelLoadProgressTracker — printed at most twice a second in between. On an ordinary world there is no in between: N is zero, and the percentage line prints at most once, on the loop’s single pass. Of the four values LevelLoadListener.Stage declares, LevelLoadListener.Stage.PREPARE_GLOBAL_SPAWN fires only on a world’s first boot, LevelLoadListener.Stage.LOAD_PLAYER_CHUNKS belongs to a player joining rather than to boot — PrepareSpawnTask, in the configuration phase (players and sessions) — and LevelLoadListener.Stage.START_SERVER is declared and fired by nothing.

Done comes before the loop

DedicatedServer.initServer logs Done (1.284s)! For help, type “help” the moment MinecraftServer.loadLevel returns — and only then starts the optional listeners. QueryThreadGs4.create runs if enable-query, RconThread.create if enable-rcon, a Server Watchdog thread if DedicatedServer.getMaxTickLength is above zero, JMX if enable-jmx-monitoring. After those come one MinecraftServer.saveEverything with flush and force and NotificationManager.serverStarted on the JSON-RPC feed. DedicatedServer.initServer returns true, MinecraftServer.runServer loads server-icon.png — the server directory’s, or failing that the world’s, and it must be 64 by 64 — builds the first ServerStatus, and enters the tick loop.

So Done is a claim about the world being ready rather than about the server being reachable in every sense. The play port has been open since before the level loaded, and RCON is still not listening when the line prints. MinecraftServer.isReady — read by the singleplayer client’s loading screen and by the JSON-RPC status method — is set at the bottom of the loop’s first iteration, one tick later still.

The threads startup leaves behind

Boot creates every thread on this list and then hands the process to one of them; the column that matters here is daemon, because it decides which of them can hold the JVM open. What each may touch is the thread reference’s, which has the complete set including the pools and the situational ones (threads).

threadmade bydaemonwhat it may touch
Server threadMinecraftServer.spin, from mainnoeverything — it is the only thread allowed to change the world
Server console handlerDedicatedServer.initServer, first statementyesSystem.in and the ConsoleInput list. The command itself runs on the Server thread
Server WatchdogDedicatedServer.initServer, only if DedicatedServer.getMaxTickLength is above zeroyesreads MinecraftServer.getNextTickTime, the game rules and ServerLevel.getWatchdogStats off-thread, mid-tick
RCON Listener, plus one RCON Client per connectionRconThread.create, after DonenoTCP accept. DedicatedServer.runCommand hops the command onto the Server thread with BlockableEventLoop.executeBlocking
Query ListenerQueryThreadGs4.create, after Donenoa UDP status protocol, read-only
Management server IOJsonRpc.create, back in mainyesthe JSON-RPC socket. ManagementServer.tick runs from DedicatedServer.tickServer
Timer hack threadUtil.startTimerHackThread, in mainyesnothing. It sleeps, and nothing wakes it

Two rows carry a consequence for the other end of the story. RconThread and QueryThreadGs4 are the only non-daemon threads in this table besides the Server thread itself — Util.ioPool’s IO-Worker threads, made outside it and squarely in the boot path, are non-daemon too — so boot is where the process acquires the three things that will later have to be stopped by hand rather than abandoned, and what that costs at the other end belongs to how a server dies. Why those two poll, and the conditions that can leave RCON unstarted with enable-rcon set, are per-thread facts and live with the rest of them (threads).

One more thing outlives boot without being a thread: server.properties stays live. Nineteen DedicatedServerProperties fields are Settings.MutableValues, and Settings.MutableValue.update rebuilds the whole properties object and DedicatedServerSettings.update writes the file back, so /whitelist, the spawn-protection setter and the JSON-RPC settings calls all edit server.properties on disk while the server runs. /difficulty does not: it changes the world, and DedicatedServer.forceDifficulty puts the file’s difficulty back at the next boot.

Singleplayer boots the same server with a shorter list

IntegratedServer uses the same MinecraftServer.spin, MinecraftServer.runServer and MinecraftServer.loadLevel, and almost nothing else on this page. IntegratedServer.initServer turns authentication on, generates the key pair, calls MinecraftServer.loadLevel, sets the MOTD from the host’s name and the level name, saves once and returns true: no console thread, no TCP bind, no legacy conversion, no RCON, no query, no watchdog and no Done line. Its player list is built in the constructor rather than in DedicatedServer.initServer, and its LevelLoadListener is a LevelLoadTracker composed with the logging one, so the client’s progress bar and a dedicated server’s log lines are two readings of the same callbacks. The whole of it is constructed from Minecraft.doWorldLoad on the Render thread, by a caller that goes back to drawing frames rather than returning from main — anatomy is that hand-off, and its diagram is the one to read for how the two loops meet.

For a 1.21-era reader. There is no spawnChunkRadius game rule: GameRuleRegistryFix deletes it out of any save that still carries one, and the chunks around the world spawn are kept alive by whoever stands in them and by nothing else. spawn-protection in server.properties survives and is unrelated — it is a permission check in DedicatedServer.isUnderSpawnProtection, not a loader.

Where to look

In boot order. server/Main · Bootstrap.bootStrap · Eula · DedicatedServerSettings · JsonRpc.create · LevelStorageSource.validateAndCreateAccess · LevelStorageSource.LevelStorageAccess · DirectoryLock.create · WorldLoader.load · Util.blockUntilDone · WorldStem · MinecraftServer.spin · MinecraftServer.runServer · DedicatedServer.initServer · MinecraftServer.loadLevel · MinecraftServer.createLevels · MinecraftServer.prepareLevels · LevelLoadListener · IntegratedServer.initServer


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

How a server dies

Verified against Minecraft 26.3 · Part III · /stop typed at the console, an exception out of the tick loop, and a tick that never ends — three endings, and what each writes of your world to disk.

An admin types /stop. The command sets one boolean and returns, and the lap already in progress carries on to its end. Everything a player would call shutting down — the players written, the unloads drained, level.dat rotated, session.lock released — happens afterwards, inside the finally of the loop that just exited, on the same Server thread that was ticking mobs a moment ago. Which is what makes the second ending strange, and worth a lecture: a crash in the tick saves your world and the watchdog does not. An exception out of the tick loop lands in that same finally, so a server that dies of a bad block entity writes exactly what /stop writes. ServerWatchdog instead calls System.exit, which runs the Server Shutdown Thread hook, which calls MinecraftServer.halt with wait true and waits for the Server thread to finish — the very thread wedged in the tick that tripped the watchdog. That wait never returns. Ten seconds later the watchdog’s own scheduled Runtime.halt ends the JVM with no save run.

Three endings, side by side

/stopa crash in the ticka watchdog kill
what clears MinecraftServer.runningMinecraftServer.halt, called by StopCommand with wait false and by five other callers belownothing: the loop is left by the throw, not by the conditionthe shutdown hook, eventually — System.exit runs it and it calls MinecraftServer.halt with wait true
does MinecraftServer.runServer’s finally runyes, on the Server threadyes, on the Server thread, after the crash reportno: the Server thread never leaves the tick
are players savedyes, PlayerList.saveAll then PlayerList.removeAllyes, identicallyno
are chunks savedyes: the unload drain, then MinecraftServer.saveAllChunks with flushyes, identicallyno — only what an autosave, an unload or the eager sweep had already written
is level.dat writtenyes, LevelStorageSource.LevelStorageAccess.saveDataTagyes, identicallyno
is session.lock releasedyes, LevelStorageSource.LevelStorageAccess.close drops the DirectoryLockyes, identicallynot by the game — the OS drops it when the process dies
is a crash report writtennoyes, into crash-reports/, from MinecraftServer.constructOrExtractCrashReportyes, into crash-reports/, from ServerWatchdog.createWatchdogCrashReport, before the exit
what ends the JVMnothing explicit: the Server thread returns and no non-daemon thread is leftthe sameRuntime.halt from the watchdog’s own timer, ten seconds after its System.exit

The first two columns differ in three of the eight rows: what clears the flag, when the finally runs relative to the crash report, and whether there is a crash report at all. The third differs from the first in all eight, and the rest of this page is why.

The cast

classwhat it decidesthread
MinecraftServerthe two shutdown flags, the tick loop, and the finally that is the whole of shutdownServer
StopCommandthat /stop is one call to MinecraftServer.halt with wait false, at Commands.LEVEL_OWNERSServer
DedicatedServerwhat wraps the base teardown: the JSON-RPC notification, Util.shutdownExecutors, and the side threads in DedicatedServer.onServerExitServer
ServerWatchdogthat a tick past max-tick-time is a dead server, and that the JVM goes with itServer Watchdog, a daemon
PlayerListthat every player is written before anyone is disconnectedServer
ServerChunkCache · ChunkMapwhen the world is quiet enough to stop draining, and what a flush save meansServer, with the writes on the IO pool
LevelStorageSource.LevelStorageAccesslevel.dat and the DirectoryLock on session.lockServer
Utilthe process-wide pools, and the three-second grace each of the two it shuts down getsany

/stop, in full

sequenceDiagram
    participant MS as MinecraftServer
    participant PL as PlayerList
    participant SL as ServerLevel
    participant SCC as ServerChunkCache
    participant LSA as LevelStorageSource.<br/>LevelStorageAccess
    participant Disk

    Note over MS: StopCommand calls halt with wait false, so running goes false
    Note over MS: the lap in progress finishes, then the loop condition fails
    MS->>MS: stopped goes true, then stopServer, out of runServer's finally
    MS->>MS: PacketProcessor.close, then the connection listener is stopped
    MS->>PL: saveAll, then removeAll
    PL->>Disk: each player's dat file, stats and advancements
    MS->>SL: noSave cleared on every level
    loop while any ChunkMap.hasWork
        MS->>MS: the tick deadline pushed one millisecond out
        MS->>SCC: deactivateTicketsOnClosing, then tick, on every level
        MS->>MS: waitUntilNextTick drains the queue for what is left of it
    end
    rect rgba(0, 0, 0, 0.04)
        Note over MS,Disk: one call: MinecraftServer.saveAllChunks with flush
        MS->>SL: save, per level, reaching ChunkMap.saveAllChunks
        SL->>Disk: region files, entities, poi, and the chunk_tickets saved data
        MS->>LSA: saveDataTag, level.dat built into a temp file
        LSA->>Disk: the temp replaces level.dat, the old rotated to level.dat_old
        MS->>MS: SavedDataStorage.<br/>saveAndJoin, after level.dat and not before
    end
    MS->>SL: close, ServerChunkCache.close then the entity manager
    MS->>LSA: close, releasing the DirectoryLock on session.lock
    MS->>MS: Util.shutdownExecutors, then onServerExit stops RCON and query
    Note over MS: the Server thread returns, and no non-daemon thread is left

The whole of shutdown, which is one finally block. The shaded band is a single call, and the three saves inside it are in that order because that call puts them there — not because MinecraftServer.stopServer asks for them one at a time.

The command is a flag

StopCommand registers one literal at Commands.LEVEL_OWNERS, sends commands.stop.stopping and calls MinecraftServer.halt with wait false. That call assigns MinecraftServer.running — volatile, and the loop’s only condition — and returns. Nothing else happens on that line of the console: the rest of the lap runs out — a console command runs in the connection phase, after every level has ticked, and a player’s between ticks — and the loop condition at the top of MinecraftServer.runServer fails on the next pass. Five other places on this side of the jar call the same method: the server GUI’s window-close listener and Main’s shutdown hook, both with wait true, so that they block until the Server thread has finished; the JSON-RPC management API’s MinecraftServerStateServiceImpl, which takes the flag from its caller; ServerCommonPacketListenerImpl.onDisconnect, which stops a singleplayer server when its host logs out; and GameTestServer, which halts itself when its test run is over. The client adds three more of its own.

Teardown itself is the loop’s finally. MinecraftServer.runServer sets MinecraftServer.stopped, the plain field other threads read through MinecraftServer.isStopped, and calls MinecraftServer.stopServer, then calls MinecraftServer.onServerExit from a nested finally, so that a teardown which throws still stops the side threads. DedicatedServer.stopServer wraps the base with NotificationManager.serverShuttingDown before and Util.shutdownExecutors after.

The front door closes, the guests do not leave

PacketProcessor.close is first, before anything is even logged. Afterwards PacketProcessor.scheduleIfPossible refuses a packet a Netty thread has just decoded, and PacketProcessor.processQueuedPackets returns without draining — so packets already in the queue go the same way as the ones still arriving. Then ServerConnectionListener.stop closes the channels it bound, and only those: closing a Netty parent channel does not close the connections accepted through it. Live sessions are severed after the players are saved, by PlayerList.removeAll, with the multiplayer.disconnect.server_shutdown reason. A connection still in handshake, login or configuration has no ServerPlayer and is in neither list, so it is closed by neither, and simply dies with the process (protocol phases: a session exists only once configuration is acknowledged).

MinecraftServer.stopped also changes how work is accepted, and the two doors answer differently. MinecraftServer.executeIfPossible throws a RejectedExecutionException outright. MinecraftServer.scheduleExecutables merely reports false, so a caller reaching BlockableEventLoop.execute from another thread runs its task inline on its own thread rather than queueing it for a loop that has stopped looping (the server tick is the same two doors while the server runs).

Players before chunks, and never MinecraftServer.saveEverything

Shutdown does not use the save entry point everything else uses. MinecraftServer.saveEverything — autosave, /save-all, the integrated server’s pause, the JSON-RPC save call — is players then chunks in one call. MinecraftServer.stopServer does the two halves by hand: PlayerList.saveAll (each player’s data through PlayerDataStorage, plus their ServerStatsCounter and PlayerAdvancements), then PlayerList.removeAll, and only much later MinecraftServer.saveAllChunks. PlayerList.removeAll is thinner than its name: it disconnects each connection and nothing else, so the tickets those players hold are not what goes with them. What lets the next step finish is ServerChunkCache.deactivateTicketsOnClosing, called on every level inside the drain loop itself.

ServerLevel.noSave is then cleared on every level. /save-off does not survive /stop.

The drain

ChunkMap.hasWork is the question, and it is a broad one — nine things in one or: pending light, pending unloads, a non-empty updating map, POI work, chunks queued to drop, a non-empty unload queue, the worldgen and light dispatchers, and DistanceManager.hasTickets, the reason the loop terminates at all. While any level answers yes, the server pushes the tick deadline one millisecond out, calls ServerChunkCache.deactivateTicketsOnClosing and ServerChunkCache.tick on each level, and runs MinecraftServer.waitUntilNextTick, which drains the main-thread queue and polls each level’s chunk executor for what is left of that millisecond.

One millisecond — each slice of the drain, so unloads and their saves proceed while the main-thread queue keeps taking chunk results.

TicketStorage.deactivateTicketsOnClosing moves every ticket except TicketType.UNKNOWN into a parked map — the flush save below writes the persistent ones out as the dimension’s chunk_tickets saved data, under TicketStorage.TYPE — and that is what ends the loop: TicketStorage.hasTickets counts only the live map, so parking a ticket stops it holding a chunk. Parked is not forgotten — which of them survive to the next boot, and what re-arms them there, belong to tickets and loading and starting a server.

The flush save

MinecraftServer.saveAllChunks with flush true is the real save. The scoreboard is pushed into its saved data, then each ServerLevel.save: ServerLevel.saveLevelData joins the level’s own SavedDataStorage, and ServerChunkCache.save runs the distance manager once more before ChunkMap.saveAllChunks in flush mode. That last one is a loop, not a pass — every holder that ChunkHolder.wasAccessibleSinceLastSave, waited on with BlockableEventLoop.managedBlock until ChunkHolder.isReadyForSaving, repeated until a whole round saves nothing new — and then SectionStorage.flushAll for the POI sections, the unloads processed, and SimpleRegionStorage.synchronize joined so that the IOWorker has actually put the bytes down (chunk storage). Entities follow, through the level’s PersistentEntitySectionManager.

level.dat is written next, the same way at every save, flush or not — LevelStorageSource.LevelStorageAccess.saveDataTag, a gzipped temp file through NbtIo.writeCompressed, and a replace that keeps the previous copy (level data and rules has the path and the retries). Only after that does the server-wide SavedDataStorage get its SavedDataStorage.saveAndJoin. There are two tiers of saved data, and they are flushed at opposite ends of this section.

The closes, and the last thread

ServerLevel.close is ServerChunkCache.close — which saves once more, then closes the level’s saved data, the ThreadedLevelLightEngine and ChunkMap — followed by the entity manager. Then the server’s own SavedDataStorage (whose SavedDataStorage.close is itself a final SavedDataStorage.saveAndJoin), the MinecraftServer.ReloadableResources, and last LevelStorageSource.LevelStorageAccess.close, which releases the DirectoryLock. From that moment the world is openable by anything else.

Util.shutdownExecutors then stops Util.backgroundExecutor and Util.ioPool with a three-second grace each, inside DedicatedServer.stopServer and so before DedicatedServer.onServerExit — nothing may need a worker after that point. Util.nonCriticalIoPool is untouched, and survives only because its threads are daemons. DedicatedServer.onServerExit closes the text filter and the GUI and stops RconThread, QueryThreadGs4 and the ManagementServer. There is no System.exit anywhere on this path, and none is needed. Every other thread the server started is a daemon — the console reader, the Netty groups, the management server’s group, the watchdog — except the RCON and query threads, which GenericThread.stop joins here in one-second slices, and the IO pool’s workers, which went a step earlier with Util.shutdownExecutors, and the chat filter’s pool, whose workers the text filter’s close shuts down, and, with the GUI up, Swing’s event dispatch thread, which ends once MinecraftServerGui.close disposes the frame. So when MinecraftServer.runServer returns, the Server thread is the last one left, and the JVM ends because there is nothing to keep it (the thread reference).

The crash that saves

MinecraftServer.runServer wraps the entire loop, DedicatedServer.initServer included. Anything thrown out of a tick — a block entity, a mob’s AI — is logged, turned into a report, saved, and then falls into the same finally. A packet handler’s exception and a command’s are caught before they get here; what escapes is an Error, or, from a packet handler, a ReportedException carrying an OutOfMemoryError.

MinecraftServer.constructOrExtractCrashReport walks the cause chain and keeps the innermost ReportedException it finds, using that exception’s own report and noting the outer throwable under a Wrapped in category. A throwable with no ReportedException anywhere in it becomes a fresh report titled Exception in server tick loop. Either way MinecraftServer.fillSystemReport fills it in — the value of MinecraftServer.running, the player count and roster, the selected and available data packs, the enabled feature flags, the world-generation lifecycle, the world seed, and the contents of the server’s SuppressedExceptionCollector, which has been quietly watching every chunk load failure, chunk save failure and packet-handler exception since boot — keeping the latest eight (the message, the class and the place, without the stack) and a count of every one by place and class. DedicatedServer.fillServerSystemReport adds two lines, the modded status and the words Dedicated Server. The file lands in crash-reports/ under MinecraftServer.getServerDirectory, named by Util.getFilenameFormattedDateTime. MinecraftServer.onServerCrash is a hook the dedicated server does not override.

A crash on another thread arrives here too. BlockableEventLoop keeps one static parked report. Util.onThreadException — the uncaught-exception handler on every Util.backgroundExecutor and IO-pool thread — and GenerationChunkHolder, when a generation step completes exceptionally, both call BlockableEventLoop.relayDelayCrash, which parks the report or suppresses the new one under a report already parked. The next BlockableEventLoop.pollTask on a loop constructed with crash propagation throws it as a ReportedException, and the dedicated server is constructed that way. So a worker that dies does not die silently: it dies as a tick-loop crash, on the Server thread, at whatever moment that thread next looks for a task (the server tick). It does not save like one. The parked report is never cleared, so the shutdown’s own drain throws it again the first time it waits for chunks: the players are written, and so are the chunks the drain’s first pass unloads, and then the teardown stops before the flush save, leaving the entities, level.dat and the saved data unwritten, the executors never shut down — the IO pool’s workers keep the JVM alive until they idle out — and the lock for the operating system to drop. The integrated server is constructed with propagation off and hands its report to the client instead, through IntegratedServer.onServerCrash.

The watchdog that does not

sequenceDiagram
    participant SW as ServerWatchdog
    participant MS as MinecraftServer
    participant JVM
    participant Hook as Server Shutdown Thread
    participant Disk

    Note over MS: wedged inside one tick, past max-tick-time
    SW->>MS: getNextTickTime, a deadline now far in the past
    SW->>SW: createWatchdog<br/>CrashReport<br/>dumps every thread
    SW->>MS: fillSystemReport, read off-thread while the tick still runs
    SW->>Disk: the report, to stdout and to crash-reports
    SW->>JVM: Runtime.halt armed for ten seconds from now
    SW->>JVM: System.exit
    JVM->>Hook: run the shutdown hooks
    Hook->>MS: halt with wait true, running becomes false
    Hook->>MS: then waits for the Server thread, which is the wedged one
    MS-->>Hook: nothing, because the tick never returns
    Note over SW,Disk: ten seconds later the armed halt fires, with no save run

The only ending that waits on a thread that will never finish: the exit waits for the hook, the hook for the wedged thread, and the armed halt is the only thing that moves. The one message to the Disk lane is the difference between this and a server that dies silently.

ServerWatchdog is a daemon thread started by DedicatedServer.initServer whenever DedicatedServer.getMaxTickLength is positive — that is DedicatedServerProperties.maxTickTime, max-tick-time, default sixty thousand milliseconds, and setting it to zero or less means the thread is never created and there is no backstop at all. Its loop is short: while MinecraftServer.isRunning, compare Util.getNanos with MinecraftServer.getNextTickTime, then sleep exactly until the earliest moment a violation could be true.

What it compares matters. MinecraftServer.getNextTickTime is the deadline the loop set for the tick it is running, not a timestamp of when that tick began, and the tick loop advances it before each tick and again when catching up after an overload. The watchdog fires when the server is that far past where it promised to be.

The report comes first, and it is the good part of the design. ServerWatchdog.createWatchdogCrashReport dumps every thread in the JVM, sorts them daemon-last, appends the lot as a Thread Dump category, and grafts the Server thread’s stack trace onto a synthetic error — so the report’s headline stack is the code that hung. MinecraftServer.fillSystemReport adds the usual, and the watchdog then adds a Performance stats category holding the random-tick game rule and ServerLevel.getWatchdogStats for every level: players, the five commonest entity types and ticking block-entity types, block and fluid tick counts, chunk source stats. All of it is read from another thread with no lock on the world, off a world that is mid-tick — which is exactly the trade the class makes, because the alternative is asking a wedged thread for the answer. It goes to real stdout through Bootstrap.realStdoutPrintln and to crash-reports/ like any other report.

Then the wait that cannot end. The watchdog schedules Runtime.halt on a timer and calls System.exit, which runs the registered shutdown hooks — including the “Server Shutdown Thread” that Main registered at boot, whose whole body is MinecraftServer.halt with wait true. That sets MinecraftServer.running false, which the wedged tick will never read, and then waits for the Server thread to end. It does not end. System.exit will not return until its hooks do, so the JVM sits there until the watchdog’s timer fires.

Ten seconds — from the watchdog’s System.exit to its Runtime.halt (ServerWatchdog.MAX_SHUTDOWN_TIME), and the Server thread writes nothing in any of them; only writes already queued on Util.ioPool can still land.

The watchdog is a liveness backstop, and reading it as a safe stop gets the guarantee backwards. Whether anything guards shutdown depends on how shutdown was reached, because the watchdog loops while MinecraftServer.running is true and only MinecraftServer.halt ever clears that flag. After /stop it is cleared before the drain begins, so a server stuck on “Saving chunks” is stuck with no watchdog left watching it. After a crash nothing clears it — the crash path never calls MinecraftServer.halt — so the watchdog is still counting. The drain loop survives that by resetting the deadline every pass; the flush save that follows it does not reset anything, so a slow enough save after a crash can be shot by the watchdog mid-write.

The endings that are /stop under another name

Ctrl-C, SIGTERM and the window button

Ctrl-C at the console and a SIGTERM from a service manager are the same thing as far as the game is concerned: the JVM runs its shutdown hooks, and the one Main registered calls MinecraftServer.halt with wait true. The contrast with the watchdog is in the health of the thread being waited for, and in the halt nothing here has armed. Here the Server thread is fine, notices the cleared flag at the top of its next lap, and runs the entire /stop teardown while the hook thread waits. A Ctrl-C on a healthy server is a /stop, and the JVM does not exit until the world is on disk. The server GUI’s window-close button does the same thing from the AWT thread.

Singleplayer ends on a poll

Minecraft.disconnect — reached from Save and Quit, from a disconnect, and from Minecraft.emergencySave — closes the client’s connection, then calls IntegratedServer.halt with wait false. That override first uses BlockableEventLoop.executeBlocking to remove every player who is not the host, then clears MinecraftServer.running and stops the LAN pinger. The client then puts up a GenericMessageScreen reading Gui.SAVING_LEVEL and calls Minecraft.renderFrame in a loop while MinecraftServer.isShutdown is false. The “Saving world” screen is not a progress bar and is not driven by the server: it is a render loop spinning on one question, is the Server thread dead yet (the client loop). Closing the game window reaches the same place by a different road: Window.shouldClose makes Minecraft.runTick call Minecraft.stop, which ends the frame loop, and Main then calls Minecraft.exitWorldAndClose on its way out. The client’s “Client Shutdown Thread” is a JVM shutdown hook rather than that path — the backstop for a kill signal — and after an ordinary exit it finds Minecraft.singleplayerServer already null.

MinecraftServer.isShutdown is the right question to spin on, and it is the one fact in this teardown that is not a field: it asks whether the Server thread is still alive, so nothing can set it and nothing can lie about it. MinecraftServer.isStopped, which the screen could have watched instead, goes true at the start of teardown, when nothing has been saved yet — a “Saving world” screen driven by that one would come down before the world was written.

The integrated server never calls Util.shutdownExecutors. IntegratedServer.stopServer tears down published state and defers to the base. The client owns those pools and shuts them down at the very end of its own life, long after the world is closed.

What you lose if you kill the process

What an autosave has already written

Ordinary autosave is MinecraftServer.saveEverything with neither flush nor force, on the countdown the tick keeps at the bottom of every lap (the server tick) — five minutes of the server’s scheduled time, whatever the tick rate. It writes every player, every dirty chunk — under none of the per-chunk spacing that throttles ordinary saving, because the sweep clears it on the way in (chunk storage) — and, unconditionally, level.dat, followed by a scheduled write of the server’s SavedData. Between the two, the spawn point and the world time (in level.dat) and the weather, the game rules and the world clocks (each its own SavedData file — weather, game_rules, world_clocks) on disk are never more than one autosave stale, even on a server nobody ever stops cleanly. Everything else — a chest filled a moment ago, a mob that walked into a new chunk, an inventory change — lives in the LevelChunk, the entity sections and the player until something saves them; a changed chunk goes out with the eager sweep once the tick has time and ten seconds have passed since its last write (chunk storage).

The honest answer, per ending

After /stop or a crash thrown by the tick itself, nothing is lost: the drain, the flush save and the joined IOWorker mean the process does not end until the bytes are down. The crash has one asterisk the clean stop does not — the watchdog is still armed all the way through it — but short of a save slow enough to trip it, both endings land the same. After a watchdog kill, or a kill -9, or a power cut, you lose everything no autosave, unload, eager sweep or logout had written, plus anything still queued inside the IOWorker — those writes run on Util.ioPool, and Runtime.halt does not wait for a pool.

What you never lose is access to the world. The lock the boot took is an operating-system one (starting a server), and an operating system drops it when the process dies however it dies — so a world left behind by a killed server opens on the next start.

What you lose with no ending at all

Individual failures are quieter than any of this. A chunk that cannot be written calls MinecraftServer.reportChunkSaveFailure: logged, added to the SuppressedExceptionCollector that the next crash report will print, written out as its own ReportType.CHUNK_IO_ERROR file under debug/, and followed by a disk-space check. The tick does not stop, the server does not stop, and the only signs at the time are a line in the log and that file, with a toast as well in singleplayer.

Where to look

The /stop road first, then the other two. StopCommand · MinecraftServer.halt · MinecraftServer.runServer (the loop and its finally) · MinecraftServer.stopServer · DedicatedServer.stopServer · ChunkMap.hasWork · MinecraftServer.saveAllChunks · LevelStorageSource.LevelStorageAccess.close · DedicatedServer.onServerExit · MinecraftServer.constructOrExtractCrashReport · BlockableEventLoop.relayDelayCrash · ServerWatchdog.run · Main (the shutdown hook) · IntegratedServer.halt · Minecraft.disconnect


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

IV · The world

Verified against Minecraft 26.3 · Part IV · The machinery that turns a place you have walked to into a place that exists: chunks asked for, made, lit, saved and forgotten, and the five pages off that line — what the place and the hour decide, and the four systems that make the world the line delivers feel alive.

Part III was one thread going round. This part is what it goes round on. A world is too big to hold, so the server holds a moving window of it, and almost everything in this part exists to decide the window’s edge: which chunks are worth building, how far past the edge to build them, which of them tick, which of them you are owed a copy of, and when one is finally written down and let go. A player recognises the part by its edge — the chunks that appear at the rim of render distance as you walk, the mobs that stop moving when you fly away from them, the Saving world bar. Almost none of that edge is one number: render distance, simulation distance and the mob-spawning radius are three different radii, answered by three different mechanisms, and only two of them are settings (two graphs, one store).

The shape of the part

Part IV is a conveyor with a vocabulary page in front of it — and the conveyor is a ring, because a chunk written out comes back in through the door it left by. One page defines the thing being handed; four more are the ring, and a chunk goes round it in order. The other five are not on the line at all — one is what the place and the hour decide, and four act on the chunk the ring delivers. Every box below is one of those ten lectures except the one in lower case, which is a chunk’s state rather than a page: the live LevelChunk the ring exists to produce.

flowchart TD
    CA["2 · Chunk anatomy: sections, palettes, heightmaps"]
    TL["3 · Tickets and loading: which chunk, at what level"]
    GP["4 · The generation pipeline: EMPTY to FULL"]
    LI["5 · Lighting: two 4-bit fields, off the tick"]
    CS["6 · Chunk storage: copy, encode, sectors"]
    LC["a live LevelChunk in a ticking world"]
    EA["1 · Environment attributes: the place and the hour"]
    ST["7 · Scheduled ticks: the appointment book"]
    FL["8 · Fluids: the book's biggest customer"]
    GV["9 · Game events and vibrations: what just happened"]
    PI["10 · Points of interest: what is worth going to"]
    CA -- "the vocabulary the ring spends" --> TL
    TL -- "a holder, a ceiling, three futures" --> GP
    GP -- "two of its ten statuses" --> LI
    LI -- "sections dirtied, one packet of them" --> LC
    LC -- "nobody needs it any more" --> CS
    CS -- "a ticket wants it back" --> TL
    LC -- "how anything happens later" --> ST
    ST -- "one customer, its own page" --> FL
    LC -- "what just happened, and who hears it" --> GV
    LC -- "what is worth walking to" --> PI
    EA -- "a value for a position and an instant" --> LC

The part numbered to the watch order: four pages and the live chunk make the ring, and five arrows run round it, the one into lighting an inclusion rather than a hand-off — two of the pipeline’s own ten statuses, run on a different executor. Chunk anatomy hands the ring its vocabulary, lecture one hands the live chunk its values, and the last four act on the live chunk, one of them through another page.

Follow the numbers rather than the arrows and the surprise is where they disagree: the first lecture is watched first and sits off the line, and the sixth hands back to the third.

Later parts hang off that line rather than feeding it: Part V’s blocks assume the sections and palettes of chunk anatomy, Part VI’s entities tick where the tickets say, and Part XII’s terrain is the cargo on the generation pipeline.

Before you start

The server tick and the level tick. Almost everything here happens on the Server thread inside that loop or on the worker pool beside it — the IO lane that chunk storage is about has threads of its own — and the level tick is where the chunk source is asked to do its five things. Part II’s codecs and registries are assumed wherever a chunk is written to disk or a type is looked up by name, and tags wherever a behaviour is defined by a set the data pack owns — which is most of what the last two pages do.

Nothing in this part needs Part V or beyond.

Watch in this order

Lectures two to six are the chain — nothing later in it can be watched first. The first is off the chain on purpose. Of the last four, fluids assumes scheduled ticks and must follow it; the other three can be watched in any order once you have the vocabulary page.

  1. Environment attributes and timelines — off the conveyor, ahead of it, and the page Part III already asked you to watch. Whether lava flows fast, what colour the sky is and when a villager goes to work are one mechanism. The night does not set the sky’s colour — it multiplies whatever the biome produced.
  2. Chunk anatomy — what a chunk is made of, down to the bit storage. The vocabulary the rest of the part spends. A section holding two distinct block states costs exactly what one holding sixteen costs.
  3. Tickets and loading — a player takes one step east and, at view distance 10, a column twenty-one chunks wide is asked for. Nothing asks for a chunk because it is loaded: it asks for a level, and two graphs reading one ticket store answer different questions about it.
  4. The chunk generation pipeline — one chunk from EMPTY to FULL through ten statuses and a pyramid of neighbour requirements. Asking for one chunk asks for 529.
  5. Lighting — a torch is placed. Two 4-bit fields flooded on a worker and published as a copy. There is no light thread, and no light phase of the tick.
  6. Chunk storage — a chunk nobody needs is copied, encoded and written, on three different lanes, and the unload waits for neither the encode nor the write. Almost every write of your world is one nobody asked for.
  7. Scheduled ticks — how anything happens later: an appointment book of two queues per chunk, and a dedup rule that quietly drops the second appointment even when it is sooner.
  8. Fluids — a bucket of water on flat stone. Water turns toward a hole four steps past the block it is about to fill, because every side runs its own search — and a side the water is not allowed to replace still votes on where the rest of it goes.
  9. Game events and vibrations — a footstep reaches a sculk sensor, through a cascade of tests that is most of the lecture. The sensor never acts on a footstep in the game tick that stamped it, which costs a mob a tick and a player nothing.
  10. Points of interest — a villager claims a bed from 48 blocks away, the moment a path to it exists. Sleeping in it sets the bed’s occupied flag and tells the index nothing, and the only behaviour that turns that flag into a change in the index can only take a claim away.

Where the part stops

4% of the part’s lines are named on no page in the book, and what it leaves over is mostly two small libraries of shapes the pages above teach: world/level/blockscan, a block search that asks each section’s palette first, as chunk anatomy describes, and the modifier classes an attribute’s arithmetic uses, the library environment attributes describes rather than enumerates.

Two omissions are deliberate. The world border has no lecture — a per-dimension SavedData answering a question about a position, with no chunk to follow and one step of the level tick to move its edge — so it sits on level data and rules. And sending a chunk stops at eligibility: the packet and its pacing are Part IX’s.

Reference this part uses

Level data and rules — who owns the seed, the spawn, the rules, the border and the dimensions, and which file remembers each; it is also where the border itself is explained, for the reason just given. Game rules — five of which this part reads. Math and primitives — ChunkPos and SectionPos, and the packings the conveyor pages assume. Block update flags — the flag word three pages here spend. Registries — most of this part’s vocabularies are registries: chunk statuses, ticket types, fluids, game events, point-of-interest types, environment attributes, timelines and world clocks. Threads — the worker pool and the IO lane. Diagram lanes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Environment attributes and timelines

Verified against Minecraft 26.3 · Part IV · dusk falls over a taiga, and one value is resolved through a stack of layers — on the server for a mob, and again on the client for the sky.

Dusk on the overworld clock is a stretch and not an instant. Between tick 11867 and tick 13670 the sky over a taiga slides from its pale blue towards black, and the sky over a pale garden slides from its grey towards black by the same proportion; part-way through, at tick 12542, every mob that burns in daylight stops being in danger of burning, and is not in danger again until tick 23460. A player would never connect the three, and in 26.3 they are one mechanism. An environment attribute is a named, typed, registered property of the world — EnvironmentAttributes puts 51 of them in BuiltInRegistries.ENVIRONMENT_ATTRIBUTE — and the world answers one for a position and an instant by running a short stack of layers over the attribute’s default value: the dimension, the biome, the timelines, the weather. The surprise is in what a timeline holds. The day timeline does not know what colour a taiga sky is and never learns: its sky track’s keyframes are not values but modifier arguments, and the night segment is a multiply. Night does not set the sky’s colour — it multiplies whatever the biome produced, which is how one data-driven curve darkens every overworld biome correctly without being told about any of them.

The cast

Eight classes carry a value from the data pack to the sky. The first two thirds of this page is the machinery, object by object; the last third runs dusk through it twice, once on each side.

classwhat it decidesthread
EnvironmentAttributethe key: a type, a default, an AttributeRange, and four flags — positional, spatially interpolated, syncable, and the full-resolution biomes that EnvironmentAttributes.NATURAL_MOB_SPAWNS sets. It holds no value and no state— (registry constant)
EnvironmentAttributeMapwhat one dimension or one biome contributes — a modifier and an argument per attribute, never a bare value— (loaded from data)
EnvironmentAttributeSystemthe baked per-level resolver: one EnvironmentAttributeSystem.ValueSampler for each attribute some layer mentionsbuilt in the level constructor, read on that level’s thread; a generating region builds its own, on the worker
Timelinea clock, an optional period, one AttributeTrack per attribute, and the named instants on that clock— (loaded from data)
AttributeTrackSamplerone track baked against a clock, with a one-tick cache of the sampled argumentServer or Render
ServerClockManagerthe owner of day time — one ServerClockManager.ServerClockInstance per registered WorldClock, saved as world_clocksServer
ClientClockManagerthe client’s copy: free-runs each clock forward between packetsRender
EnvironmentAttributeProbethe client’s smoothing layer, living on Camera: 216 biome samples a tick, a lerp every frameRender

The stack a value falls through

flowchart TD
    DEF["EnvironmentAttribute.defaultValue enters here"]
    DIM["1 — dimension: DimensionType.attributes, a Constant folded into the baked base"]
    BIO["2 — biome: one Positional layer per attribute some biome mentions"]
    TLS["3 — timelines: DimensionType.timelines, one EnvironmentAttributeLayer.TimeBased per track"]
    WEA["4 — weather: WeatherAttributes.addBuiltinLayers, only where Level.canHaveWeather"]
    ASK{"who built this stack"}
    FLASH["two client-only layers, one attribute each"]
    SAN["EnvironmentAttribute.sanitizeValue clamps to the AttributeRange"]
    DEF --> DIM
    DIM --> BIO
    BIO --> TLS
    TLS --> WEA
    WEA --> ASK
    ASK -- "the server" --> SAN
    ASK -- "the client" --> FLASH
    FLASH --> SAN

The four rungs a level’s value falls through, and the one branch that is not on every value’s path: weather is the last word on the server, and the client stacks two more layers above it, each bolted to one attribute — one lerps the sky colour toward the lightning flash, the other pins the sky light factor to 1. A client asking for anything else falls the same four rungs a server does, and a region being generated builds only the first two.

EnvironmentAttributeSystem.Builder.addDefaultLayers stacks those four in that order and only that order. There is no priority number anywhere and no ordering data: a biome cannot run before its dimension, and weather is the last word on the server. The client’s two extra layers are not a fifth rung: ClientLevel adds each with EnvironmentAttributeSystem.Builder.addTimeBasedLayer against one named attribute — EnvironmentAttributes.SKY_COLOR and EnvironmentAttributes.SKY_LIGHT_FACTOR — so they are two entries in two attributes’ stacks and are absent from the other forty-nine. Each is a no-op unless ClientLevel.getSkyFlashTime is above zero. The stack is per attribute, too — an attribute nothing in the level mentions has no EnvironmentAttributeSystem.ValueSampler at all, and EnvironmentAttributeSystem.getValue hands back its default. And it is baked once: the level’s whole stack is built in the ServerLevel and ClientLevel constructors and never rebuilt, the only writer being ServerLevel.setEnvironmentAttributes, which is deprecated, marked for testing and called only from TestEnvironmentDefinition. A data-pack reload does not touch it.

Each rung earns its shape. The dimension’s is an EnvironmentAttributeLayer.Constant, and EnvironmentAttributeSystem.bakeLayerSampler walks off the front of the list folding every leading constant into one baked base value, so a dimension costs nothing at read time. The biome’s is an EnvironmentAttributeLayer.Positional, added once per attribute that any biome in the whole registry mentions — in vanilla, thirteen attributes across sixty-seven biome files, with visual/sky_color in fifty-seven of them. Timeline layers are EnvironmentAttributeLayer.TimeBased, one per track, and so is weather: one for each of the nine attributes named by WeatherAttributes.RAIN or WeatherAttributes.THUNDER, blending rain in first and thunder second. Rain is weighted at Level.getRainLevel minus the thunder level, so a thunderstorm never counts twice. Level.canHaveWeather wants sky light, no ceiling and not the End, so a rain-free dimension does not carry a weather layer that does nothing: it carries none.

ClientLevel adds two more after those four, both keyed on the lightning flash that LightningBolt sets through Level.setSkyFlashTime. One lerps EnvironmentAttributes.SKY_COLOR a fixed 22% toward a pale blue-white, the other pins EnvironmentAttributes.SKY_LIGHT_FACTOR to 1 outright, and both read the flash through the accessibility option Hide Sky Flashes, which reports a flash time of zero while it is switched on — so the two layers are still there and simply never fire. (The End’s sky flash is a different thing entirely — EndFlashState, read by the renderers rather than through the stack; see lightmap, fog and sky.)

An attribute is positional unless its builder says otherwise — positional meaning its answer is allowed to differ from block to block — and that flag is what polices who may write it. Biome.getAttributes is read through EnvironmentAttributeMap.CODEC_ONLY_POSITIONAL, so naming a non-positional attribute in a biome file is a load error, and no biome can locally change sky light level or lava speed. WorldGenRegion.environmentAttributes shuts the door from the other side: a region being generated builds a stack of the two static rungs alone, dimension and biome, so a feature or a spawn that asks about the environment during generation gets the place and never the hour, because generation must not depend on the hour.

Arguments, not values

EnvironmentAttributeMap is not a map of values. It is a map of EnvironmentAttributeMap.Entry, and an entry is an argument plus an AttributeModifier. EnvironmentAttributeMap.Builder.set is sugar for EnvironmentAttributeMap.Builder.modify with AttributeModifier.override; the interesting entries multiply, blend, maximise or or into whatever the layer below produced. AttributeTrack is the same shape — a modifier plus a KeyframeTrack of arguments — which is why Timeline.Builder.addTrack is only the override case and Timeline.Builder.addModifierTrack is the general one, and why the day timeline can say multiply sky light by 0.267 at night rather than sky light is 4 at night. In a data pack the shorthand shows: an entry written as the value itself means override, even for an attribute whose value is an object, and any other entry is an object carrying a modifier and an argument.

What a type allows

AttributeType is a record of a value codec, an AttributeType.modifierLibrary of the operations legal on that type and a codec for them, four separate LerpFunctions, one for each way two values of it can meet, and two converters.

lerp slotused when
AttributeType.keyframeLerpbetween two keyframes of a timeline track that overrides the value — a track of modifier arguments uses the modifier’s own AttributeModifier.argumentKeyframeLerp instead
AttributeType.stateChangeLerpfading weather in and out
AttributeType.spatialLerpacross a biome boundary
AttributeType.partialTickLerpbetween two client ticks, inside a frame

AttributeTypes registers fifteen of them: two boolean kinds, three numeric, two colour, six a data pack sets whole — a moon phase, a villager activity, a bed rule, a particle, a piece of background music, the ambient sounds — and two that combine, the ambient particles, a list that cross-fades, and the mob-spawn settings, which overlay. One built by AttributeType.ofNotInterpolated gets a step function in all four slots, each with its own threshold, which is how a MoonPhase snaps while a colour slides; AttributeType.toFloat and AttributeType.toInt are nullable, and their presence decides whether a loot table can read an attribute as a number, which the trace below reaches. Each type publishes a small, closed library of operations — six logic gates for a boolean, six arithmetic ones for a float, five for a colour, four of them ways to combine two colours — and a track or an entry that asks for one the type does not publish fails where it is built, AttributeType.checkAllowedModifier in the game’s own builders and the type’s modifier codec for a data pack, so an illegal combination is a load error rather than a runtime surprise. Three codecs then decide who may write what:

codecused byeffect
EnvironmentAttributeMap.CODECDimensionType.attributesanything
EnvironmentAttributeMap.CODEC_ONLY_POSITIONALBiome.getAttributesrejects non-positional attributes
EnvironmentAttributeMap.NETWORK_CODECDimensionType.NETWORK_CODEC, Biome.NETWORK_CODECdrops every non-syncable entry before the wire

Who owns the clock

WorldClock is a unit record. It holds nothing at all: it is an identity token in the Registries.WORLD_CLOCK registry, and vanilla registers two, WorldClocks.OVERWORLD and WorldClocks.THE_END. Every piece of state lives in ServerClockManager.ServerClockInstance — a total tick count, a fractional partial tick, a rate and a paused flag — and the manager holding those is a SavedData under ServerClockManager.TYPE, saved once for the whole server as world_clocks.

The four numbers, and the two records that copy them

The live instance is mutable, so its numbers exist twice more as records. ClockState is the saved form and carries all four; PackedClockStates is the map of them a save file holds. ClockNetworkState is the wire form and carries three — total ticks, partial tick and rate — and the one it leaves behind, the paused flag, is the whole of what the client does not get. ClockManager is the one thing the two managers share, an interface with a single method, ClockManager.getInstance: this clock’s ClockInstance — its total tick count, partial tick, rate and pause. Everything a reader of an attribute needs from a clock is behind that method, which is why AttributeTrackSampler can be the same class on both sides, and why ServerClockManager is the owner of day time in this book.

What moves a clock

MinecraftServer calls ServerClockManager.tick once per server tick, inside the clocks profiler zone and only while the tick-rate manager runs normally; the GameRules.ADVANCE_TIME check sits inside the method itself, and gates every clock at once, whereas ServerClockManager.setPaused gates one. Each unpaused instance then gains its rate, accumulating the fraction, so a clock at rate 0.5 gains a tick every other server tick and one at rate 1000 gains a thousand — /time rate accepts anything from 0.00001 to 1000.

Naming an instant on a clock

A ClockTimeMarker is a named instant on a clock: ClockTimeMarkers.DAY, NOON, NIGHT, MIDNIGHT, WAKE_UP_FROM_SLEEP, ROLL_VILLAGE_SIEGE. Markers are declared inside timelines and collected onto the clock by ServerClockManager.init, and Timeline.validateRegistry fails the whole registry load if two timelines on one clock declare the same one. The subset /time suggests is the one flagged ClockTimeMarker.showInCommands, though it accepts any marker on the clock; ServerClockManager.isAtTimeMarker is how VillageSiege asks whether the siege roll is due, and ServerLevel.tick calls ServerClockManager.moveToTimeMarker to jump the clock when enough players are asleep. Everything TimeCommand offers is offered twice, once against the source level’s DimensionType.defaultClock and once under /time of against a clock the player names, because a command that says set the time has to be told whose time it means.

The four timelines

timelineperiodwhat it carries
Timelines.OVERWORLD_DAY24000the whole day/night curve — sun, moon and star angles, sky and fog colours, sky light, and the gameplay flags that flip at dusk
Timelines.MOON192000 — eight daysthe moon phase, and the surface slime spawn chance riding the same steps
Timelines.VILLAGER_SCHEDULE24000EnvironmentAttributes.VILLAGER_ACTIVITY and EnvironmentAttributes.BABY_VILLAGER_ACTIVITY
Timelines.EARLY_GAMEnoneone ramp that ands EnvironmentAttributes.CAN_PILLAGER_PATROL_SPAWN with false until tick 120000

Three of the four have a period and are cycles. The fourth is the interesting one: a timeline with no period is not a cycle at all — its track runs once against the clock’s total ticks and then holds its last value forever, which is what makes Timelines.EARLY_GAME a switch, thrown when the overworld clock passes 120000 ticks and closed again only if /time sets the clock back below it, rather than something that comes round again.

All four run on WorldClocks.OVERWORLD; no timeline is bound to the End’s clock, which the End’s dimension type names as its default only, the clock /time sets there and the End’s flash reads. Which of them a dimension runs is a tag on DimensionType.timelines: TimelineTags.IN_OVERWORLD names the day, moon and early-game timelines on top of TimelineTags.UNIVERSAL, while TimelineTags.IN_NETHER and TimelineTags.IN_END name only the universal one, which holds the villager schedule. That schedule is the clearest case of a timeline driving something that is not a colour: Brain.setSchedule takes an EnvironmentAttribute of Activity, Villager is the only caller — adults read EnvironmentAttributes.VILLAGER_ACTIVITY and babies EnvironmentAttributes.BABY_VILLAGER_ACTIVITY, two tracks on the one timeline — and Brain.updateActivityFromSchedule samples it at the villager’s own position, but only when more than twenty game ticks have passed since it last looked. Where the activity it reads then sends a villager is in points of interest.

What crosses the wire

The rules travel, never the resolved values. Registries.TIMELINE and Registries.WORLD_CLOCK are in RegistryDataLoader.SYNCHRONIZED_REGISTRIES — the timeline through Timeline.NETWORK_CODEC, so only syncable tracks go — while Registries.ENVIRONMENT_ATTRIBUTE and Registries.ATTRIBUTE_TYPE are built-in code registries that never go out at all.

Syncable is the third flag, and it draws the line between the two sides. Thirty-three of the 51 carry it: every one of the 24 visual/ attributes and all four audio/ ones, and five of the gameplay flags — sky_light_level, fast_lava, water_evaporates, piglins_zombify, creaking_active. The eighteen left behind are gameplay decisions the server makes alone: whether monsters burn, whether a raid can start, what a villager should be doing now. Dropping their entries and tracks before the wire costs the client almost nothing: the one place it asks for them is the bed rules, read through the sleeping code it shares with the server, and there it gets the defaults. So the client’s stack is shorter than the server’s, and identical everywhere else the client looks.

Clock state rides ClientboundSetTimePacket: a game time — the world’s own tick count, which no clock owns — plus a ClockNetworkState — total ticks, partial tick, rate — per clock in its map. ServerClockManager.createFullSyncPacket fills that map on join, respawn and a change of dimension, and on a GameRules.ADVANCE_TIME change. Every mutator broadcasts a one-clock update. The routine broadcast from MinecraftServer.forceGameTimeSynchronization, once every twenty ticks, sends an empty map and nothing but the game time. ClientClockManager.handleUpdates adopts what arrives and ClientClockManager.tick free-runs the rest — which is why a paused clock travels as rate 0: the client has no paused flag to receive.

Dusk, and the same question asked twice

A mob asks whether it should be burning, and the camera asks what colour the sky is. Both go through the stack above, and this is the machinery running.

sequenceDiagram
    participant SL as ServerLevel
    participant EAS as Environment<br/>AttributeSystem
    participant EVS as Environment<br/>AttributeSystem.<br/>ValueSampler
    participant Mob as Mob
    participant ATS as Attribute<br/>TrackSampler
    participant KTS as Keyframe<br/>TrackSampler

    Note over SL,KTS: one server tick
    SL->>EAS: invalidateTickCache — before the world border, before the weather
    EAS->>EVS: the cached value is dropped, the cache tick id bumped
    Mob->>EAS: getValue(MONSTERS_BURN, position)
    EAS->>EVS: getValue — is any layer of this attribute positional?
    EVS->>EVS: none is — start from the baked base, the default false
    EVS->>ATS: applyTimeBased(value, cache tick id)
    ATS->>KTS: sample at the overworld clock's ClockInstance.totalTicks
    KTS-->>ATS: the argument — false at 12542, true again at 23460
    ATS-->>EVS: the modifier is BooleanModifier.OR, applied to the value
    EVS->>EVS: the weather layer is on nine attributes, not this one
    EVS-->>Mob: false, clamped and cached for the tick

One attribute resolved once, on the server, inside one tick: the level’s only contribution is the first two arrows, which throw last tick’s answer away, and every arrow after them is the stack being walked because a mob asked. Nothing here is pushed — a value exists because something wanted it.

Read the arrows as decisions. EnvironmentAttributeSystem.invalidateTickCache computes nothing: it drops each sampler’s cached value and bumps a counter, and that counter is the identity every track sampler compares against. AttributeTrackSampler.applyTimeBased keeps a one-entry cache of the sampled argument and reuses it for every reader arriving with the same tick id, so a thousand mobs asking EnvironmentAttributes.MONSTERS_BURN cost one keyframe sample between them.

The step that reads oddly is the one where the sampler asks whether any layer of the attribute is positional and finds that none is. EnvironmentAttributes.MONSTERS_BURN is a positional attribute, and yet its stack is one layer deep, because in vanilla nothing but the day timeline mentions it: no dimension type, no biome. EnvironmentAttributeSystem.ValueSampler decides by layers, not by the flag, so with no positional layer present the position is ignored and the whole answer is memoised for the tick. The flag still governs where the attribute may be read: it makes EnvironmentAttributeCheck and the two EnvironmentAttributeValue number providers declare LootContextParams.ORIGIN among the parameters they read, and the default answers when a context has none, and it makes EnvironmentAttributeSystem.getDimensionValue throw in a development build if asked for a positional attribute at all. Only two attributes are built EnvironmentAttribute.Builder.notPositional — sky light level and lava speed — and three call sites read them that positionless way: Level.updateSkyBrightness for EnvironmentAttributes.SKY_LIGHT_LEVEL, and LavaFluid.isFastLava and Entity for EnvironmentAttributes.FAST_LAVA, the two sites that between them decide how fast lava flows and how hard it shoves. A fourth site names no attribute at all: EnvironmentAttributeReader sends any non-positional attribute down this road when a loot context asks for one.

The period matters when a KeyframeTrackSampler is built: for a periodic track it bakes two extra segments, last keyframe to first on either side of the loop, so a value interpolates across the wrap — tick 0, where the day timeline’s period closes and ClockTimeMarkers.WAKE_UP_FROM_SLEEP sits — instead of snapping, and KeyframeTrackSampler.sample reduces the clock’s total ticks with a floor-mod before choosing one. EasingType supplies the curve, and the day timeline’s sun, moon and star angles share one symmetric cubic Bézier whose two keyframes both sit at tick 6000, so the baked segment runs noon to noon. The sun therefore turns slowest at its zenith — two thirds of the linear rate — and fastest at midnight, at about six fifths of it. That is why a Minecraft day is not two equal halves: the sun spends roughly 13,560 ticks above the horizon against 10,440 below.

The same value on the client

The client resolves the same stack and then does two things to it the server never does. Both of them are in the picture: one is about space, the other about time.

sequenceDiagram
    participant Camera as Camera
    participant EAP as Environment<br/>AttributeProbe
    participant GS as GaussianSampler
    participant SAI as SpatialAttribute<br/>Interpolator
    participant EAS as Environment<br/>AttributeSystem
    participant SR as SkyRenderer

    Note over Camera,SR: one client tick
    Camera->>EAP: tick(level, position) — once per client tick
    EAP->>GS: sample around the camera
    GS->>SAI: accumulate(weight, that biome's attributes) — 216 times
    Note over Camera,SR: between ticks, once per frame
    SR->>EAP: getValue(SKY_COLOR, partialTicks)
    opt only on the first frame of the tick that asks for this attribute
        EAP->>EAS: getValue(attribute, position, interpolator)
        EAS->>SAI: applyAttributeLayer — weighted blend of every biome in range
    end
    EAP-->>SR: AttributeType.partialTickLerp between last tick's value and this one

The same stack, on the other side of the wire, with the client’s two kinds of smoothing around it. Read the two note bars as the boundary they mark: the 216 biome samples happen once a tick whatever the frame rate, and the opt box is the part of the frame nobody pays for twice — the stack is resolved by the first frame that asks and every later frame in the same tick only lerps.

The client resolves the same stack from the same data — it is never sent a resolved value. What it adds is two kinds of smoothing the server never does. In space, EnvironmentAttributeProbe.tick prunes, clears, then runs GaussianSampler.sample over a 6×6×6 neighbourhood of quart-resolution biome cells — the 4×4×4 blocks a biome is stored per — for 216 samples in all. The weights come from a seven-entry kernel, 0-1-4-6-4-1-0, with each of the six taps along an axis lerped between two neighbouring entries by the camera’s sub-cell offset: the weights slide as the camera moves instead of snapping when it crosses a cell. Those weights accumulate into a SpatialAttributeInterpolator, whose SpatialAttributeInterpolator.applyAttributeLayer applies each contributing biome’s modifier to the base value and lerps the results together by weight. Only the 21 attributes flagged EnvironmentAttribute.isSpatiallyInterpolated are resolved that way; anything else takes the single biome under the position. But that flag is not a gate on the sampling: the probe runs its 216 samples every tick regardless, and the test is applied afterwards, when the layer is applied. The server pays none of it — it passes no interpolator at all. In time, each probed value keeps last tick’s answer beside this tick’s and returns AttributeType.partialTickLerp between them — and prunes itself, dropping any value nobody read during a tick. The resolve itself is once a tick, not once a frame: EnvironmentAttributeProbe.ValueProbe.tick nulls this tick’s answer, the first frame that asks for the attribute fills it in by walking the whole stack, and every frame after that in the same tick does nothing but the lerp. Only the first frame in a tick that asks pays for the walk.

The probe lives on Camera, ticked from Camera.tick and emptied by Camera.reset, and everything that draws the sky goes through it — the sky, the lightmap, the two fog environments, the clouds and the music. That is why lightmap, fog and sky never touches EnvironmentAttributeSystem directly. The probe is not a wall, though: the clock item reads sun_angle and moon_phase off ClientLevel.environmentAttributes directly, and so does ClientLevel itself for ambient particles.

The last difference is a tick wide, and it is the reason the two sides can disagree about the sky for one tick at dusk. They invalidate at opposite ends of the tick: ServerLevel.tick calls EnvironmentAttributeSystem.invalidateTickCache before the world border and the weather, then runs Level.updateSkyBrightness later in the same method, once sleeping and weather have resolved; ClientLevel.tick does the reverse, Level.updateSkyBrightness first and invalidation last, so the client’s sky-darken value comes from the previous tick’s clock.

Questions players ask

Why does the nether have no night? It has no day timeline: the nether’s and the End’s DimensionType.timelines both resolve to TimelineTags.UNIVERSAL alone. Its environment is constants instead — the dimension type pins gameplay/sky_light_level, gameplay/fast_lava, gameplay/water_evaporates and twelve more, which EnvironmentAttributeSystem.bakeLayerSampler folds into a base value that never changes again.

Does setting the time in the overworld move the End? No: each clock keeps its own ServerClockManager.ServerClockInstance. Every mutator does invalidate the cache on every level at once, though — ServerClockManager walks MinecraftServer.getAllLevels on each change, because a time jump must not leave half a tick of stale sky behind.

Why do pillager patrols not show up on day one? Because Timelines.EARLY_GAME is the timeline with no period, and its single modifier track ands EnvironmentAttributes.CAN_PILLAGER_PATROL_SPAWN with false until tick 120000 — a hundred minutes of play at the normal rate, sooner for every night slept through — and with true after. Nothing counts your days; the switch is one keyframe on a track that never comes round again.

For a 1.21-era reader. Three sets of names moved. The gameplay booleans on DimensionType — ultrawarm, bed_works, piglin_safe, respawn_anchor_works — are entries in DimensionType.attributes now, and ultrawarm has become several, EnvironmentAttributes.FAST_LAVA and EnvironmentAttributes.WATER_EVAPORATES chief among them. BiomeSpecialEffects keeps only the water, foliage and grass tints: sky and fog are Biome.getAttributes. And the villager Schedule class is Timelines.VILLAGER_SCHEDULE, a data-pack Timeline like any other.

Where to look

EnvironmentAttributes · EnvironmentAttribute.Builder · AttributeTypes · EnvironmentAttributeMap.Entry · EnvironmentAttributeSystem.Builder · EnvironmentAttributeSystem.bakeLayerSampler · EnvironmentAttributeSystem.invalidateTickCache · WeatherAttributes.addBuiltinLayers · Timelines · Timeline.createTrackSampler · AttributeTrackSampler.applyTimeBased · KeyframeTrackSampler.sample · ServerClockManager.tick · ClientClockManager.handleUpdates · EnvironmentAttributeProbe.tick · SpatialAttributeInterpolator.applyAttributeLayer · GaussianSampler.sample · TimeCommand


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Chunk anatomy

Verified against Minecraft 26.3 · Part IV · One block is placed, and the write travels down through the chunk until it lands in four bits of one long.

You place a single block of deepslate at y −40, in a section that until now held nothing but stone and air. The click ends in LevelChunk.setBlockState, and the write goes down four objects: a LevelChunkSection sixteen blocks tall, a PalettedContainer of 4,096 block states, a palette that turns the state into a small integer, and a SimpleBitStorage that puts that integer into some fixed number of bits of one long. Everything else in this part moves chunks — tickets load them, the pyramid generates them, the light engine walks them, the region file stores them. This is the page that looks inside, and it is the vocabulary the rest of the part spends. Begin with the part that surprises people: a section holding two distinct block states costs exactly what one holding sixteen costs — four bits an entry, 256 longs, on disk as well as in memory — and the block that makes it seventeen re-encodes all 4,096 entries into a wider storage before it can be written.

The cast

classwhat it decidesthread
ChunkAccesseverything a chunk has whatever its shape: position, height, sections, heightmaps, block entities, structures, the two volatile flagsabstract — whichever thread owns the shape below
ProtoChunka chunk under construction: status, entities as NBT, the light engine it reports towritten on the worker pool, one writer at a time
LevelChunka chunk that is part of a Level: block entities, tickers, tick containers, the full-status supplierthe Server thread — on the client, the Render thread
ImposterProtoChunkwhat a still-generating neighbour sees when the chunk it asked for is already livebuilt on the Server thread; read on the worker pool — by a neighbour’s step on the worldgen executor or in the buildTerrain fork, and by the light engine on the light executor
LevelChunkSection16×16×16: two palette containers and four counters that let a whole section be skippedwhichever thread holds its permit
PalettedContainerthe mapping from 4,096 (or 64) entries to values, and when to widen itone writer at a time — a second is detected, blocks, and both threads die; reads are lock-free
Strategywhich palette and which bit width each entry count deserves, for block states and for biomesimmutable, shared by every container in the level
Heightmapthe top of each of 256 columns, for one definition of topwith the chunk that owns it

The four shapes a chunk takes

classDiagram
    class ChunkAccess {
        <<abstract>>
        position and height
        sections, heightmaps, block entities, structures
    }
    class ProtoChunk {
        under construction
        written on the worker pool
    }
    class LevelChunk {
        part of a Level
        owned by the Server thread
    }
    class ImposterProtoChunk {
        what a still-generating neighbour is handed
        sections shared, most other writes dropped
    }
    class EmptyLevelChunk {
        void air, one fixed biome
    }
    ChunkAccess <|-- ProtoChunk
    ChunkAccess <|-- LevelChunk
    ProtoChunk <|-- ImposterProtoChunk
    LevelChunk <|-- EmptyLevelChunk
    ProtoChunk ..> LevelChunk : promoted, by ChunkStatusTasks.full
    ImposterProtoChunk --> LevelChunk : holds the one it stands in for

The four shapes and the one abstract base, with the two relations that are not inheritance: the dashed arrow is the promotion the whole part exists to reach, and the solid one is a wrapper holding the chunk it pretends to be. The two subclasses hang off different parents, which is the thing to look at — an imposter is a proto chunk and an empty chunk is a live one.

The diagram is the hierarchy and the two links across it, because the hierarchy is short enough to be the whole truth. ChunkAccess is the abstract chunk and has exactly two direct concrete lines — ProtoChunk (ChunkType.PROTOCHUNK) and LevelChunk (ChunkType.LEVELCHUNK) — with ImposterProtoChunk a subclass of the first and EmptyLevelChunk of the second. Nothing else extends it.

Every one of them carries the same core. ChunkAccess.chunkPos says where it is, and a LevelHeightAccessor says how tall: LevelHeightAccessor.getMinY and LevelHeightAccessor.getHeight are the only two facts about height there are, and the overworld’s −64 and 384 give LevelHeightAccessor.getSectionsCount of 24, section Y −4 through 19. Beside them sit the heightmaps, the block entities in two maps (ChunkAccess.blockEntities live, ChunkAccess.pendingBlockEntities still NBT, ChunkAccess.getBlockEntitiesPos the union), ChunkAccess.structureStarts and ChunkAccess.structureReferences, the per-section ChunkAccess.postProcessing offsets to revisit after load (ProtoChunk.packOffsetCoordinates packs four bits each of x, y and z into a short), ChunkAccess.inhabitedTime behind local difficulty (the level tick), ChunkAccess.upgradeData and the nullable ChunkAccess.blendingData whose presence is ChunkAccess.isOldNoiseGeneration (blending), and two volatile flags — ChunkAccess.unsaved, whose test-and-clear ChunkAccess.tryMarkSaved the saver uses, and ChunkAccess.isLightCorrect, saved as isLightOn. It is also three interfaces at once — LightChunk, which is what the light engine reads through LightChunk.findBlockLightSources and LightChunk.getSkyLightSources, plus StructureAccess and BiomeResolver — and ChunkAccess.getPersistedStatus is the ChunkStatus that goes to disk, with ChunkAccess.getHighestGeneratedStatus folding in BelowZeroRetrogen.targetStatus for a chunk still being deepened.

A ProtoChunk adds what only generation needs: a volatile ProtoChunk.status (ProtoChunk.setPersistedStatus also retires a finished BelowZeroRetrogen), a ProtoChunk.lightEngine from ProtoChunk.setLightEngine that it reports to only once the status ChunkStatus.isOrAfter ChunkStatus.INITIALIZE_LIGHT, its entities as a list of CompoundTag (ProtoChunk.addEntity serialises on the spot), and ProtoChunkTicks that ProtoChunk.unpackBlockTicks turns into LevelChunkTicks on promotion (appointments that survive a restart). The pool that fills all of that in is the generation pipeline. Ask it for a biome before ChunkStatus.BIOMES and ProtoChunk.getNoiseBiome throws Asking for biomes before we have biomes.

A LevelChunk adds what only a live chunk needs: LevelChunk.level, LevelChunk.setLoaded, a supplier of FullChunkStatus the holder owns (the four statuses), two LevelChunkTicks that LevelChunk.registerTickContainerInLevel attaches to the level’s queues and LevelChunk.unregisterTickContainerFromLevel detaches, the ticker map, a one-shot LevelChunk.postLoad processor that LevelChunk.runPostLoad fires, the per-section LevelChunk.gameEventListenerRegistrySections — an EuclideanGameEventListenerRegistry each, built on demand and only on a server (listeners live in the chunk) — and a LevelChunk.unsavedListener that LevelChunk.markUnsaved fires only on the false-to-true edge, which is how the server’s dirty set learns of a change without scanning. Its LevelChunk.getPersistedStatus is always ChunkStatus.FULL.

ImposterProtoChunk exists because a neighbour still generating asks the holder for “the chunk at status X” and must be handed something ProtoChunk-typed even when that chunk is already live. Reads delegate to ImposterProtoChunk.getWrapped; writes are dropped unless allowWrites, which both of the two places that construct one pass as false — heightmaps, structure starts and references and block-entity NBT unconditionally, and the rest for want of the flag. The sections are the exception: ImposterProtoChunk.getSections hands back the wrapped chunk’s array unconditionally, and the single-section ImposterProtoChunk.getSection, though gated on the flag, falls back to that same array, so a write through a section lands on the live chunk; and ImposterProtoChunk.markUnsaved and ImposterProtoChunk.setLightCorrect always pass through and ImposterProtoChunk.canBeSerialized is false, because the LevelChunk under it is what gets saved. Its ImposterProtoChunk.fixType maps a request for a _WG heightmap onto the live one, but only inside ImposterProtoChunk.getHeight: asking it to create a _WG heightmap creates a real one on the live chunk.

What makes that wrapper honest is what promotion does not do. ChunkAccess always allocates its own array and copies the section references in, so a promoted chunk leaves two chunks holding two arrays over one set of LevelChunkSection objects: writing through either is writing the same blocks, and the proto chunk a generating neighbour is still holding goes on working. The imposter is a third handle on the same sections.

EmptyLevelChunk is the other direction: Blocks.VOID_AIR everywhere, EmptyLevelChunk.isEmpty true where LevelChunk.isEmpty is false, one fixed biome, and EmptyLevelChunk.getFullStatus a flat FullChunkStatus.FULL. ClientChunkCache.emptyChunk is one shared instance handed out for any client miss, and only when the caller asked to load or generate — otherwise the miss returns null; PathNavigationRegion builds its own for whatever a mob’s pathfinder cannot see. The real client chunks live in ClientChunkCache.Storage, an AtomicReferenceArray ring whose ClientChunkCache.Storage.onSectionEmptinessChanged and its double-buffered added and removed sets are the renderer’s feed of which sections exist.

Sections and their four counters

A chunk’s block storage is four things nested inside each other — the chunk, its sections, each section’s containers and each container’s data — and the four counters hang off the second. Here is the whole of it.

flowchart TD
    LC["LevelChunk: one 16 by 16 column of the build height"]
    HM["at least four Heightmaps, 256 entries of 9 bits each"]
    ARR["ChunkAccess.sections: 24 LevelChunkSection in the overworld, never a null slot"]
    CNT["four shorts: non-empty blocks, fluids, and a ticking count of each"]
    ST["LevelChunkSection.states: PalettedContainer of BlockState, 4,096 entries"]
    BIO["LevelChunkSection.biomes: PalettedContainerRO of Biome, 64 entries"]
    D1["its own PalettedContainer.Data: a Palette and a BitStorage, sized together"]
    D2["its own PalettedContainer.Data, on a shorter ladder"]
    LC --> HM
    LC --> ARR
    ARR --> CNT
    ARR --> ST
    ARR --> BIO
    ST --> D1
    BIO --> D2

A chunk’s block storage, down to the bit storage: the two containers of a section have a PalettedContainer.Data each and never share one, and the four counters hang off the section rather than the containers, which is why they can answer without touching either. Nothing in this picture is light — the two 4-bit fields live in the light engine’s own storage, not on the section (lighting).

The array never has a hole: ChunkAccess.replaceMissingSections runs in the constructor and fills every empty slot with a fresh all-air section from the level’s PalettedContainerFactory, so nothing that walks sections checks for null.

The four counters are what make a section cheap to skip. LevelChunkSection.setBlockState adjusts all four from the outgoing and incoming state on every single write — LevelChunkSection.nonEmptyBlockCount (zero is LevelChunkSection.hasOnlyAir, which is also how LevelChunk.getBlockState answers air without touching a palette), LevelChunkSection.fluidCount, LevelChunkSection.tickingBlockCount and LevelChunkSection.tickingFluidCount — and LevelChunkSection.isRandomlyTicking is the or of the last two. That one boolean lets ServerLevel skip a section of solid stone without looking at any of its 4,096 blocks. Only a load from disk recounts: LevelChunkSection.recalcBlockCounts runs from the two-container constructor, whose only caller is SerializableChunkData.

On a client, two of the four are permanently wrong and nothing notices. LevelChunkSection.write carries only the non-empty-block and fluid counts, and LevelChunkSection.read takes exactly those two and never recounts, so a client section starts with LevelChunkSection.tickingBlockCount and LevelChunkSection.tickingFluidCount at zero and only ever counts what has changed since the chunk arrived. The sole reader of LevelChunkSection.isRandomlyTicking is ServerLevel, and the client runs no random ticks.

Biomes share the section but are coarse and read-only. Two bits per axis, 64 entries of 4×4×4 blocks each — the 2 LevelChunkSection.BIOME_CONTAINER_BITS names. The field is a PalettedContainerRO and the published one is never mutated: LevelChunkSection.fillBiomesFromNoise, LevelChunkSection.read and LevelChunkSection.readBiomes each build a replacement through PalettedContainerRO.recreate, fill it, and swap the reference. The block-state container is the only one anything writes to in place.

Two different copies leave a section. The saver takes LevelChunkSection.copy, a deep copy of both containers and all four counters, on the Server thread inside SerializableChunkData.copyOf — the IO lane never sees a live section, and even the NBT encoding of the copy runs on the background pool (copy on the server, encode on a worker, write on the IO lane). The client mesher takes something cheaper: a SectionCopy takes PalettedContainer.copy of the block-state container alone (nothing at all when the section is air) plus an immutable snapshot of the chunk’s block-entity map. A worker that means to write many blocks at once instead brackets its work with LevelChunkSection.acquire and LevelChunkSection.release.

The permit, and what happens to the thread that misses it

That bracket is the permit the cast table names, and it is not a mutex. PalettedContainer.threadingDetector is a ThreadingDetector: a one-permit semaphore with a reporter attached. ThreadingDetector.checkAndLock tries the permit and, on failure, records itself as the loser and then blocks. It is the winner that notices, in ThreadingDetector.checkAndUnlock: it builds ThreadingDetector.makeThreadingException — Accessing PalettedContainer from multiple threads, with both stack traces — and throws it, and the loser re-throws the same report the instant the permit comes free. Both threads die, deliberately: an interleaved section write would be a corrupt world rather than a crash.

Exactly one thread writes a section at a time, then — the Server thread for a live chunk, and on the worker pool whoever holds the permit: most generation takes it for one write at a time, and two hold it across many through LevelChunkSection.acquire, NoiseBasedChunkGenerator, which holds every section across its noise range, and the BulkSectionAccess that OreFeature, its only user, holds over every section it touches until it closes. Those hold the permit already, so they write through the unchecked five-argument LevelChunkSection.setBlockState and PalettedContainer.getAndSetUnchecked.

The palette and the ladder it climbs

A PalettedContainer’s whole state is one volatile record, PalettedContainer.Data, holding a Configuration, a Palette and a BitStorage. PalettedContainer.get reads the record once into a local and takes no lock, because a resize swaps in a whole new record rather than editing the old one. PalettedContainer.read from the wire is the exception that proves it: PalettedContainer.createOrReuseData hands back the existing record whenever the incoming bit count wants the same configuration, and the palette and the long array are then overwritten in place — and on a client the record overwritten can be one already in use, whenever ClientChunkCache reuses a LevelChunk it still holds at that position. Which record a given entry count deserves is decided by a top-level Strategy — no longer nested inside the container — whose Strategy.createForBlockStates and Strategy.createForBiomes are called once per level by PalettedContainerFactory.create over Block.BLOCK_STATE_REGISTRY and the biome registry (registries), which is why the global palette’s width is a runtime number and not a constant.

Strategy.getConfigurationForBitCount is the ladder, and block states and biomes climb different ones:

distinct values (bits needed)block statesbiomes
1 (0 bits)Strategy.ZERO_BITS → SingleValuePalette + ZeroBitStoragethe same
2 … 8 (1–3 bits)Strategy.FOUR_BITS_LINEAR → LinearPalette, already 4 bitsStrategy.ONE_BIT_LINEAR / Strategy.TWO_BITS_LINEAR / Strategy.THREE_BITS_LINEAR → LinearPalette at its own width
9 … 16 (4 bits)Strategy.FOUR_BITS_LINEAR, still 4 bitsConfiguration.Global already
17 … 256 (5–8 bits)Strategy.FIVE_BITS_HASHMAP … Strategy.EIGHT_BITS_HASHMAP → HashMapPaletteConfiguration.Global — there is no hashed tier for biomes
moreConfiguration.Global → GlobalPalette, the registry’s own IdMapConfiguration.Global

SingleValuePalette holds one value and asks for width 1 the moment a second arrives — which for block states means jumping straight to the 4-bit rung. LinearPalette is a flat array of 2^bits slots scanned by identity, HashMapPalette a CrudeIncrementalIntIdentityHashBiMap, and GlobalPalette writes nothing on the wire, maps an unknown value to id 0 and answers Palette.maybeHas with an unconditional yes. Each of the first three calls PaletteResize.onResize when it fills, and the container is its own PaletteResize: PalettedContainer.onResize builds the next record, PalettedContainer.Data.copyFrom walks every entry of the old storage through the old palette into the new one (or, from a single value, fills), the record is published, and only then is the value that triggered the growth added — under PaletteResize.noResizeExpected, which throws if a second growth were somehow needed.

The palette is also what lets a search rule out a section it never reads. LevelChunkSection.maybeHas puts a predicate to the palette alone, so the scan for block-light sources behind ChunkAccess.findBlockLightSources, and the points-of-interest check on a loaded chunk, can dismiss 4,096 blocks with a handful of comparisons. The one rung where that stops paying is Configuration.Global, whose Palette.maybeHas is an unconditional yes.

SimpleBitStorage never lets an entry straddle a long: its SimpleBitStorage.valuesPerLong is 64 divided by the width, so 4,096 entries are 256 longs at four bits, 342 at five and 512 at eight, and the cell index comes from a multiply-shift table (SimpleBitStorage.MAGIC) rather than a division. ZeroBitStorage answers 0 for everything and shares one empty ZeroBitStorage.RAW array. An array of the wrong length raises SimpleBitStorage.InitializationException, which PalettedContainer.unpack turns into a DataResult error instead of a crash.

What packing actually buys

The two serialised forms differ. PalettedContainer.write is the wire: a bits byte, the palette, a fixed-size long array at exactly the in-memory width. PalettedContainer.pack is the disk (the palette and optional data fields of a PalettedContainerRO.PackedData, behind PalettedContainer.codecRW and PalettedContainer.codecRO — codecs), and it re-encodes into a fresh HashMapPalette before asking Strategy.getConfigurationForPaletteSize for the width — the same ladder memory climbs. What packing recomputes is the palette: unreferenced entries are dropped, and the smaller palette is then measured against the same rungs. Narrower entries are the consequence, in two cases only — when the shrunken palette lands a rung lower than the one memory was using, and when a Configuration.Global container writes at Configuration.bitsInStorage instead of Configuration.bitsInMemory. PalettedContainer.unpack re-encodes on the way back only for Configuration.Global, whose Configuration.alwaysRepack is true — every Configuration.Simple rung reports one width for both, so its long array is adopted exactly as it lies on disk.

The third form, and what the client is handed

Memory and disk are two of three. The wire form is ClientboundLevelChunkWithLightPacket, whose ClientboundLevelChunkPacketData carries the heightmaps for which Heightmap.Types.sendToClient is true, one buffer holding every section’s LevelChunkSection.write — empty ones included — and the block-entity update tags. The writer pre-sizes that buffer from the sum of LevelChunkSection.getSerializedSize and throws if ClientboundLevelChunkPacketData.extractChunkData does not fill it to the byte, and the reader refuses anything over two megabytes. Light rides beside it in ClientboundLightUpdatePacketData.

The client applies the chunk through ClientPacketListener.handleLevelChunkWithLight → ClientChunkCache.replaceWithPacketData → LevelChunk.replaceWithPacketData, which clears the block entities, gives each section LevelChunkSection.read, installs the raw heightmaps with ChunkAccess.setHeightmap and rebuilds the sky-light sources, while the light it carries is queued, for ClientLevel.pollLightUpdates to apply later in the same frame unless a backlog is ahead of it. Biome-only refreshes come later as ClientboundChunksBiomesPacket → LevelChunk.replaceBiomes, and the block-entity tags travel as ClientboundLevelChunkPacketData.BlockEntityInfo. When a chunk is sent, and to whom, belongs to what the client is told.

The six heightmaps

A Heightmap is 256 entries — one per column, indexed x + z·16 — in a SimpleBitStorage whose width is Mth.ceillog2 of height + 1, so 9 bits for a 384-tall world, stored relative to the minimum Y. Heightmap.getFirstAvailable is the first free Y and Heightmap.getHighestTaken the one below it. Heightmap.primeHeightmaps fills several types in a single top-down column scan (starting from the deprecated-for-removal ChunkAccess.getHighestSectionPosition) into maps that ChunkAccess.getOrCreateHeightmapUnprimed makes on demand. Heightmap.update is the incremental path, raising the height when an opaque block is placed at or above it and rescanning downward only when the block that turned transparent was the top one.

typeopaque meansusagesavedsent
Heightmap.Types.WORLD_SURFACE_WGnot airHeightmap.Usage.WORLDGENproto onlyno
Heightmap.Types.WORLD_SURFACEnot airHeightmap.Usage.CLIENTyesyes
Heightmap.Types.OCEAN_FLOOR_WGin BlockTags.BLOCKS_MOTION_IN_HEIGHTMAPHeightmap.Usage.WORLDGENproto onlyno
Heightmap.Types.OCEAN_FLOORin the same tagHeightmap.Usage.LIVE_WORLDyesno
Heightmap.Types.MOTION_BLOCKINGin that tag, or holds fluidHeightmap.Usage.CLIENTyesyes
Heightmap.Types.MOTION_BLOCKING_NO_LEAVESin BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP_NO_LEAVES, or holds fluidHeightmap.Usage.CLIENTyesyes

Which of the six a chunk carries follows its status: ChunkStatus.heightmapsAfter is the two _WG maps through ChunkStatus.BIOMES and ChunkStatus.FINAL_HEIGHTMAPS — the other four — from ChunkStatus.TERRAIN on, and a LevelChunk is constructed with exactly those four. A ProtoChunk primes any of its status’s maps that are missing the first time a block is written. What is saved, though, is not Heightmap.Types.keepAfterWorldgen: the saver writes every heightmap the chunk holds, so a proto chunk saves its two _WG maps along with any it has primed since, and a LevelChunk saves the four it was built with and any asked for since, which ChunkAccess.getHeight primes on demand. Separately and privately, ChunkAccess.skyLightSources is a second 256-entry bit storage — a ChunkSkyLightSources — that only the sky-light engine reads (the sky column is a table, not a flood).

What placing a block actually does

LevelChunk.setBlockState is the ordinary write path into a live chunk, and its order matters more than any single step in it. Twelve steps, of which three consult the caller’s flag word — and one of those three has only a part of itself skipped. The bits are enumerated in block update flags:

in orderwhat happenswhen it is skipped
1the section is fetched and its emptiness rememberedair into an all-air section returns null immediately
2LevelChunkSection.setBlockState writes the palette entry and moves all four counters—
3the old state is compared to the newidentical state returns null, and nothing below runs
4all four heightmaps take Heightmap.update—
5if the section’s emptiness flipped: LevelLightEngine.updateSectionStatus and ChunkSource.onSectionEmptinessChangedwhen it did not flip
6if LightEngine.hasDifferentLightProperties: ChunkSkyLightSources.update, then LevelLightEngine.checkBlockwhen opacity and emission are unchanged and neither state uses a shape for light occlusion
7the old block entity is dropped, preceded on the server by BlockEntity.preRemoveSideEffectswhen the block did not change, had no block entity, or BlockBehaviour.BlockStateBase.shouldChangedStateKeepBlockEntity — the side effects alone are skipped on the client and under Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS
8BlockBehaviour.BlockStateBase.affectNeighborsAfterRemovalwhen the block did not change and the new one is no BaseRailBlock, off the server, or without Block.UPDATE_NEIGHBORS and not moved by a piston
9the section is re-read — if step 8 changed the block again, the call returns null—
10BlockBehaviour.BlockStateBase.onPlaceon the client, or under Block.UPDATE_SKIP_ON_PLACE
11the new block entity is created or re-validated, and its ticker reboundwhen the new state has no block entity, or step 10 changed the block
12LevelChunk.markUnsaved—

One step there is easy to misread. Step 9 exists because step 8 runs arbitrary block code that may write the same position again, and LevelChunk.setBlockState will not claim a placement step 8 took from it — which is the sharpest thing this write path says about itself: it is the only step whose whole job is to notice that the world moved underneath it. Step 8’s own place in the wider update story, and the shape updates and redstone notifications that fire only after the chunk returns, are told in back in Level.setBlock’s tail.

ProtoChunk.setBlockState is the same idea with everything live stripped out: section write, light only past ChunkStatus.INITIALIZE_LIGHT, the status’s heightmaps updated (and any absent primed after the write), and no block entity, no BlockBehaviour.BlockStateBase.onPlace, no neighbours at all.

What step 11 leaves behind, and what the chunk goes on holding

The ticker step matters to a chunk’s anatomy for one reason: the handle it creates belongs to the chunk, not to the block entity in it. LevelChunk.addAndRegisterBlockEntity puts a wrapper in LevelChunk.tickersInLevel, and the level’s flat ticker list holds that: a block entity swapped in place, or kept through a state change, is rebound into the same wrapper, and one that is removed takes its wrapper out of the map and rebinds it to nothing, which the level’s list then drops. What the wrappers are and how the level’s list prunes itself is told in loaded is not enough to tick.

The other thing the chunk goes on holding is a debt from generation (and, for a chunk from an old save, from its upgrade), paid when the chunk starts ticking: LevelChunk.postProcessGeneration replays the post-processing offsets, promotes every pending block entity and applies UpgradeData.upgrade.

Pending is the word to take seriously. A block entity placed during generation that nothing has asked for since, every one in a half-generated chunk read back from disk, and one saved while still pending are not BlockEntitys yet (a structure’s loot chest is made live when its loot table is set, and a full chunk read from disk makes the rest live as it loads); each is a CompoundTag in ChunkAccess.pendingBlockEntities (block entities). Any call to LevelChunk.getBlockEntity promotes one through LevelChunk.promotePendingBlockEntity on the first touch, whatever the LevelChunk.EntityCreationType asked for; LevelChunk.postProcessGeneration promotes the rest in bulk when the chunk starts ticking. Of the three creation types only LevelChunk.EntityCreationType.IMMEDIATE and LevelChunk.EntityCreationType.CHECK have callers left in the game — LevelChunk.EntityCreationType.QUEUED has none.

Where to look

In the order the write takes them: ChunkAccess · LevelChunk.setBlockState · LevelChunkSection.setBlockState · PalettedContainer.onResize · Strategy.createForBlockStates · SimpleBitStorage · Heightmap.Types. Then the shapes a chunk is not: ProtoChunk.setBlockState · ImposterProtoChunk · ChunkStatusTasks.full. And the two serialised forms: PalettedContainer.pack · ClientboundLevelChunkPacketData.extractChunkData. Every field no page names is in the class index.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

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

classwhat it decidesthread
TicketStoragewhich tickets exist on which chunk, and which survive a restart — it is the chunk_tickets SavedDataServer
DistanceManagerowns both graphs, the two player-radius trackers and the four-slot throttleServer
LoadingChunkTracker · SimulationChunkTrackerthe two graphs: ChunkTrackers over DynamicGraphMinFixedPoint, one level higher per ringServer
ChunkLevelthe number line — which level means which FullChunkStatus and which generation statusstatic
ChunkHolderone chunk’s level and its three futuresServer; futures complete on workers, confirmations hop back
ChunkMapthe holders (an updating map and a visible clone), each player’s ChunkTrackingView, unloadsServer; workers read the visible map
ServerChunkCachethe level’s ChunkSource: the tick slot, the synchronous ServerChunkCache.getChunk, and a BlockableEventLoop pinned to the Server threadServer
PlayerChunkSenderthe per-player batches, paced by the client’s acknowledgementsServer

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:

typetimeoutloadssimulateskeeps dimension activepersistswho 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.PORTAL300✓✓✓✓Entity on portal travel, radius 3
TicketType.ENDER_PEARL40✓✓✓ServerPlayer, the pearl’s chunk, radius 2
TicketType.DRAGON—✓✓EnderDragonFight, radius 9
TicketType.PLAYER_SPAWN20✓PrepareSpawnTask during configuration, radius 3
TicketType.SPAWN_SEARCH1✓PlayerSpawnFinder
TicketType.UNKNOWN1✓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 questionwho asks itwhich graph answers
does a holder exist, and how far may it generateChunkMap.updateChunkScheduling, GenerationChunkHolder.updateHighestAllowedStatusloading
may this chunk be sent to a playerChunkMap.onChunkReadyToSend, from the level-32 futureloading
do its blocks and fluids tickServerLevel.shouldTickBlocksAtsimulation
do its entities tickthe entity loop, through DistanceManager.inEntityTickingRangesimulation
which chunks does the level walk for random ticksChunkMap.forEachBlockTickingChunk, a wrapper over DistanceManager.forEachEntityTickingChunksimulation
may mobs spawn hereDistanceManager.naturalSpawnChunkCounter, feeding ChunkMap.anyPlayerCloseEnoughForSpawningneither: 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):

  1. The tick. ServerChunkCache.tick, from the level tick, runs TicketStorage.purgeStaleTickets and then ServerChunkCache.runDistanceManagerUpdates.
  2. Idle time. When the server’s own queue has nothing to run and the server is ahead of its schedule, blocked or sprinting, MinecraftServer.pollTaskInternal offers the levels’ ServerChunkCache.MainThreadExecutor.pollTask a turn, one after another until one runs something; that poll runs the distance updates first and, if they did any work, returns at once.
  3. A synchronous ask. ServerChunkCache.getChunk from anywhere on the Server thread checks a four-entry cache, then ServerChunkCache.getChunkFutureMainThread adds a TicketType.UNKNOWN ticket 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 momentwhat the ticket system decidesthe gate
the player crosses a section boundarythe two crescents: which chunks entered the view and which leftChunkMap.updateChunkTracking returns early on the same centre and view distance, and ChunkMap.applyChunkTrackingView sends ClientboundSetChunkCacheCenterPacket only when the centre moved
a chunk enters the viewChunkMap.markChunkPendingToSend, but only for a chunk that already existsChunkMap.getChunkToSend wants a ticking chunk; a fresh one waits for its promotion instead
a chunk reaches BLOCK_TICKINGChunkMap.onChunkReadyToSend makes it pending for every player whose view holds itChunkHolder.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 viewClientboundForgetLevelChunkPacketonly 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 deliverednothing at allChunkMap.isChunkTracked is false while the chunk sits in PlayerChunkSender.pendingChunks; the full chunk will carry it
the settings changea radius moves, for every player at oncePlayerList.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 ticketdies when
no timeout — player, forced, dragonits 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.UNKNOWNthe 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, spawnthe 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 stopsevery 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 (in world/level, not server/level) is the saved data now. The two graphs are separate top-level classes, not inner classes of DistanceManager, and the whole ChunkHolder generation 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.

The chunk generation pipeline

Verified against Minecraft 26.3 · Part IV · A ticket asks for one chunk at FULL, and the server claims five hundred and twenty-nine of them before it runs a single step.

A player walks east, a loading ticket lands on the chunk that just entered view, and ChunkHolder.updateFutures asks that chunk for ChunkStatus.FULL. The request names that chunk and the eight around it, and nothing further out. But FULL is the last of ten steps, and the steps below it read — and one of them writes — the chunks around the one being built, so the first thing ChunkGenerationTask.create does is walk out to Chebyshev distance 11 and take a claim on every holder in that square: asking for one chunk asks for 529 of them, and the eleven rings it claims will never, on this task’s account, become chunks you could stand on. Eleven is not a tuning constant. It is the index of the last entry of ChunkStep.accumulatedDependencies for the FULL step — a twelve-entry list, one per ring from the centre out: ChunkLevel.RADIUS_AROUND_FULL_CHUNK reads that radius off the pyramid, and ChunkLevel.MAX_LEVEL, 44, is 33 plus it. Change the pyramid and the world’s loading radius changes with it.

The cast

classwhat it decidesthread
ChunkStatusthe ten names and their order, each with the chunk type and the heightmaps it leaves — no task, no radius, no workstatic, a BuiltInRegistries.CHUNK_STATUS entry
ChunkPyramidthe two step lists — one for generating, one for loading — that say what each status needs and what runs itstatic
ChunkStepone status’s direct and accumulated dependencies, its block-state write radius, and its bodystatic
ChunkGenerationTaskone (chunk, target) walk: which layer is in flight, which pyramid it is using, and when to yieldthe worldgen executor
GenerationChunkHolderone chunk’s ten futures, its ticket-derived ceiling, and the compare-and-set that runs each step exactly onceany — its mutable fields are atomic or volatile
ChunkMapmakes the tasks, owns both executors, and turns the EMPTY step into a disk readServer, but ChunkMap.applyStep runs on whatever thread reached it
ChunkTaskDispatcherwhich chunk’s batch of work the executor gets next, and re-sorts the queue when tickets moveits own single-file queue on the worker pool
WorldGenRegionwhat a running step may read and what it may write, checked per callthe thread running the step

The pyramid, drawn

The pipeline is ten steps in a fixed order, and three facts about each of them — how wide it is swept, where it runs, and how far from its own chunk it may write. Those are three columns, so they are a table:

#statusswept towhere it runsmay write
1ChunkStatus.EMPTY11the disk read — region file on the IO lane, upgrade and parse on the pool, chunk object on the Server thread—
2ChunkStatus.STRUCTURE_STARTS11inline, worldgen executor—
3ChunkStatus.STRUCTURE_REFERENCES3inline, worldgen executor—
4ChunkStatus.BIOMES3forked to the worker pool, as createBiomes—
5ChunkStatus.TERRAIN2forked to the worker pool, as buildTerrain0
6ChunkStatus.FEATURES1inline, worldgen executor1
7ChunkStatus.INITIALIZE_LIGHT1the light executor—
8ChunkStatus.LIGHT0the light executor—
9ChunkStatus.SPAWN0inline, worldgen executor—
10ChunkStatus.FULL0the Server thread—

Six of the ten leave the worldgen executor and one of them — ChunkStatus.EMPTY — is not worldgen work at all. The swept to column is how wide that layer is swept when the target is FULL and the task has decided it must generate: ChunkGenerationTask.getRadiusForLayer asks the FULL step of whichever pyramid is in play for ChunkStep.getAccumulatedRadiusOf that status. A task aiming lower sweeps narrower rings, and a chunk that only ever reaches STRUCTURE_STARTS is swept at radius 0 by its own task. ChunkStatus.EMPTY is the row to read twice: the first sweep is the loading pyramid’s radius 1, and only a chunk that turns out to need generating is swept at 11, as the load-or-generate section below explains.

Those radii nest, and nesting is what the word pyramid is doing. Read from the middle outward:

flowchart TD
    subgraph OUT["STRUCTURE_STARTS, to 11"]
        subgraph R3["BIOMES, to 3"]
            subgraph R2["TERRAIN, to 2"]
                subgraph R1["INITIALIZE_LIGHT, to 1"]
                    C["the chunk you asked for: SPAWN, then FULL"]
                end
            end
        end
    end

The pyramid seen from above: each box is a ring of neighbours, labelled with how deep into the pipeline that ring must already be, four rings at one instant rather than an order in time. The outer one is the number to take away: radius 11 is 23 by 23, so one chunk reaching ChunkStatus.FULL has claimed 529.

That is ChunkStep.accumulatedDependencies drawn: a list indexed by distance, holding the deepest status needed at that distance. For FULL it has twelve entries — ChunkStatus.SPAWN at distance 0, ChunkStatus.INITIALIZE_LIGHT at 1, ChunkStatus.TERRAIN at 2, ChunkStatus.BIOMES at 3, and ChunkStatus.STRUCTURE_STARTS for every distance from 4 out to 11 — so the radius is 11 and the neighbourhood is 529.

A ChunkStatus carries no work. It is a registry entry with an index, a parent, a ChunkType (ChunkType.PROTOCHUNK for the first nine, ChunkType.LEVELCHUNK for ChunkStatus.FULL) and the heightmaps that become valid after it, ChunkStatus.heightmapsAfter. Everything else — the dependencies, the write radius, the body — lives in the ChunkStep that ChunkPyramid holds for that status. Each step is built from its predecessor, so every step silently requires its own parent status at distance 0 before it declares anything; ChunkStep.Builder.addRequirement then widens the array outward, taking the later of the two statuses at every distance already covered.

The generation pyramid’s declared requirements, with the parent requirement resolved in (INITIALIZE_LIGHT and FULL, like STRUCTURE_STARTS, declare nothing beyond their parent):

stepneedsmay write
STRUCTURE_STARTSEMPTY at 0—
STRUCTURE_REFERENCESSTRUCTURE_STARTS from 0 out to 8—
BIOMESSTRUCTURE_REFERENCES at 0, STRUCTURE_STARTS out to 8—
TERRAINBIOMES within 1, STRUCTURE_STARTS out to 8radius 0
FEATURESTERRAIN within 1, STRUCTURE_STARTS out to 8radius 1
LIGHTINITIALIZE_LIGHT within 1—
SPAWNLIGHT at 0, BIOMES at 1—

The rows that do the work are the radius-1 ones: they force a neighbour to run one step ahead of the chunk being built. Four requirements in the pyramid have radius 1, and only three of them widen the accumulated list — which is the arithmetic the page’s headline number rests on, so it is worth stating as a rule.

ChunkStep.Builder.getRadiusOfParent asks a narrower question than does this step want anything a ring out. It asks how far out this step still demands its own immediate predecessor, and it charges that many rings to everything the predecessor already needed. TERRAIN’s predecessor is BIOMES and TERRAIN asks for BIOMES at radius 1, so its whole inherited list slides out by one. SPAWN’s predecessor is LIGHT, and although SPAWN also asks for BIOMES within 1, BIOMES is behind LIGHT: at ring 1 SPAWN is demanding something its predecessor has already passed, which costs nothing. The three that pay are TERRAIN wanting BIOMES, FEATURES wanting TERRAIN and LIGHT wanting INITIALIZE_LIGHT, each one ring. Three ones on top of STRUCTURE_STARTS out to 8 is where the 11 comes from — a radius of 11 is a list of twelve, and the walk that claims it is 23 chunks on a side, or 529. The 8 is the distance ChunkStatus.MAX_STRUCTURE_DISTANCE names.

The same arithmetic sets the edge of the world. ChunkPyramid.SAFETY_MARGIN_CHUNKS is 32 plus the twelve accumulated entries plus one, doubled — 90 chunks — subtracted from the coordinate maximum to give ChunkPyramid.MAX_CHUNK_COORDINATE_VALUE, which ChunkPos.isValid enforces and the GenerationChunkHolder constructor throws on. It is a guard against arithmetic, not against players: at about 33.5 million blocks it sits three and a half million blocks outside Level.MAX_LEVEL_SIZE, the ±30 000 000 nobody can build past anyway.

A ticket sets a ceiling, and a separate call names the target

Two different numbers reach a holder from the ticket system. DistanceManager.runAllUpdates first gives every touched holder GenerationChunkHolder.updateHighestAllowedStatus, which is ChunkLevel.generationStatus of the new ticket level — the number line that maps 34 through 44 onto ever-earlier statuses belongs to the number line. That is a ceiling, not a goal: GenerationChunkHolder.isStatusDisallowed gates every request against it and hands back GenerationChunkHolder.UNLOADED_CHUNK_FUTURE for anything above. Then ChunkHolder.updateFutures crosses FullChunkStatus.FULL, calls ChunkMap.prepareAccessibleChunk, and that names the target: ChunkMap.getChunkRangeFuture over the 3×3, ChunkStatus.FULL on the centre and ChunkLevel.getStatusAroundFullChunk — INITIALIZE_LIGHT — on the eight around it, each through GenerationChunkHolder.scheduleChunkGenerationTask.

If the ceiling later drops, GenerationChunkHolder.updateHighestAllowedStatus fails every pending future between the new ceiling and the old with GenerationChunkHolder.UNLOADED_CHUNK and reschedules the task at the highest status anyone is still waiting for. Nothing is interrupted; a worker mid-step finishes it and finds nobody listening.

The other way in is synchronous: ServerChunkCache.getChunk from the server thread manufactures its own ticket and then blocks on the future it arms (when the graphs run). What matters to this page is the consequence — the thread that is blocked on generation is also the thread finishing it, because the executor it blocks on drains chunk work while it waits.

The task claims its 529 before it runs anything

GenerationChunkHolder.scheduleChunkGenerationTask finds no task in flight, so GenerationChunkHolder.rescheduleChunkTask calls ChunkMap.scheduleGenerationTask and ChunkGenerationTask.create builds the holder set: a StaticCache2D whose radius is the generation pyramid’s accumulated radius of ChunkStatus.EMPTY for the target — 11 for FULL — filled by GeneratingChunkMap.acquireGeneration, the five-method interface ChunkMap implements and the pipeline holds. That radius is taken from the generation pyramid unconditionally, before anything has looked at the disk, so even a chunk that turns out to be sitting complete in a region file claims all 529 holders first.

The claim is a reference count. GenerationChunkHolder.increaseGenerationRefCount on the first claim arms GenerationChunkHolder.generationSaveSyncFuture and hangs it off the holder’s save dependency, so nothing in the square can be saved or unloaded while the task lives (a chunk nobody needs any more). The task itself waits in ChunkMap.pendingGenerationTasks until ChunkMap.runGenerationTasks, at the end of the same ServerChunkCache.runDistanceManagerUpdates that created it.

Dispatch, and why the parallelism is smaller than the thread names

ChunkMap builds two ConsecutiveExecutors over the shared worker pool, named worldgen and light, each wrapped in a ChunkTaskDispatcher. A ConsecutiveExecutor runs one task at a time: AbstractConsecutiveExecutor.run pops a single item, runs it under the executor’s name, and re-registers itself on the pool. The dispatcher in front of it is a ChunkTaskPriorityQueue of ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT buckets — 46, ChunkLevel.MAX_LEVEL plus two — keyed by the holder’s queue level, and ChunkTaskDispatcher.scheduleForExecution hands over one chunk’s runnables at a time, polling again only when they have all returned. So the steps a dimension runs inline run single file, however many Worker-Main-n threads the shared pool has (four threads worth memorising sizes it, and serialising onto a pool is what a ConsecutiveExecutor is for); what runs in parallel is what a step hands off: the disk read’s upgrade and parse and the biome and terrain forks on the pool, many chunks’ at once, the light work on its own executor, and FULL on the Server thread. There is no generation thread setting: the only knob is the pool’s, and widening it widens those forks and everything else that shares the pool.

One worldgen runnable executes at a time per dimension (ChunkMap.worldgenTaskDispatcher, over a single ConsecutiveExecutor).

Overlap comes from yielding. ChunkGenerationTask.runUntilWait returns the moment a layer holds a future that is not yet done; ChunkMap.runGenerationTask chains a resubmit onto that future and the executor moves to another chunk’s task at once. No worldgen thread ever blocks waiting for a neighbour, and a task parked on a biome fork costs nothing.

Priority is live, not fixed at submission. ChunkHolder.updateFutures ends by telling both dispatchers through ChunkTaskDispatcher.onLevelChange, and ChunkTaskPriorityQueue.resortChunkTasks moves work already queued into its new bucket — at a higher priority inside the dispatcher’s own four-slot queue than new submissions get, so “closer to a player runs first” stays true while the player is moving. ThrottlingChunkTaskDispatcher is a subclass of the same thing but is not worldgen: it caps how many player-view chunks the ticket tracker may have in flight, on the Server thread.

The EMPTY step asks the only question that changes the walk

ChunkGenerationTask.scheduleNextLayer always begins with ChunkStatus.EMPTY at the loading pyramid’s radius, which for a FULL target is 1. ChunkMap.applyStep special-cases that status: instead of a step body it runs ChunkMap.scheduleChunkLoad — the region read, ChunkMap.upgradeChunkTag on the pool under upgradeChunk, SerializableChunkData.parse on the pool under parseChunk, the POI file prefetched alongside through SectionStorage.prefetch, then SerializableChunkData.read on the server thread. What comes out is a ProtoChunk at whatever status the file recorded, an ImposterProtoChunk wrapping a real LevelChunk if the file was already at FULL, or ChunkMap.createEmptyChunk when there was no file. The futures are not done, so the task yields and is re-entered when they land (chunk storage).

A file that will not parse is regenerated, not skipped

There is a fourth outcome, and it rejoins the third. SerializableChunkData.parse returning null logs missing level data and empties the optional, so the step falls through to ChunkMap.createEmptyChunk exactly as though the file had never existed; and anything thrown along the way reaches ChunkMap.handleChunkLoadFailure on the Server thread, which re-throws a JVM Error as a crash report and otherwise reports the failure through MinecraftServer.reportChunkLoadFailure and hands back an empty chunk. Either way the empty proto chunk fails the load test below, so the walk generates, and the position is marked replaceable in ChunkMap.chunkTypeCache, which lets the saver write a half-made chunk over it — an unreadable chunk is regenerated, not skipped, and the old bytes stay on disk until something writes over them.

Load or generate, decided per chunk and per layer

Now ChunkGenerationTask.canLoadWithoutGeneration decides, and it asks two things. First, is the centre persisted at or past the target — for a FULL target, FULL. Then, separately, is every chunk in the loading pyramid’s accumulated square at or past what its distance demands: for FULL that square is the 3×3, wanting SPAWN at the centre and INITIALIZE_LIGHT on the ring, so the centre is checked twice and the second check is the weaker one.

If both hold, the walk stays narrow. Ten steps still run, but ChunkPyramid.LOADING_PYRAMID gives five of them no body at all — STRUCTURE_REFERENCES, BIOMES, TERRAIN, FEATURES and SPAWN pass straight through. Of the five that remain, EMPTY is the disk read ChunkMap.applyStep special-cases, and four carry a task: ChunkStatusTasks.loadStructureStarts, which only posts the saved starts to StructureCheck, the two light steps, and ChunkStatusTasks.full. A loaded chunk still walks all ten steps, and it still needs its 3×3 neighbours at INITIALIZE_LIGHT before its own LIGHT step will run.

If it does not hold, ChunkGenerationTask.needsGeneration goes true and EMPTY is scheduled a second time, now at radius 11 — reading only the chunks the first sweep did not touch. GenerationChunkHolder.applyStep runs GenerationChunkHolder.acquireStatusBump, a compare-and-set on GenerationChunkHolder.startedWork from a status’s parent to the status itself, so exactly one caller ever runs a step for a holder and every other caller is handed the existing future.

And the choice of pyramid is made again for every chunk in every layer, not once for the task. ChunkGenerationTask.scheduleChunkInLayer compares that chunk’s persisted status with the layer being applied and takes the generation pyramid only if the chunk is genuinely behind, so a generating task’s 23×23 square routinely mixes both — which is exactly what stops already-finished neighbours being generated a second time.

Two steps may write, and only two

ChunkStep’s default block-state write radius is −1, not 0 — so for eight of the ten steps WorldGenRegion.ensureCanWrite fails even for the chunk’s own column, and WorldGenRegion.setBlock logs and returns false rather than doing anything. Only TERRAIN (radius 0) and FEATURES (radius 1) can change a block at all. What rides on those steps — the density functions, the material rules, the carvers, the features and the structures they place — is Part XII’s subject (terrain, density functions, structure placement). This page is the conveyor.

STRUCTURE_STARTS runs ChunkGenerator.createStructures for every chunk in the radius-11 square that is not already past it — seed and placement state only, no terrain — and is skipped entirely when WorldOptions.generateStructures is off. Either way ServerLevel.onStructureStartsAvailable posts the chunk’s starts to the Server thread. STRUCTURE_REFERENCES then records, per chunk, which starts within eight chunks reach into it: the reason starts needed a radius of 8 around it. BIOMES forks — ChunkGenerator.createBiomes, which no generator overrides, puts the work on the pool under createBiomes — so biomes always leave the worldgen executor. TERRAIN forks only for NoiseBasedChunkGenerator, under buildTerrain — the noise fill, the surface and the carvers in one job — and then applies the BelowZeroRetrogen bedrock fix-ups if the chunk is being deepened and primes the four final heightmaps with Heightmap.primeHeightmaps; FlatLevelSource and DebugLevelSource return a completed future and stay inline. FEATURES decorates and calls Blender.generateBorderTicks (blending).

FEATURES is the interesting one, because a tree at a chunk edge writes into a neighbour and nothing about that neighbour’s status says it is safe. What makes it safe is the executor: the layer steps its chunks one at a time, and every step the dimension runs inline, decoration among them, is serialised behind the one ConsecutiveExecutor, so no two feature steps in a dimension are ever running at once. The ordinary cross-chunk write is the plain ChunkAccess.setBlockState, which takes and releases the section per write; only OreFeature holds a section open across many writes, through BulkSectionAccess (sections and their four counters).

A read too far crashes, a read too wide only warns

Both bad accesses are caught, and they are caught differently. A read outside the step’s ChunkStep.directDependencies — too far away, or at a status that distance does not guarantee — throws out of WorldGenRegion.getChunk as a crash report naming the step, the requested and actual statuses, the distance and the whole dependency list. A read that is merely outside the write zone is one line in the log, at error level, from WorldGenRegion.warnIfReadOutsideWriteZone, naming the feature through WorldGenRegion.currentlyGenerating. The first is a bug in the pyramid; the second is a bug in a feature, and the game keeps going.

Light runs on a second executor, and the task waits for it

ChunkStatusTasks.initializeLight calls ChunkAccess.initializeLightSources and ProtoChunk.setLightEngine — from that moment the proto chunk forwards block changes to the engine — and then hands the chunk to ThreadedLevelLightEngine.initializeLight. ChunkStatusTasks.light follows with ThreadedLevelLightEngine.lightChunk. Both queue through the light ChunkTaskDispatcher onto the light ConsecutiveExecutor, and the future each returns is completed by a later task on that same executor, so the generation task genuinely parks here (what actually kicks it).

Both are passed a lighted flag from ChunkStatusTasks.isLighted, and what it decides — that light saved on disk is re-enabled rather than recomputed — is told in lit before you ever see it.

FULL is assembled on the server thread

ChunkStatusTasks.full is scheduled exactly like the other nine steps, but its body is a supplyAsync on WorldGenContext.mainThreadExecutor — the ServerChunkCache.MainThreadExecutor, a BlockableEventLoop pinned to the Server thread. There are two shapes it can take. If the chunk is already an ImposterProtoChunk, because the file held a finished chunk, it unwraps the LevelChunk inside and replaces nothing. Otherwise a LevelChunk is built from the ProtoChunk, sharing its sections, and GenerationChunkHolder.replaceProtoChunk rewrites slots 0 through 8 of the holder’s future array to an ImposterProtoChunk over it with writes disallowed — every slot checked, and an exception thrown if any of them is not a ProtoChunk or was changed by another thread in the meantime, which GenerationChunkHolder.applyStep relays as a crash.

Then the chunk becomes part of the world, in order: LevelChunk.setFullStatus wired to the holder, LevelChunk.runPostLoad turning ProtoChunk.getEntities into real entities through ServerLevel.addWorldGenChunkEntities, LevelChunk.setLoaded, LevelChunk.registerAllBlockEntitiesAfterLevelLoad, LevelChunk.registerTickContainerInLevel and LevelChunk.setUnsavedListener. GenerationChunkHolder.completeFuture publishes it at FULL.

Nothing here crosses the network. A chunk reaches a client only after the separate promotion to FullChunkStatus.BLOCK_TICKING (which chunks a player is owed).

Release, and what the ring is left as

ChunkGenerationTask.runUntilWait comes round, finds the scheduled status equal to the target, and calls ChunkGenerationTask.releaseClaim: GenerationChunkHolder.removeTask on the centre, then ChunkMap.releaseGeneration on all 529. Every holder whose count reaches zero completes its save-sync future, and the square is free to be saved or dropped as its own tickets dictate — which the outer rings, only ever raised to the status their distance demanded, mostly are. ChunkHolder.scheduleFullChunkPromotion was called back in ChunkHolder.updateFutures, long before any of this; what happens now is its confirmation landing on the Server thread and ChunkMap.onFullChunkStatusChange tells the entity manager. ChunkLoadCounter watches this from outside — at start-up it holds the holders the restored saved tickets raise to FULL, and for a joining player those its spawn ticket raises, drops each as it arrives and feeds the load progress, and MinecraftServer.prepareLevels loops until none is left.

The whole walk, once

Everything above, spent once on one chunk that was not on disk. The lanes are the seven objects that hand it along, and the interesting thing about them is which thread each is standing on.

sequenceDiagram
    participant DM as DistanceManager
    participant CM as ChunkMap
    participant CTD as ChunkTaskDispatcher
    participant CGT as ChunkGenerationTask
    participant Worker as Worker
    participant TLE as ThreadedLevel<br/>LightEngine
    participant SL as ServerLevel

    Note over DM,SL: the Server thread, inside ServerChunkCache.runDistanceManagerUpdates
    DM->>CM: the holder reaches level 33 — updateHighestAllowedStatus, then updateFutures
    CM->>CGT: create — one task, and a claim on all 529 holders
    CM->>CTD: submit runUntilWait at the holder's queue level
    Note over CTD,CGT: thread hop — the worldgen ConsecutiveExecutor, one task at a time per dimension
    CTD->>CGT: runUntilWait, once the executor reaches this chunk
    CGT->>CM: layer EMPTY at radius 1 — applyStep becomes scheduleChunkLoad
    CM->>Worker: the pool's upgradeChunk and parseChunk, after the region read on the IO lane
    Worker->>CM: back on the Server thread, no file, so createEmptyChunk
    Note over CTD,CGT: the task yields on the first unfinished future, and is resubmitted
    CGT->>CGT: nothing on disk, so EMPTY again, now to radius 11
    CGT->>CM: STRUCTURE_STARTS to 11, then STRUCTURE_REFERENCES to 3
    CM->>SL: onStructureStartsAvailable, posted to the Server thread
    CGT->>Worker: BIOMES to 3 and TERRAIN to 2, forked to the pool
    CGT->>CM: FEATURES to 1, inline
    CGT->>TLE: INITIALIZE_LIGHT at 1, then LIGHT at 0, on the light executor
    CGT->>CM: SPAWN at 0, inline
    CGT->>SL: FULL, on the main-thread executor
    SL-->>CGT: the LevelChunk, built, wrapped, loaded and registered
    CGT->>CM: releaseGeneration on all 529, and the task is removed

One chunk from a level change to a live LevelChunk, handed between the worldgen executor the task runs on, the IO lane for the region read, the worker pool for its upgrade and parse stages and the two forks, the light executor, and the Server thread for the read’s result, the posted structure starts and ChunkStatus.FULL. The self-message in the middle is the load-or-generate decision: below it, the sweep to radius 11 and the structure, biome, terrain, feature and spawn generation are what a chunk already on disk would skip, and the structure-start replay, the light steps and FULL are what it would still run.

Questions players ask

Why does adding cores not speed up world generation? It speeds up only the part that forks. A dimension’s inline steps run on one ConsecutiveExecutor, one task at a time, and the dispatcher in front of it releases one chunk’s work at a time; what it forks onto the worker pool — the biome fill and the terrain job, the noise, the surface and the carvers — runs many chunks at once, and a wider pool runs more of it. The structure starts, the features and the spawns stay single file, and there is no thread-count setting for generation.

Why does a chunk I have visited before still take work to load? It walks all ten steps. Five of them pass through and cost nothing, but the disk read, the structure-start replay, both light steps and the FULL assembly are real work, and its LIGHT step needs the 3×3 neighbours read first.

Why does a chunk sometimes hang on the edge of the view forever? Its ticket level puts the ceiling below FULL. GenerationChunkHolder.isStatusDisallowed refuses anything higher, so the chunk sits at whatever its level allows — STRUCTURE_STARTS at the outer edge, INITIALIZE_LIGHT just outside the ring that reaches FULL — correct and unfinished, for as long as the level says so.

For a 1.21-era reader. The noise, surface and carvers statuses are gone: terrain does their work, as one step. The pool jobs init_biomes and wgen_fill_noise are now createBiomes and buildTerrain.

Where to look

The two lists that decide everything: ChunkPyramid.GENERATION_PYRAMID · ChunkPyramid.LOADING_PYRAMID · ChunkStep.getAccumulatedRadiusOf. Then one task’s life, in order: ChunkGenerationTask.create · ChunkGenerationTask.runUntilWait · ChunkGenerationTask.canLoadWithoutGeneration · ChunkGenerationTask.scheduleChunkInLayer. Where a step runs: ChunkMap.applyStep · GenerationChunkHolder.acquireStatusBump · ChunkStatusTasks.full. And what polices a running step: WorldGenRegion.getChunk · WorldGenRegion.ensureCanWrite.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Lighting

Verified against Minecraft 26.3 · Part IV · A torch is placed on a cave wall: the write queues a task, a worker floods the change, and the sections it touched are published as a copy and mailed to the client.

A player right-clicks a torch onto stone. The block goes into its LevelChunkSection immediately, the heightmaps move, and then LevelChunk.setBlockState reaches the light engine, for this torch, only to call LevelLightEngine.checkBlock, which on the server computes nothing at all. It wraps the call in a runnable and puts it on a queue the level tick never drains itself. There is no light thread and no light phase of the tick: the flood that turns the torch’s 14 into a sphere of falling numbers runs because the Server thread went looking for something to do, usually because the tick finished early. The kick is ThreadedLevelLightEngine.tryScheduleUpdate, and its usual caller is ServerChunkCache.MainThreadExecutor.pollTask, which the idle poll MinecraftServer.pollTaskInternal reaches while the server is ahead of its schedule, blocked or sprinting, and a synchronous chunk wait reaches in the middle of a tick. Ask the server engine to light something synchronously and it refuses: ThreadedLevelLightEngine.runLightUpdates throws.

The cast

classwhat it decidesthread
LevelLightEnginethe facade both sides hold — one LightEngine per layer, the read side, and the padding above and below the worldwhoever calls it
ThreadedLevelLightEnginethe server’s wrapper: every mutator becomes a queued task, and running updates on demand is an errorqueued from Server, drained on the light executor
BlockLightEngine · SkyLightEnginethe algorithm — what a changed block means, and how far the change travelsthe light executor (Client: the Render thread)
LayerLightSectionStoragethe double buffer: which sections hold data, which changed, and when to publishthe same
DataLayer2048 bytes of nibbles for one section, or no bytes at allwhoever holds it
ChunkSkyLightSourcesthe chunk’s 256-entry lowest sky source Y table — the one piece of light work that is not deferredServer, inside the block write
ServerChunkCachethe LightChunkGetter: which chunks the engine may read, and where the one callback landscallback on the light executor, body on Server
ChunkHolderwhich sections each watching player is owed, as two BitSetsServer

From a click to a lit wall

sequenceDiagram
    participant LC as LevelChunk
    participant TLE as ThreadedLevel<br/>LightEngine
    participant SCC as ServerChunkCache
    participant BLE as BlockLightEngine
    participant LLSS as LayerLight<br/>SectionStorage
    participant CH as ChunkHolder
    participant CL as ClientLevel

    Note over LC,CH: one server tick, inside LevelChunk.setBlockState
    LC->>LC: the light properties differ, so the column's sky-light source is repaired
    LC->>TLE: checkBlock, as a PRE_UPDATE task at the chunk's queue level
    Note over LC,SCC: still the same tick, and nothing has been lit
    SCC->>TLE: tryScheduleUpdate, because the poll found no chunk work
    Note over TLE,LLSS: the light executor, off the Server thread
    TLE->>BLE: the window's PRE tasks reach checkBlock, then runLightUpdates on this layer
    BLE->>BLE: checkNode enqueues a pull-in decrease and an emission increase of 14
    BLE->>LLSS: setStoredLevel, 14 then 13, 12 and down, as the increases propagate
    LLSS->>LLSS: a fresh copy of the updating map is published
    LLSS->>SCC: onLightUpdate once per affected section
    Note over SCC,CH: back on the Server thread, whenever the posted task is polled
    SCC->>CH: sectionLightChanged — it bails below INITIALIZE_LIGHT, and again with no ticking chunk
    Note over SCC,CH: end of ServerChunkCache.tickChunks, normally the next tick
    SCC->>CH: broadcastChanges, once the chunk tick is over
    CH->>CL: Clientbound<br/>LightUpdatePacket,<br/>to a player at the<br/>edge of the view
    Note over CL: the next frame, not the next tick
    CL->>CL: pollLightUpdates, applyLightData, then runLightUpdates on the client's own engine

A torch placed, and the four boundaries the server’s light crosses before it reaches a player at the edge of the view: the placement and the lighting are in the same tick but not on the same thread, the packet waits for the end of the chunk tick, and the client applies it on a frame rather than a tick. Nothing on this page blocks the Server thread on a light result.

Most sections below walk one stretch of that diagram. Two classes are absent because they carry no decision: ChunkTaskDispatcher, which holds the queued task under the chunk’s priority and hands it to the executor, and ClientPacketListener, which on arrival only wraps the packet in a closure for later.

Two 4-bit fields, and the sections that may not exist

Light is LightLayer.SKY and LightLayer.BLOCK, computed by separate machinery that shares only its shape. LevelLightEngine holds one LightEngine per layer — LevelLightEngine.skyEngine is null in a dimension whose DimensionType.hasSkyLight is false, and LevelLightEngine.getLayerListener hands out LayerLightEventListener.DummyLightLayerEventListener in its place. Readers rarely see the layers apart: LevelLightEngine.getRawBrightness is the maximum of block light and sky light minus a darkening term, which is what LevelReader.getMaxLocalRawBrightness passes LevelReader.getSkyDarken into, and BlockAndLightGetter.canSeeSky is just sky light at 15.

Storage is per layer and per section, one section taller than the world at each end — LevelLightEngine.LIGHT_SECTION_PADDING is 1 and LevelLightEngine.getLightSectionCount is the world’s sections plus two. A section’s storage is a DataLayer: 16×16×16 nibbles in DataLayer.SIZE bytes, indexed y then z then x, and allocated lazily. DataLayer.data starts null and every read answers DataLayer.defaultValue until the first write forces the array into existence, and DataLayer.fill throws it away again. DataLayer.isEmpty therefore means homogeneous zero, not all zeroes I checked, and it is the test both the chunk saver and the packet builder use to charge nothing for a dark section.

Which sections have a DataLayer at all is decided by LayerLightSectionStorage.sectionStates, one byte per section holding a has-data bit and a five-bit count of how many of its 26 neighbours have data (LayerLightSectionStorage.SectionState). A byte of zero means no storage, so a section of pure air beside a built-up one is allocated and a section in the middle of nothing is not. Above the sky column’s top section there is no storage at all, and SkyLightSectionStorage.getLightValue answers 15 without looking; its upward walk happens below the top, when a section inside the column has no DataLayer and the search climbs for one.

The write that queues nothing but a task

LevelChunk.setBlockState asks two questions about light. If the section flipped between all-air and not — the torch is the first block into an empty section, or the last one out — LevelLightEngine.updateSectionStatus goes in. Then, if LightEngine.hasDifferentLightProperties says the old and new states differ in emission or in dampening, or if either of them uses a shape for light occlusion, two things happen. The first is ChunkSkyLightSources.update on the chunk’s own table, and it runs inline, on the Server thread, inside the block write: the sky column’s lowest source Y for that one (x, z) is repaired immediately, because everything downstream reads it. The second is LevelLightEngine.checkBlock, and on the server that is where the work stops. ProtoChunk.setBlockState does the same pair, but only once the chunk’s status has reached ChunkStatus.INITIALIZE_LIGHT; before that a generator writing blocks tells the light engine nothing.

ThreadedLevelLightEngine overrides every mutator the same way. The call becomes a Runnable tagged ThreadedLevelLightEngine.TaskType.PRE_UPDATE or ThreadedLevelLightEngine.TaskType.POST_UPDATE and goes through ChunkMap.lightTaskDispatcher, at the chunk’s own queue level for the five mutators that belong to one chunk — LevelLightEngine.checkBlock, LightEventListener.propagateLightSources, LevelLightEngine.setLightEnabled, ThreadedLevelLightEngine.initializeLight and ThreadedLevelLightEngine.lightChunk — and at a flat top priority for bookkeeping such as LevelLightEngine.updateSectionStatus and LevelLightEngine.queueSectionData. When the dispatcher releases the task it runs on the light executor, not on the Server thread, and all it does there is append to ThreadedLevelLightEngine.lightTasks — a plain array list with no synchronisation, safe precisely because only that executor appends to it.

What actually kicks it

The light executor is a ConsecutiveExecutor named light, built in ChunkMap’s constructor over the shared background pool. It has no thread of its own: it takes one task from its queue, runs it on a borrowed pool thread, and re-registers itself. Two callers ever schedule a batch on it. ServerChunkCache.MainThreadExecutor.pollTask runs the distance-graph updates first and calls ThreadedLevelLightEngine.tryScheduleUpdate only if they had nothing to do — so light propagates in the Server thread’s spare moments, after the ticket system is quiescent (when the graphs run). Whichever caller gets there, ThreadedLevelLightEngine.scheduled, an AtomicBoolean, keeps exactly one batch in flight. The other caller is ChunkMap.scheduleUnload, kicking the engine right after it has queued the nulling of an unloading chunk’s data.

If nobody kicks, the queue is still not unbounded: ThreadedLevelLightEngine.addTask runs a batch inline as soon as ThreadedLevelLightEngine.lightTasks has reached a thousand, the number ThreadedLevelLightEngine.DEFAULT_BATCH_SIZE names — still on the light executor, still never on the Server thread.

One batch, and what it publishes

The light executor’s stretch of that trace is one batch, and a batch is defined by the two maps it sits between rather than by the flooding it does.

flowchart TD
    PRE["ThreadedLevelLightEngine.runUpdate takes a window of up to 1,000 tasks"]
    C["LightEngine.checkNode, on every queued position"]
    D["LightEngine.propagateDecreases, to an empty queue"]
    I["LightEngine.propagateIncreases, to an empty queue"]
    UP[("LayerLightSectionStorage.updatingSectionData: the scratch map, cloned per section")]
    S["LayerLightSectionStorage.markNewInconsistencies,<br/>then LayerLightSectionStorage.swapSectionMap"]
    VIS[("LayerLightSectionStorage.visibleSectionData: volatile, what every reader sees")]
    POST["the window's POST_UPDATE tasks run, and the window is dropped"]
    PRE --> C
    C --> D
    D --> I
    C -- "writes" --> UP
    D -- "writes" --> UP
    I -- "writes" --> UP
    I --> S
    UP -- "copied whole" --> S
    S --> VIS
    S --> POST

One batch, drawn for one of the two layers it runs in turn between the same PRE and POST tasks, and the two maps that layer is written between: the three stages down the left all write the same scratch map, and none of them writes the one another thread reads. That is why the picture has two cylinders and not one — and why a reader of the visible map gets that layer before the batch or after it, never a half-propagated flood.

The two maps are why no reader of light ever waits on the light engine. LayerLightSectionStorage.updatingSectionData is the engine’s scratch copy and no other thread reads it; LayerLightSectionStorage.visibleSectionData is volatile and is what every reader sees, and what the saver and the packet builder see wherever no newer layer is still queued. LayerLightSectionStorage.setStoredLevel clones a section’s DataLayer through DataLayerStorageMap.copyDataLayer the first time the batch touches that section, recording it in LayerLightSectionStorage.changedSections, and LayerLightSectionStorage.swapSectionMap copies the whole updating map into a new visible map at the end. A reader of the visible map on another thread sees each layer before the batch or after it, never a half-propagated flood.

Inside a layer the order is fixed. Every position from LightEngine.blockNodesToCheck goes through LightEngine.checkNode, which zeroes the stored level where the light that is there must go and enqueues the rest as work; then LightEngine.propagateDecreases runs the decrease queue to empty, then LightEngine.propagateIncreases runs the increase queue to empty. Decreases always finish first because a decrease discovers brighter neighbours and enqueues them as increase back toward me refills — running increases first would spread light that is about to be removed. Both queues hold pairs of longs whose second member is a LightEngine.QueueEntry: four bits of level, six of allowed directions, and the two flags LightEngine.QueueEntry.FLAG_FROM_EMPTY_SHAPE and LightEngine.QueueEntry.FLAG_INCREASE_FROM_EMISSION. The torch’s own entries are LightEngine.PULL_LIGHT_IN_ENTRY — a level-1 decrease in all six directions, meaning re-pull from my neighbours — and an emission increase of 14, a torch, Blocks.TORCH or Blocks.WALL_TORCH, being a BlockBehaviour.Properties.lightLevel of 14. BlockLightEngine.propagateIncrease then spreads level minus LightEngine.getOpacity, which is BlockBehaviour.BlockStateBase.getLightDampening — by default derived rather than declared: 15 for a solid render, 0 for a state that BlockBehaviour.BlockStateBase.propagatesSkylightDown, 1 otherwise, though leaves and tinted glass declare their own — floored at LightEngine.MIN_OPACITY. It stops where a neighbour is already as bright and where LightEngine.shapeOccludes. A level of 1 is still written; what it does not do is propagate, because only a new level above 1 is enqueued again. That is why a torch at 14 reaches thirteen blocks and no further.

The batch tells the chunk holders which sections changed only through LightChunkGetter.onLightUpdate, fired from the map swap. The engine has no idea that ChunkHolder exists.

The sky column is a table, not a flood

Sky light does not use the heightmaps, and it does not usually propagate downward one block at a time. Each chunk owns a ChunkSkyLightSources (ChunkAccess.skyLightSources), a bit-packed 256-entry table of the lowest Y at this (x, z) that still sees the sky, filled by ChunkSkyLightSources.fillFrom and repaired per block by ChunkSkyLightSources.update. What ends a column is ChunkSkyLightSources.isEdgeOccluded: the lower block dampens light at all, or the two faces occlude each other.

SkyLightEngine.checkNode reads that table and takes one of three paths. A block at or above the column’s lowest source enqueues a remove-source decrease and an add-source increase — the expensive case. A block below it that held light has that light zeroed and decreased away. A block below it that was already dark, which is what a torch under a solid ceiling is, gets a pull-in that changes nothing: placing a torch in a cave does no sky work worth measuring. Before any of the three, SkyLightEngine.updateSourcesInColumn runs SkyLightEngine.removeSourcesBelow and SkyLightEngine.addSourcesAbove so the stored 15s agree with the table again.

Two more pieces of the sky model exist because sections may have no storage. SkyLightEngine.propagateFromEmptySections handles light crossing a section edge sideways at the bottom row of a section: if the source column had no data for a run of sections below it, light had been continuing downward implicitly, so the same level is written straight down the destination column through that run. And SkyLightSectionStorage.createDataLayer seeds a newly needed section below existing data by repeating the bottom slice of the section above it (SkyLightSectionStorage.repeatFirstLayer) rather than starting dark.

Lit before you ever see it

Generation lights a chunk in two steps, and the first usually turns light off. ChunkStatusTasks.initializeLight builds the sky table with ChunkAccess.initializeLightSources, then calls ThreadedLevelLightEngine.initializeLight, whose PRE task marks every non-air section as having data and whose POST task sets the column’s enabled flag to whatever ChunkStatusTasks.isLighted says — false for a freshly generated chunk, whose persisted status has not reached ChunkStatus.LIGHT. Enabling is the next step’s doing: ChunkStatusTasks.light calls ThreadedLevelLightEngine.lightChunk, which for an unlit chunk runs LevelLightEngine.propagateLightSources, and both BlockLightEngine.propagateLightSources and SkyLightEngine.propagateLightSources open by enabling the column before seeding it — the block engine from LightChunk.findBlockLightSources, the sky engine from its own table and its four neighbours’. Only then does a POST task set ChunkAccess.setLightCorrect. A chunk read from disk with isLightOn set skips the propagation entirely, because ChunkStatusTasks.isLighted hands ThreadedLevelLightEngine.lightChunk a flag that says it is lit.

Two consequences are worth naming. ServerChunkCache.getChunkForLighting hands the engine chunks at ChunkStatus.FEATURES, one status below ChunkStatus.INITIALIZE_LIGHT — the engine reads chunks the rest of the game is not allowed to see yet. And nothing waits for light at the send itself: a chunk reaches ChunkStatus.FULL only after its own ChunkStatus.LIGHT task has run on the light executor, and ChunkPyramid.GENERATION_PYRAMID gives ChunkStatus.LIGHT a requirement of ChunkStatus.INITIALIZE_LIGHT at radius 1, so a chunk cannot climb to ChunkStatus.FULL until its neighbours have their sections marked too (the pyramid, drawn), and it is sent only from ChunkMap.prepareTickingChunk, which waits for all eight neighbours at ChunkStatus.FULL, so each of them has run its own ChunkStatus.LIGHT task as well. The one real send dependency, ChunkMap.waitForLightBeforeSending → ThreadedLevelLightEngine.waitForPendingTasks → ChunkHolder.addSendDependency, has exactly one caller: EnderDragonFight, holding back the send of the chunks around a new exit portal until its light is in.

And what unloading throws away

Unloading is the mirror: ChunkMap.scheduleUnload calls ThreadedLevelLightEngine.updateChunkStatus, which disables the column, drops the retain flag and nulls every section’s data (a chunk nobody needs any more). On load SerializableChunkData.read calls LevelLightEngine.retainData once and then LevelLightEngine.queueSectionData for each saved DataLayer.

Off the server thread and onto the wire

ServerChunkCache.onLightUpdate is the whole of the hop back. It is called on the light executor, and its body is a task posted to ServerChunkCache.mainThreadProcessor — no ChunkHolder is touched off-thread. When the Server thread later runs that task it finds the holder in the visible map and calls ChunkHolder.sectionLightChanged, which has two gates and does its one piece of bookkeeping between them. It gives up at once on a chunk not present at ChunkStatus.INITIALIZE_LIGHT; otherwise it marks the chunk unsaved (light is saved data); then it gives up again if there is no ticking chunk to broadcast for. Only past both, for a section in range whose bit is not already set, does it set one bit in ChunkHolder.skyChangedLightSectionFilter or ChunkHolder.blockChangedLightSectionFilter and answer yes, and the posted task then puts the holder in ServerChunkCache.chunkHoldersToBroadcast. So a chunk short of ChunkStatus.INITIALIZE_LIGHT is not even marked dirty by a light change, and one that is loaded but not ticking is marked dirty and never broadcast.

One packet, and only to the players who cannot work it out

The packet goes out at the end of ServerChunkCache.tickChunks — once a tick even while the tick rate is frozen, and never at all in a debug world, whose Level.isDebug guard wraps that whole method. ServerChunkCache.broadcastChangedChunks calls ChunkHolder.broadcastChanges, which builds one ClientboundLightUpdatePacket per chunk from the two filters and sends it before any block-change packet — but not to everyone watching. It asks ChunkHolder.PlayerProvider.getPlayers with borderOnly true, so ChunkMap.isChunkOnTrackedBorder keeps only the players for whom this chunk has an untracked neighbour. A player in the middle of their own view distance is never sent light for a block change: they were sent the block, they have every neighbouring chunk, and their own engine will reach the same numbers. The packet exists for the players who cannot, because the flood may be arriving from a chunk they do not have. Its ClientboundLightUpdatePacketData carries four bitsets — a data mask and an empty mask per layer — and the 2048 bytes of each non-empty changed section, read through LayerLightEventListener.getDataLayerData, which answers the queued layer if there is one and the visible map otherwise, never the updating copy. A section whose DataLayer is empty costs one bit; one with no DataLayer at all appears in neither mask. The same ClientboundLightUpdatePacketData rides inside ClientboundLevelChunkWithLightPacket with both filters null — every section — when PlayerChunkSender.sendChunk first sends a chunk.

Up to fourteen sections can be dirtied by one torch, and not because a write marks its neighbours. LayerLightSectionStorage.setStoredLevel marks only the sections within one block of the position written (SectionPos.aroundAndAtBlockPos): one for an interior block, up to eight for a block on a corner. The 14 comes from the flood, which writes as far as thirteen blocks away in taxicab distance. Grow that octahedron by the one-block halo and it can reach three sections deep on every axis, but never fills the 3×3×3 box of 27, whose outer sections need diagonals the octahedron does not have. Fourteen sections, across as many as seven chunk columns — and each of the fourteen is one the client will have to re-mesh. The real 3×3×3 marking, LayerLightSectionStorage.markSectionAndNeighborsAsAffected, fires only when a section is first given a DataLayer.

The client lights per frame

The client runs the same LevelLightEngine unwrapped: ClientChunkCache.lightEngine is a plain one, and its LightChunkGetter.onLightUpdate goes straight to LevelExtractor.setSectionDirty. It runs from ClientLevel.update, which Minecraft.renderFrame calls once a frame, not once a tick, so light converges at your framerate. ClientPacketListener.handleLightUpdatePacket applies nothing; it pushes a closure onto ClientLevel.lightUpdateQueue, and ClientLevel.pollLightUpdates runs a tenth of them per frame, at least ten, or, once a thousand are queued, all of them (what it does simulate: the two cadences owns that budget). Each closure is ClientPacketListener.applyLightData: ClientPacketListener.readSectionList turns every masked section into a cloned or empty DataLayer through LevelLightEngine.queueSectionData and dirties it with its neighbours, and then the chunk’s column is enabled with LevelLightEngine.setLightEnabled. Only after the polled closures does ClientLevel.update call LevelLightEngine.runLightUpdates, splicing the queued layers in and swapping the map exactly as the server does.

Enabling a column is a separate thing from lighting it, and on the client that distinction reaches all the way to the geometry: a section is not meshed at all until LevelLightEngine.lightOnInColumn is true for each of its eight surrounding columns, so a light flag decides whether a section may have a mesh (the sweep that only looks at what you can see). Meanwhile the client had already lit this torch itself: MultiPlayerGameMode.startPrediction runs the placement locally through the same LevelChunk.setBlockState, and for the player who placed it, in the middle of their own view, no light packet follows at all: their own engine’s numbers are the ones they see.

Questions players ask

Why does the server’s light reach some players a tick after the torch lands? Only a player at the edge of the view is sent light at all, and for them the write only queues a task, the queue waits for the Server thread’s spare moments, the callback is posted back to the Server thread, and the packet is built at the end of ServerChunkCache.tickChunks — four hand-offs, only the last of them on a schedule. Every watching client lights the torch itself when the block arrives; the packet brings what flooded in from chunks that player does not hold.

Why do the F3 light numbers not match what I see? They are three numbers, and none of them is the one you are looking at. DebugEntryLight prints a combined figure and then the two raw layers, and the combined figure is LevelLightEngine.getRawBrightness called with the time-of-day darkening argument set to zero — so even it is a noon answer. What renders is the two layers looked up in the Lightmap texture, which is where the hour, the dimension and the mob effects arrive (the lightmap).

For a 1.21-era reader. getLightBlock is now BlockBehaviour.BlockStateBase.getLightDampening, and LightTexture is Lightmap (Part XI). DynamicGraphMinFixedPoint, LeveledPriorityQueue and SpatialLongSet still live in world/level/lighting, but no light engine uses them any more: DynamicGraphMinFixedPoint survives for ChunkTracker (tickets) and SectionTracker (points of interest) and takes LeveledPriorityQueue with it, and SpatialLongSet has no callers at all.

Where to look

Follow the torch: LevelChunk.setBlockState · LightEngine.hasDifferentLightProperties · ThreadedLevelLightEngine.tryScheduleUpdate · ThreadedLevelLightEngine.runUpdate · BlockLightEngine.checkNode · BlockLightEngine.propagateIncrease · LayerLightSectionStorage.swapSectionMap. The sky’s own machinery is a separate read: ChunkSkyLightSources.update · SkyLightEngine.checkNode · SkyLightSectionStorage.getLightValue. Then the way out: ServerChunkCache.onLightUpdate · ChunkHolder.broadcastChanges · ClientPacketListener.applyLightData · ClientLevel.pollLightUpdates. Where generation turns light on and computes it: ThreadedLevelLightEngine.initializeLight · ThreadedLevelLightEngine.lightChunk.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Chunk storage

Verified against Minecraft 26.3 · Part IV · A chunk nobody needs any more is dropped from the world and written to disk, and the server thread never waits for it.

You walk away from your base. Soon after, the chunk you were standing in is no longer reachable from any ticket, its loading level climbs past ChunkLevel.MAX_LEVEL, and a queued task takes a snapshot of it, hands that to a worker to turn into NBT, and hands that to a lane that compresses it and finds it somewhere to live in r.X.Z.mca. Nothing about that is surprising. What is surprising is that the chunk was almost certainly written several times before you left, and that none of those writes was anybody’s idea. A chunk you keep changing is written by a background sweep roughly every ten seconds — ChunkMap.saveChunksEagerly, at most ChunkMap.CHUNK_SAVED_EAGERLY_PER_TICK (20) chunks a tick, only while fewer than ChunkMap.MAX_ACTIVE_CHUNK_WRITES (128) writes are in flight, each chunk no sooner than ChunkMap.EAGER_CHUNK_SAVE_COOLDOWN_IN_MILLIS (10 000 ms) after its last — and the autosave everyone thinks of as the save comes every five minutes of the server’s scheduled time, at any steady /tick rate. Almost every write of your world is one nobody asked for.

The cast

classwhat it decidesthread
ChunkMapwhich chunks are dirty, at which of four moments each is written, and whether a half-generated chunk may overwrite a finished one — it is the region/ store, because it extends SimpleRegionStorageServer
SerializableChunkDatathe chunk file as a record: what gets copied while the world is frozen and what gets encoded afterServer copies, a Worker-Main-n encodes
IOWorkerone store’s single lane, and the write-behind map that lets a read answer from a write that has not landedany IO-Worker-n, one task at a time
RegionFileStoragewhich r.X.Z.mca files are open — an LRU of RegionFileStorage.MAX_CACHE_SIZE (256)the IO lane
RegionFilethe sector allocator and the two header tables of one 32×32-chunk file, and the order the bytes land inthe IO lane
EntityStoragethe entities/ store: what a chunk’s mobs cost to write, and that they are rebuilt on the Server threadServer builds and parses, IO lane writes
SectionStoragethe poi/ store under PoiManager: which sections are dirty, and the one load that blocksServer
MinecraftServerwhen the next autosave falls, and whether every region file is opened with DSYNCServer

Copy on the server, encode on a worker, write on the IO lane

flowchart LR
    S["the Server thread:<br/>SerializableChunkData.copyOf"]:::server
    W["a worker:<br/>SerializableChunkData.write"]:::worker
    F["the IO lane, foreground:<br/>IOWorker.store"]:::disk
    B["the IO lane, background:<br/>IOWorker.storePendingChunk"]:::disk
    R["RegionFileStorage.write"]:::disk
    D[("r.X.Z.mca")]:::disk
    S --> W
    W --> F
    F --> B
    B --> R
    R --> D

One chunk saved, across the three lanes that touch it, coloured by which one: the server’s share for a full chunk is the first box alone, and it ends at a snapshot, so everything to the right of it happens after the tick that decided to save has moved on. The two middle boxes are the same lane at two priorities — the foreground task waits for the encode and parks the tag in IOWorker.pendingWrites, and the background one runs only when nothing is queued in front of it, which is why a write can sit there for a long time and cost the Server thread nothing.

That figure is the page’s answer to why doesn’t saving lag the server. Apart from flushing the chunk’s POI column and, for a chunk that is not full, the guard’s read, the Server thread’s whole share of a save is the middle of ChunkMap.save: SerializableChunkData.copyOf, which copies every LevelChunkSection with LevelChunkSection.copy and each non-empty block and sky DataLayer out of LevelLightEngine.getLayerListener, clones every heightmap the chunk holds, pulls block-entity NBT through ChunkAccess.getBlockEntityNbtForSaving, packs the ticks through ChunkAccess.getTicksForSerialization, and packs the structure starts. Everything after that — the palette codecs, the deflate, the sector arithmetic, the syscall — runs somewhere else, and ChunkMap.save returns as soon as the copy is done.

Four folders, three of them the same shape

LevelStorageSource.LevelStorageAccess.getDimensionPath defers to DimensionType.getStorageFolder, which puts every dimension — the overworld included — under dimensions/<namespace>/<path>/ in the world folder. Inside are region/, entities/, poi/ and data/. The first three are region stores of the same shape — a folder of r.X.Z.mca files, a RegionFileStorage, an IOWorker, and a RegionStorageInfo naming the store (chunk, entities or poi) so that MinecraftServer.reportChunkSaveFailure can say which one broke. Only chunk belongs to ChunkMap itself; EntityStorage and SectionStorage each hold a SimpleRegionStorage rather than being one. data/ is not a region store at all, and it is the section below.

A LevelChunk’s entities are not in region/. The SerializableChunkData.entities list is written only when the chunk’s persisted status is a ChunkType.PROTOCHUNK — worldgen’s spawns, waiting for the column to become full. A full chunk’s entities live in entities/, one region entry per chunk, holding a Position and an Entities list. If an old save still has entities inside a full chunk’s region/ entry, ServerLevel.addLegacyChunkEntities adopts them on load.

The other store under data/

SavedDataStorage is the fourth folder and the one that is not a region store: no sectors, no LRU of open files, one gzipped data/<namespace>/<path>.dat per thing that has state. It keeps the Server thread off the disk as a chunk does, and for the same reason. SavedDataStorage.scheduleSave encodes every dirty entry on the caller’s thread — the Server thread, inside the save it was asked for — and hands the finished tags to Util.ioPool, at most Util.maxAllowedExecutorThreads writes at a time, chaining each onto SavedDataStorage.pendingWriteFuture so that two saves of one file cannot race. SavedDataStorage.saveAndJoin is the only place anything waits, and only a flush save, shutdown or the world upgrader reaches it. Encode while the world is still, write while it moves: half the bargain the chunk path makes, over a much smaller object. Which file holds what belongs to level data and rules.

The four moments a chunk is written

the momentwhat runs itwhich chunkswhat holds it back
an unloadthe task ChunkMap.scheduleUnload queued, drained by ChunkMap.processUnloadsthe one chunk being dropped, at whatever status it reachedthe tick’s budget, except for the oldest tasks past 2,000 in ChunkMap.unloadQueue, which drain regardless; no cooldown
the eager sweepChunkMap.saveChunksEagerly, the last statement of that same ChunkMap.processUnloadseverything in ChunkMap.chunksToEagerlySave20 a tick, fewer than 128 writes outstanding, the tick’s time budget, and ten seconds per chunk
an autosaveMinecraftServer.autoSave → ServerLevel.save → ServerChunkCache.save → ChunkMap.saveAllChunks, without flushevery holder in ChunkMap.visibleChunkMaponly the per-chunk gates: ChunkMap.saveAllChunks clears ChunkMap.nextChunkSaveTime, but ChunkMap.saveChunkIfNeeded still wants an accessible, ready, unsaved LevelChunk (an ImposterProtoChunk passes its type test but is never unsaved)
a flush save/save-all flush, /stop, ServerChunkCache.closeevery accessible holder, over and over until a pass saves noneit blocks the Server thread instead

The dirty set behind the second row is narrower than it looks. ChunkMap.setChunkUnsaved is installed as WorldGenContext.unsavedListener and handed to a chunk by ChunkStatusTasks at the moment it becomes full, and LevelChunk.markUnsaved fires that listener only on the false→true edge. So ChunkMap.chunksToEagerlySave holds full chunks that have changed since their last write, each added once, and a chunk still being generated is never in it.

Turning saving off is not as total as it sounds. ChunkMap.tick ticks PoiManager first and unconditionally, and only then asks ServerLevel.noSave whether to run ChunkMap.processUnloads — so a no-save world still writes village data through SectionStorage.tick, and stops letting go of chunks until something forces the issue. The only other drain of ChunkMap.unloadQueue and ChunkMap.toDrop is inside ChunkMap.saveAllChunks with flush, which runs ChunkMap.processUnloads on an always-true budget — so /save-all flush unloads them, since MinecraftServer.saveAllChunks suppresses ServerLevel.noSave when force is set. Shutdown needs neither: it clears ServerLevel.noSave on every level, which reopens the tick’s drain, and ticks until ChunkMap.hasWork is false before its flush save. An explicit save is a different question again: MinecraftServer.saveAllChunks passes ServerLevel.noSave on to ServerLevel.save only when its force flag is clear, and /save-all sets that flag while MinecraftServer.autoSave does not.

The guard against writing a half-made chunk over a finished one, and where it leaks

The cast row promises this one, so here it is: ChunkMap.save refuses to write a non-full chunk over a full one on disk, and the refusal is best effort in two ways. ChunkMap.isExistingChunkFull returns false — meaning go ahead — whenever the read throws or comes back empty, so an IO error licenses exactly the clobber the guard exists to prevent. And ChunkAccess.tryMarkSaved clears the unsaved flag before the guards run, so a chunk the guard turns away has already been marked clean and will not be offered again.

Two shapes never reach the write at all. A proto chunk still at ChunkStatus.EMPTY with no valid structure start is dropped by the same block, and an ImposterProtoChunk refuses outright: ImposterProtoChunk.tryMarkSaved and ImposterProtoChunk.canBeSerialized are flat falses (so is ImposterProtoChunk.isUnsaved), where ImposterProtoChunk.markUnsaved, ImposterProtoChunk.isLightCorrect and ImposterProtoChunk.setLightCorrect defer to the chunk underneath (chunk anatomy). The imposter’s reason is that the LevelChunk under it is what the saver will be handed instead.

A chunk nobody needs any more

The save above is the middle of a longer story, which ordinarily runs inside one tick and stretches over several only when the budget runs out or a save dependency is pending. This is the whole of it, from the level change that drops the chunk to the write that finally records it.

sequenceDiagram
    participant DM as DistanceManager
    participant CM as ChunkMap
    participant CH as ChunkHolder
    participant SCD as SerializableChunkData
    participant IOW as IOWorker
    participant SL as ServerLevel
    participant PESM as PersistentEntity<br/>SectionManager

    DM->>CM: the level climbs past ChunkLevel.MAX_LEVEL, so the key joins ChunkMap.toDrop
    Note over CM,CH: the unload phase of the same ServerChunkCache.tick
    CM->>CM: processUnloads moves<br/>the holder to<br/>ChunkMap.<br/>pendingUnloads
    CM->>CH: getSaveSyncFuture, and the unload task is hung off it
    CH-->>CM: the future is complete, or completes later, and the task joins unloadQueue
    Note over CM,CH: the same drain while the budget says yes, or a later one, or among the oldest past 2,000
    CM->>CH: getSaveSyncFuture again — the same one, or the task rearms and stops
    CM->>CM: this exact holder<br/>leaves ChunkMap.<br/>pendingUnloads, or<br/>a ticket took it back
    CM->>CM: setLoaded false, then save — PoiManager.flush and the proto-over-full guard
    CM->>SCD: copyOf takes the snapshot, a worker turns it into a CompoundTag
    CM->>IOW: store, handed the encode future, on the chunk lane
    CM->>SL: unload clears the block entities and the tick containers
    Note over SL,PESM: in a walk-away, the entities left rings earlier, when the chunk fell below FULL
    IOW-->>CM: the IOWorker.PendingStore future completes, one fewer active write

A chunk nobody needs, from the level that stopped needing it to the write that records it, ordinarily inside one tick: the two note bars mark where the budget or a pending save can push the rest to a later one, and the two dotted arrows are completions the Server thread does not wait for. The entities are not in it — in a walk-away they left by a different road, earlier, when the chunk fell below full.

Three things there are load-bearing. The first is that nothing happens until ChunkHolder.saveSync is done: every promotion future is chained into it by ChunkHolder.addSaveDependency, and so is GenerationChunkHolder.generationSaveSyncFuture for as long as a generation step holds a reference, so a chunk mid-promotion or mid-generation cannot be saved or unloaded at all.

The second is the guard. ChunkMap.pendingUnloads is removed by identity: if a ticket re-adopted the position while the task waited, ChunkMap.updateChunkScheduling has already pulled the holder back out of that map, the removal fails, and the task quietly does nothing — nothing is lost and nothing is written twice. And if the sync future changed while waiting, the task rearms itself on the new one rather than proceeding.

The third is that entities go by a different road, and earlier. PersistentEntitySectionManager.updateChunkStatus saw the chunk fall below full, rings before it was dropped, and queued the position in PersistentEntitySectionManager.chunksToUnload. If the chunk’s entity file is still being read, PersistentEntitySectionManager.storeChunkSections returns false and the whole thing is retried next tick, so a half-loaded set never clobbers the file. Otherwise each entity EntityAccess.shouldBeSaved accepts is serialised with Entity.save on the Server thread, its passengers inside it, the tag goes to the entities lane, and those entities and their passengers are removed with Entity.RemovalReason.UNLOADED_TO_CHUNK. The three types besides Player built never to be saved — a leash knot, a lightning bolt, a fishing bobber — pass the filter, write nothing in Entity.save, and are removed with the rest. The filter runs before the removal, not after it, so what it turns away on its own account — a Player, a vehicle carrying exactly one player — is neither written nor removed. The last of those is not an oddity: ServerPlayer writes its own root vehicle into the player file under RootVehicle under exactly that condition, so a boat with one rider travels in the player’s file rather than the chunk’s.

Two other things leave with the chunk, both after the snapshot is taken: ServerLevel.unload clears its block entities and unregisters its tick containers, and ThreadedLevelLightEngine.updateChunkStatus queues the light engine to forget its layers (lit before you ever see it).

Why the server thread never waits

IOWorker is not a thread. It holds a PriorityConsecutiveExecutor over Util.ioPool — a cached pool whose threads are named IO-Worker-n — and its guarantee is that one task at a time runs for that store, not that the same thread runs them. Its three priorities are strictly ordered: IOWorker.Priority.FOREGROUND for IOWorker.store and IOWorker.loadAsync, IOWorker.Priority.BACKGROUND for IOWorker.storePendingChunk — the task that touches the disk — and IOWorker.Priority.SHUTDOWN last. The lowest priority has exactly one user in the whole game: the barrier IOWorker.waitForShutdown parks behind everything else when the store closes. A flush is not one of them — IOWorker.synchronize submits it at foreground priority like a store — but it still lands behind the writes, because before it flushes it waits on every IOWorker.PendingStore future, and those complete only when the background tasks have run.

IOWorker.pendingWrites is what that buys. It is a sequenced map from ChunkPos to IOWorker.PendingStore, and a second store for a position already in it overwrites that entry’s data in place without moving it, so N saves of one chunk before the lane drains become one disk write and one shared future. IOWorker.loadAsync looks in the same map first and returns a copy of the pending tag, so a chunk re-loaded while its write is still pending never touches the region file — read-your-writes by lane order rather than by any lock. IOWorker.STORE_EMPTY is the null supplier that means delete, and IOWorker.scanChunk is the streaming ChunkScanAccess that StructureCheck uses to peek into chunks nobody has loaded.

The three places that do wait

Three places besides a synchronous chunk load and a flush save make the Server thread wait on a disk. ChunkMap.isExistingChunkFull, the guard that stops a ProtoChunk overwriting a finished chunk, answers from ChunkMap.chunkTypeCache when it can but joins the read future inline on a cold entry — the IO lane, then a datafix pass on the worker pool. And SectionStorage.getOrLoad joins too, for a POI section that SectionStorage.prefetch has not fetched. The third is not a chunk-storage method at all: StructureCheck.tryLoadFromStorage joins IOWorker.scanChunk to peek at a chunk it will not load, which is what an eye of ender, a dolphin, an explorer map and /locate all end up doing on the Server thread. Only the first is on the save path, and only for a chunk that is not full, which is why a full chunk’s save costs a copy.

Inside a region file

The last box of the first figure is a file format, and it is where the ordering that makes a half-finished save survivable lives.

flowchart TD
    A["IOWorker.storePendingChunk pops the oldest entry of IOWorker.pendingWrites"] --> B["RegionFileStorage.getRegionFile, an LRU of 256 regions"]
    B --> C["RegionFile.getChunkDataOutputStream wraps a RegionFile.ChunkBuffer in the compressor, NbtIo writes in"]
    C --> D["closing the buffer back-patches the length and calls RegionFile.write"]
    D --> E{"how many sectors"}
    E -- "under 256 sectors" --> F1["RegionBitmap.allocate takes the first free run"]
    F1 --> F2["the compressed chunk goes to those new sectors — the content, first"]
    F2 --> F3["offsets and timestamps updated, then RegionFile.writeHeader"]
    F3 --> F4["any stale sidecar for this chunk is deleted"]
    F4 --> Z["and only now are the old sectors freed"]
    E -- "256 sectors or more" --> G1["one sector is allocated for a stub"]
    G1 --> G2["a five-byte EXTERNAL_STREAM_FLAG stub, and the payload to a temp file"]
    G2 --> G3["offsets and timestamps updated, then RegionFile.writeHeader"]
    G3 --> G4["the temp file is moved onto c.X.Z.mcc — the content, last"]
    G4 --> Z

Two ways to write one chunk, putting the content and the pointer to it in opposite orders: on the left the bytes are on disk before the header that names them, and on the right the header is written first, pointing at a stub, and the real payload only lands when the temp file is moved. Both arms meet at the last box, which is the rule that makes either safe: the old sectors are not freed until the new ones can be found.

A RegionFile is one r.X.Z.mca: two header sectors (RegionFile.SECTOR_BYTES is 4096) holding a 1024-entry offset table, RegionFile.offsets, packed as sector number ≪ 8 with the sector count in the low byte, and a 1024-entry RegionFile.timestamps. Free space is a RegionBitmap, with the header’s two sectors forced used at construction and RegionBitmap.allocate handing out the first run big enough. Nothing ever reads the timestamp table back: RegionFile.write stamps each entry with epoch seconds from RegionFile.getTimestamp, and the only clock that spaces one chunk’s writes is the monotonic Util.getMillis behind ChunkMap.nextChunkSaveTime, while the autosave counts server ticks. None of them is game time, which the chunk carries only as data. Each stored chunk starts with RegionFile.CHUNK_HEADER_SIZE (5) bytes — a length and a compression id — and both RegionFile.write and RegionFile.getChunkDataInputStream are synchronised on the RegionFile itself rather than on the channel, though in practice only one lane ever drives a given folder.

Read the two branches of the figure against each other and the page’s best fact falls out. For an ordinary chunk the new bytes are on disk before the header points at them, and the old bytes are released after — so a crash at any point leaves either the old chunk or the new one, and a chunk never overwrites itself in place. For an oversized chunk the ordering is reversed. Anything needing RegionFile.EXTERNAL_CHUNK_THRESHOLD (256) sectors or more cannot be described by an eight-bit count field at all, so it goes to a .mcc sidecar and the region file keeps only a stub carrying RegionFile.EXTERNAL_STREAM_FLAG; and the sidecar is moved into place after RegionFile.writeHeader has already committed the pointer to it, destroying the previous copy at a fixed path. The in-file case is content-then-pointer. The sidecar case is pointer-then-content. Either way the ordering only buys anything if the writes reach the platter in that order, which is what DSYNC is for: MinecraftServer.forceSynchronousWrites returns true as the base default, and two of the three servers override it — DedicatedServer from DedicatedServerProperties.syncChunkWrites (sync-chunk-writes, default true) and IntegratedServer from Options.syncWrites, whose default is true only on Windows. GameTestServer keeps the base answer.

The compression byte is per chunk, not per file. RegionFileVersion.selected — set once by RegionFileVersion.configure from DedicatedServerProperties.regionFileComression (Mojang’s spelling; the property is region-file-compression) — decides only what new writes use, choosing between RegionFileVersion.VERSION_DEFLATE (the RegionFileVersion.DEFAULT), RegionFileVersion.VERSION_NONE and RegionFileVersion.VERSION_LZ4. Reads honour whatever byte each chunk carries, including RegionFileVersion.VERSION_GZIP, which has no option name and so can be read but never chosen, and RegionFileVersion.VERSION_CUSTOM, which exists so that RegionFile.createChunkInputStream can recognise it and refuse.

The way back in

Loading is the same road driven backwards, in four stages across three lanes. ChunkMap.scheduleChunkLoad starts with IOWorker.loadAsync on the IO lane; ChunkMap.readChunk then hops to Util.backgroundExecutor under the name upgradeChunk for ChunkMap.upgradeChunkTag, which is where datafixing happens; SerializableChunkData.parse runs on the same pool under parseChunk, so those two stages share a lane; and SerializableChunkData.read runs on the Server thread, where the sections are installed, the saved light is queued into the light engine, and PoiManager.checkConsistencyWithBlocks re-derives the points of interest of any section whose stored record is missing or not marked valid. Running beside all of it, SectionStorage.prefetch pulls the POI file in, and the two are joined before the server-thread step — which is exactly why that step’s SectionStorage.getOrLoad calls do not block. From there the generation pipeline takes over — including what happens when the bytes will not parse.

Entities come back the same shape but land differently: EntityStorage schedules both the datafix and EntityType.loadEntitiesRecursive on EntityStorage.entityDeserializerQueue, a ConsecutiveExecutor over the server main-thread executor, so only the NBT read is off-thread.

Doing all of it at once, with no server running

Optimize World in the world-select screen is the same read and the same write with the game in between removed. WorldUpgrader starts a single daemon thread, World Upgrader #n, and hands each of the three stores to a RegionStorageUpgrader, which walks every r.X.Z.mca file in the folder, datafixes each chunk tag and writes back the ones that changed — or every one, into fresh region files, which is what compacts a save whose sectors have fragmented, though only the dedicated server’s –recreateRegionFiles asks for that and the button never does. UpgradeProgress is the counter the screen reads. Nothing here loads a chunk, generates one, or consults a status: the world is a folder of tags, and the button’s whole promise is that every tag is at the current data version before a server ever opens the save.

Questions players ask

Does the game stall when it saves? The chunk stores wait for the disk only on a flush, though an autosave still copies every unsaved chunk, serialises the loaded entities, and writes level.dat and every player’s file itself, on the Server thread, in the tick it falls. ChunkMap.saveAllChunks with flush loops over the accessible holders, blocking the main-thread executor on each ChunkHolder.isReadyForSaving until a whole pass saves nothing, then flushes POIs with SectionStorage.flushAll, runs ChunkMap.processUnloads with an always-true budget, and finally joins IOWorker.synchronize with flush. That, with the entity store’s and the saved data’s own joins in the same save, is where waiting for the disk is the point rather than an accident, and it is what /save-all flush and /stop do.

Does lowering the tick rate push out my autosave? Only the one already counting down, and never past five minutes. The interval is counted in ticks: MinecraftServer.computeNextAutosaveInterval is the tick rate times 300 — or, while the server is sprinting, 300 times the rate its recent tick times imply — floored at MinecraftServer.MIMINUM_AUTOSAVE_TICKS (100, and the typo is Mojang’s), and the very first is MinecraftServer.AUTOSAVE_INTERVAL (6000 ticks), so each autosave is five minutes of the server’s scheduled time at any rate. MinecraftServer.onTickRateChanged recomputes it on every /tick rate but assigns it only when it is smaller than the ticks left: a slower rate stretches those ticks, up to five minutes from the change, and a faster one can only bring the save forward. The server tick has the rest of that loop.

Why is my entities/ folder full of files with nothing in them? Because nothing deletes a region file once its entries are cleared — and emptying a chunk costs one write. EntityStorage.storeEntities with an empty set only writes when EntityStorage.emptyChunks did not already contain the position, and that write is IOWorker.STORE_EMPTY, which zeroes the region entry and deletes any sidecar. The first time a chunk goes empty costs a write. Every later save of it costs nothing.

Where to look

ChunkMap.tick · ChunkMap.processUnloads · ChunkMap.scheduleUnload · ChunkMap.save · ChunkMap.saveChunksEagerly · ChunkMap.saveChunkIfNeeded · ChunkMap.saveAllChunks · SerializableChunkData.copyOf · SerializableChunkData.write · IOWorker.store · IOWorker.storePendingChunk · IOWorker.loadAsync · RegionFileStorage.write · RegionFile.write · RegionBitmap.allocate · PersistentEntitySectionManager.storeChunkSections · EntityStorage.storeEntities · SectionStorage.writeChunk · SectionStorage.prefetch · ChunkMap.scheduleChunkLoad · SerializableChunkData.read · MinecraftServer.computeNextAutosaveInterval · DimensionType.getStorageFolder

Next door: tickets and loading raises the level, chunk anatomy owns what LevelChunkSection.copy copies, lighting owns the layers the unload throws away, points of interest owns the poi/ store, the server tick owns the budget every method here is handed, how a server dies is the save that does not happen, and entity lifecycle is what Entity.RemovalReason.UNLOADED_TO_CHUNK means to a mob.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Scheduled ticks

Verified against Minecraft 26.3 · Part IV · A repeater’s input goes high, and the two ticks before its output follows are one entry in a queue.

A repeater set to its shortest delay is not counting anything. When the wire behind it changes, DiodeBlock.neighborChanged, which RepeaterBlock inherits, runs on the Server thread, notices that DiodeBlock.POWERED no longer matches the input, and books an appointment — a ScheduledTick naming this Block at this BlockPos, due two game ticks from now, at TickPriority.HIGH — and then forgets. Two ticks later a scheduler the repeater has never heard of hands the position back to ServerLevel.tickBlock, which checks that a repeater is still there and calls the block again. Nearly everything the world does later is one of these: a fluid flowing, a pressure plate releasing, a dispenser firing, a redstone torch burning out. They all share two queues per chunk and one rule that surprises everybody: the queue dedups on type and position alone, so a second tick for the same block — even a much sooner one — is silently dropped.

“Rescheduling moves the tick” is folklore. ScheduledTick.UNIQUE_TICK_HASH hashes the position and compares the type by object identity — the trigger tick, the priority and the sub-order are no part of it — and LevelChunkTicks.schedule queues a tick only if its dedup set, LevelChunkTicks.ticksPerPosition, did not already hold that pair. The booking that loses is not queued, not merged and not logged, which is why so many blocks ask before they book: DiodeBlock, ComparatorBlock and RedstoneTorchBlock consult LevelTickAccess.willTickThisTick, and seven blocks — ObserverBlock, TargetBlock, LightningRodBlock, TripWireBlock, SculkSensorBlock, DriedGhastBlock and SpeleothemBlock — consult TickAccess.hasScheduledTick.

The cast

classwhat it decidesthread
ScheduledTickthe appointment itself — type, position, trigger tick, TickPriority, sub-order — and the comparisons the system sorts and dedups bya record, no thread
LevelChunkTicksone chunk’s queue and its dedup set: whether a booking is new, and which of its ticks is nextServer
LevelTicksthe per-level scheduler: which chunks are due, which ticks run this level tick, and where the budget fallsServer
ScheduledTickAccessthe write side every block sees, so no block knows which container it is booking intoany — the worldgen executor books through it
ServerLevelwhen the drain runs, the per-chunk gate it runs under, and the type re-check that makes a tick cancellableServer
SavedTickthe disk form: a relative delay in place of an absolute timeServer, written by the IO worker
ProtoChunkTicksa generating chunk’s bookings, all at delay zerothe worldgen executor
BlackholeTickAccessaccept every booking and run nothing — the client’s answer to the whole system, and also an ImposterProtoChunk’sRender, and the worldgen executor

Those containers implement one small interface stack — TickAccess (schedule, ask, count), TickContainerAccess per chunk, LevelTickAccess per level, which also answers LevelTickAccess.willTickThisTick, and SerializableTickContainer for one that can SerializableTickContainer.pack itself for disk — which is why LiquidBlock and RepeaterBlock run unchanged during generation, on a client that will never tick them, and on a server that will.

The pipeline, end to end

The pipeline has a seam in it, booking and the drain, so it is two figures. This is booking into a live level; the worldgen executor books into a generating chunk’s own list instead (below).

flowchart TD
    B["a block books: ScheduledTickAccess.scheduleTick"]
    C["LevelAccessor.createTick — game time plus delay, a TickPriority, a sub-order"]
    S{"LevelTicks.schedule looks for the chunk's container"}
    DROP["Util.logAndPauseIfInIde — logged and dropped, never deferred"]
    D{"LevelChunkTicks.ticksPerPosition already holds this type and position"}
    SKIP["dropped in silence, even when this one is sooner"]
    Q[("one priority queue per chunk, ordered by ScheduledTick.DRAIN_ORDER")]
    B --> C
    C --> S
    S -- "no container" --> DROP
    S -- "found" --> D
    D -- "yes" --> SKIP
    D -- "no" --> Q

Everything that can happen to an appointment booked into a live level before it is a queued tick — and two of the three outcomes are that it is not one. The right-hand refusal is the page’s subject: the dedup slot is checked against the type and position alone, so the second booking is thrown away whether or not it is due sooner than the one already there.

The drain is the other half, it runs once per level tick per type, and it never leaves the Server thread.

flowchart TD
    IDX[("LevelTicks.nextTickForContainer: one entry per container, the head's time")]
    SC{"LevelTicks.sortContainersToTick asks LevelTicks.tickCheck"}
    DR["LevelTicks.drainContainers, while the budget lasts"]
    RUN["LevelTicks.runCollectedTicks, to ServerLevel.tickBlock or ServerLevel.tickFluid"]
    CL["LevelTicks.cleanupAfterTick empties all four working collections"]
    AGAIN["a block books again, from inside its own run"]
    IDX --> SC
    SC -- "the chunk is not ticking" --> IDX
    SC -- "due, and ticking" --> DR
    DR -- "overtaken, budget left" --> DR
    DR -- "not due, or the budget spent" --> IDX
    DR --> RUN
    RUN --> CL
    RUN --> AGAIN

The three phases of one drain, and the ways back to the index, none of them a failure: a container whose next tick is not yet due waits for it, and one whose chunk is not ticking or whose budget ran out is late, never lost. The bottom-right box is the loop this page’s repeater lives in — the run books the next appointment, which re-enters the figure above.

Everything below is one stage of those two figures.

Booking: a type, a position, a time and a tie-breaker

A block calls one of the ScheduledTickAccess.scheduleTick defaults with a delay in ticks and, optionally, a TickPriority. The default asks the level for LevelAccessor.createTick, which stamps the appointment with LevelAccessor.getGameTime plus the delay, the priority (TickPriority.NORMAL if none was given) and a fresh sub-order from LevelAccessor.nextSubTickCount, then hands it to ScheduledTickAccess.getBlockTicks or ScheduledTickAccess.getFluidTicks. Two type arguments, two parallel worlds: every class here — LevelTicks, LevelChunkTicks, ScheduledTick — declares one parameter, and it is filled with Block in one world and Fluid in the other, so a block tick and a fluid tick never meet and ServerLevel owns one LevelTicks of each.

The sub-order is the FIFO tie-breaker for two ticks at the same time and priority, and it carries the one threading fact of this page. Level.subTickCount is a plain counter incremented by Level.nextSubTickCount, because a level’s scheduler is touched only from the Server thread; WorldGenRegion.subTickCount is an atomic one, because generation books ticks off the Server thread, on the worldgen executor. The drain is server-thread only. Booking is not.

TickPriority runs TickPriority.EXTREMELY_HIGH (−3) through TickPriority.NORMAL (0) to TickPriority.EXTREMELY_LOW (3), lower first, and it is not a queue-jump across time: a TickPriority.LOW tick due now still beats a TickPriority.EXTREMELY_HIGH tick due next tick. Priority only settles ties between ticks already due together.

Fluids are the scheduler’s largest customer by a distance, and they exploit the dedup key: Fluids.WATER and Fluids.FLOWING_WATER are different registry objects, so one tick of each can be pending at one position. What those ticks then do is fluids.

Where an appointment waits

Every LevelChunk owns exactly two containers, LevelChunk.blockTicks and LevelChunk.fluidTicks, and they are the only place a live chunk’s tick waits until a drain collects it (the four shapes a chunk takes): a priority queue, LevelChunkTicks.tickQueue, in ScheduledTick.DRAIN_ORDER, beside the dedup set that decides what gets into it.

LevelTicks never scans those queues looking for work. It keeps LevelTicks.allContainers, chunk key to container, and LevelTicks.nextTickForContainer, chunk key to the earliest trigger time that chunk holds, defaulting for an unknown chunk to the largest possible long. The index is maintained by LevelTicks.chunkScheduleUpdater, the callback every container is handed through LevelChunkTicks.setOnTickAdded when it registers, and it acts only when the tick just added is the container’s new head. A chunk with nothing due costs one map entry and one comparison per level tick, and a chunk with an empty queue costs nothing at all — which is what makes tens of thousands of loaded chunks affordable.

Registration follows the chunk’s life exactly: ChunkStatusTasks.full calls LevelChunk.registerTickContainerInLevel (LevelTicks.addContainer, both types), and ServerLevel.unload calls LevelChunk.unregisterTickContainerFromLevel → LevelTicks.removeContainer, which also drops the callback so an orphaned container can no longer touch the index. In between, a tick aimed at a chunk with no registered container is neither deferred nor queued: LevelTicks.schedule finds nothing, calls Util.logAndPauseIfInIde, and the appointment ceases to exist. ClientLevel short-circuits all of it — ClientLevel.getBlockTicks and ClientLevel.getFluidTicks return BlackholeTickAccess.emptyLevelList, which accepts every booking, answers false to every question and runs nothing, so everything a client sees of a flowing fluid or a firing repeater arrives as block-update packets. (ContainerSingleItem also lives in world/ticks, a one-slot inventory interface with nothing to do with ticks — a packaging accident, noted only so it does not confuse you.)

What one drain actually does

ServerLevel.tick calls LevelTicks.tick twice in its tickPending section, blocks first and then fluids, each with the current game time and a budget of ServerLevel.MAX_SCHEDULED_TICKS_PER_TICK, 65536 — a budget per call, so 65536 block ticks and 65536 fluid ticks (the level tick). The whole section is skipped in a debug world and whenever TickRateManager.runsNormally is false. Each call is three phases.

Collect. LevelTicks.sortContainersToTick walks the index, not the containers: it leaves a future entry alone, deletes one whose container has vanished or emptied, corrects one whose head is later than the index claimed, and tests a genuinely due one against LevelTicks.tickCheck. That predicate is ServerLevel.isPositionTickingWithEntitiesLoaded, asked about the chunk, not the position, and true only when the chunk is in DistanceManager.inBlockTickingRange, its ChunkHolder.getTickingChunkFuture has already succeeded, and PersistentEntitySectionManager.areEntitiesLoaded holds (two graphs, one store). A chunk that fails keeps its index entry untouched and is asked again next tick — its ticks are late, never lost — and one that passes moves into LevelTicks.containersToTick, a priority queue of containers ordered by LevelTicks.CONTAINER_DRAIN_ORDER, on their heads.

LevelTicks.drainContainers polls the best container, takes one tick and hands to LevelTicks.drainFromCurrentContainer, which keeps pulling from that same container while its next tick is still due and still beats the next-best container’s head — containers are re-heaped only when the winner stops winning. Where a container goes next turns on the budget as much as on its own head. Overtaken but still due, with budget left: back into the container queue, to be polled again this tick. Head no longer due: back to the index at its head’s time. Still due but the budget is spent: back to the index, and asked again next tick. Drained empty: neither. Then LevelTicks.rescheduleLeftoverContainers runs over whatever is still sitting in the container queue when the drain stopped and returns each to the index at its head’s trigger time, already in the past — which gets it collected next tick but buys it no place in the order, because LevelTicks.CONTAINER_DRAIN_ORDER compares priority and sub-order and has no time term at all.

Run. LevelTicks.runCollectedTicks drains LevelTicks.toRunThisTick in order, moving each entry to LevelTicks.alreadyRunThisTick and handing its position and type to ServerLevel.tickBlock or ServerLevel.tickFluid. Both re-read the world there and run BlockBehaviour.BlockStateBase.tick or FluidState.tick only if the block or fluid is still the one the appointment named. A tick is a promise to a type, and that check is the whole of cancellation for anything a block does: break the block and its pending ticks evaporate with no cancellation code anywhere. The only code that removes a pending tick outright is bulk — LevelChunkTicks.removeIf, through LevelTicks.clearArea below. Clean up is LevelTicks.cleanupAfterTick, emptying all four working collections including LevelTicks.toRunThisTickSet, which is built lazily and only if somebody asks LevelTicks.willTickThisTick. That list is the whole difference between the two questions a block can ask. TickAccess.hasScheduledTick reads the container and answers is one booked; LevelTickAccess.willTickThisTick reads this already-collected list and answers is one about to run in this very level tick — a thing TickAccess.hasScheduledTick can no longer see, because LevelChunkTicks.poll freed the slot during collect.

The comparisons, and which is used where

comparisoncomparesused by
ScheduledTick.DRAIN_ORDERtrigger tick, then priority, then sub-orderthe priority queue inside every LevelChunkTicks
ScheduledTick.INTRA_TICK_DRAIN_ORDERpriority, then sub-order — no time termLevelTicks.drainFromCurrentContainer, comparing two already-due ticks
LevelTicks.CONTAINER_DRAIN_ORDERthe same, applied to two containers’ headsLevelTicks.containersToTick
LevelChunkTicks.SUB_TICK_ORDERINGsub-order aloneLevelChunkTicks.pack, on save only
ScheduledTick.UNIQUE_TICK_HASHnot an ordering — identity on type and positionLevelChunkTicks.ticksPerPosition and LevelTicks.toRunThisTickSet

Time drops out of the second comparison because a container reaches the collect queue only once its head is already due, and once everything in play is due, time no longer discriminates.

Separating the phases has two consequences. A tick booked during the run phase lands in its container after collect has finished, so it waits for a later drain even at delay zero — the one exception being a block tick that books a fluid tick at delay zero, which the fluid drain, running afterwards in the same ServerLevel.tick, still catches. And the dedup slot is released by LevelChunkTicks.poll during collect, not at run, so a tick may book its own successor from inside its own run: exactly how a fluid keeps flowing and how a repeater arms its turn-off.

That the run list outlives the run is what makes bulk edits correct across the phase boundary. LevelTicks.copyAreaFrom, called by /clone in CloneCommands, harvests matching ticks from LevelTicks.alreadyRunThisTick, from LevelTicks.toRunThisTick and from the containers in the area, then re-bases every sub-order above the highest it found so copies keep their relative order without colliding with the originals. LevelTicks.clearArea does the mirror image for the gametest framework (StructureUtils.clearSpaceForStructure, GameTestInfo). Both touch block ticks only — nothing in the game copies or clears fluid ticks by area.

A repeater, appointment by appointment

A repeater with RepeaterBlock.DELAY 1 — RepeaterBlock.getDelay doubles it, so two game ticks — with a redstone wire behind it that goes to 15 and then back to 0 one tick later.

sequenceDiagram
    participant SL as ServerLevel
    participant RB as RepeaterBlock
    participant LTs as LevelTicks
    participant LCTs as LevelChunkTicks

    SL->>RB: neighborChanged — the wire behind went to 15
    RB->>RB: DiodeBlock.<br/>checkTickOnNeighbor<br/>— unlocked, unpowered,<br/>so it should turn on
    RB->>LTs: willTickThisTick at this position? no
    RB->>SL: scheduleTick — delay 2, TickPriority.HIGH
    SL->>SL: createTick — gameTime plus 2, HIGH, nextSubTickCount
    SL->>LTs: schedule
    LTs->>LCTs: schedule — the dedup slot is free, the queue takes it
    LCTs-->>LTs: updateContainerScheduling — the index learns gameTime plus 2
    Note over SL,RB: next tick, the wire drops to 0 — the diode books nothing and cancels nothing
    Note over SL,LCTs: two ticks after the booking, in the level tick's pending-block phase
    LTs->>LCTs: poll — the tick leaves the queue and the dedup set
    LTs->>SL: tickBlock at this position, for Blocks.REPEATER
    SL->>RB: still a repeater, so<br/>BlockBehaviour.<br/>BlockStateBase.tick
    RB->>SL: setBlock POWERED true, with Block.UPDATE_CLIENTS
    SL->>RB: the section write lands, then DiodeBlock.onPlace
    RB->>SL: neighborChanged on the block it powers, then that block's other sides
    RB->>SL: scheduleTick again — the turn-off, at TickPriority.VERY_HIGH

One repeater, from the wire behind it changing to the appointment for turning itself off again. The first note bar is the tick the page is built on — the wire went back to zero and the booking already made was neither cancelled nor noticed — and the second opens the tick in which the appointment runs.

The booking is a priority the block chooses, not one this queue assigns. The repeater above asks for TickPriority.HIGH to turn on and TickPriority.VERY_HIGH to turn off, and which of the seven each redstone block asks for in which situation — and why a comparator cannot ask for the two most urgent at all — is diodes and the observer. What this page supplies is what the priority then buys: the sub-tick order above.

A booking cannot be called off. Nothing in the game cancels a single scheduled tick: the only removal is the gametest framework’s bulk LevelTicks.clearArea above. So a booking made while the input was present survives the input going away, and the block finds out only when its turn comes. That is the queue’s half of pulse extension; what a repeater does when it gets that turn is the diode page’s.

Nothing here uses Block.UPDATE_NEIGHBORS (block update flags has the bits), so the write in the diagram above wakes no neighbour through Level.setBlock’s neighbour channel; only its shape pass runs. How a diode’s signal leaves instead is diodes and the observer.

The other kind of turn: random ticks

The appointment book is one of the ways a block gets a turn, and the contrast with random ticks is what defines it: a random tick is booked by nobody, aimed at no block, and carries no promise.

It also reaches a different set of chunks, and the difference is one ring. Random ticks walk ChunkMap.forEachBlockTickingChunk, which despite its name is the entity-ticking set; the scheduled-tick gate, ServerLevel.isPositionTickingWithEntitiesLoaded, reads the wider block-ticking radius instead. So at the edge of a player’s simulation distance there is a ring of chunks where appointments still come due and nothing is ever chosen at random. How the level tick then spends the rule’s number, and why a section of solid stone costs nothing, belongs to the level tick.

Two things about that walk belong here, because they are about what a promise is worth. The first is that the eligible blocks are decided at bootstrap, not per tick: BlockBehaviour.BlockStateBase.isRandomlyTicking is baked into the state at BlockBehaviour.BlockStateBase.initCache from BlockBehaviour.isRandomlyTicking, which defaults to BlockBehaviour.Properties.randomTicks, so unlike an appointment, which names a type and is checked against the world when it comes due, a random tick’s eligibility is settled before the world exists.

The second is that lava gets its random tick twice. LiquidBlock.isRandomlyTicking and LiquidBlock.randomTick both delegate straight to the fluid, so a chosen lava position runs LavaFluid.randomTick once through the block branch of that loop and again through the fluid branch. It is the only fluid it happens to: Fluid.isRandomlyTicking is false by default and LavaFluid.isRandomlyTicking is the sole override. Water is never randomly ticked at all — every inch of its motion is a scheduled tick.

Appointments that survive a restart

A SavedTick stores a relative delay rather than an absolute time, so a chunk that unloads while the world runs on fires its ticks the same number of ticks after it starts ticking again: ScheduledTick.toSavedTick subtracts the current game time on the way out and SavedTick.unpack adds the new one on the way back. LevelChunkTicks.pack writes the pending list first and then the live queue sorted by LevelChunkTicks.SUB_TICK_ORDERING, and SerializableChunkData stores the two lists under block_ticks and fluid_ticks through SavedTick.codec (copy on the server, encode on a worker, write on the IO lane). On the way in, SavedTick.filterTickListForChunk discards any saved tick whose position is not in the chunk being loaded.

Coming back is two-stage. SerializableChunkData builds a LevelChunkTicks for a chunk at ChunkStatus.FULL and a ProtoChunkTicks for one below it, and the LevelChunkTicks constructor holds the saved list as LevelChunkTicks.pendingTicks — not in the queue — while pre-seeding the dedup set from it, so a fresh booking cannot double up a saved one that is not unpacked yet. The saved ticks enter the queue only when ChunkMap.prepareTickingChunk reaches ServerLevel.startTickingChunk → LevelChunk.unpackTicks → LevelChunkTicks.unpack, which counts sub-orders up from minus the list’s length. Every unpacked tick gets a negative sub-order, and Level.subTickCount starts each session at zero and only rises, so a loaded tick always sorts before anything this session booked at the same time and priority.

Generation takes the same road. A ProtoChunk holds ProtoChunkTicks, which records everything at delay zero and dedups under SavedTick.UNIQUE_TICK_HASH, and WorldGenRegion exposes both of its containers as a WorldGenTickAccess — a router that finds the right chunk per position and answers LevelTickAccess.willTickThisTick with a flat false. At promotion ProtoChunk.unpackBlockTicks turns that list into a LevelChunkTicks in the pending state, and it becomes real at the same ServerLevel.startTickingChunk a loaded chunk goes through — due immediately, at the game time the chunk started ticking.

Questions players ask

Why did breaking one block stop a machine that was two ticks from firing? A tick names a type. The appointment stays in the queue and still runs, but ServerLevel.tickBlock re-reads the position, finds a different block, and runs it to nothing.

Does /tick freeze stop scheduled ticks? Yes, and among the tick commands it is the only one that does — a debug world skips the section too. TickRateManager.runsNormally returns TickRateManager.runGameElements, recomputed every tick as not frozen, or stepping — so /tick step runs the tickPending section normally for the ticks it steps, and /tick sprint clears the freeze flag outright for the length of the sprint (ServerTickRateManager.requestGameToSprint) and puts back whatever it found when the sprint ends.

Where do my ticks go when a chunk stops ticking? Nowhere. They sit in the chunk’s own queue, the index entry is left untouched, and the moment the chunk is block-ticking again the due ones are collected, as far as the drain’s budget goes. If the chunk unloads first they are written to disk with it. Outside generation and the test framework’s area clear, the only appointment lost is one booked into a chunk with no registered container.

For a 1.21-era reader. BlockBehaviour.updateShape no longer takes a LevelAccessor. It takes a LevelReader and a separate ScheduledTickAccess — a small interface for booking: ScheduledTickAccess.createTick, ScheduledTickAccess.getBlockTicks, ScheduledTickAccess.getFluidTicks and four ScheduledTickAccess.scheduleTick overloads that compose them. Every waterloggable block books water’s tick from its own override of it.

Where to look

The appointment and the two things that decide its fate: ScheduledTick · ScheduledTick.UNIQUE_TICK_HASH · ScheduledTick.DRAIN_ORDER. Booking, from the block’s side in: ScheduledTickAccess.scheduleTick · LevelAccessor.createTick · LevelChunkTicks.schedule. One drain, in its three phases: LevelTicks.tick · LevelTicks.sortContainersToTick · LevelTicks.drainContainers · LevelTicks.runCollectedTicks · ServerLevel.tickBlock · LevelTicks.cleanupAfterTick. The gate the collect phase asks: ServerLevel.isPositionTickingWithEntitiesLoaded. And the disk form and the two stand-ins for a live chunk’s containers: SavedTick · ProtoChunkTicks · BlackholeTickAccess.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Fluids

Verified against Minecraft 26.3 · Part IV · A bucket of water is emptied on flat stone and spreads one step.

A player holding a water bucket clicks the top of a stone block. Their own client places the source at once, on a prediction, and will never spread it a single block for the rest of the session. The server places the same block and then also does nothing — for five ticks. When the appointment falls due, the source looks down, finds stone, and hands the decision to four independent searches, one per horizontal direction, each walking out through the surrounding blocks looking for somewhere the water could fall. On flat stone all four fail in exactly the same way, so all four tie and the water goes out evenly. Break the symmetry and it does not, because water turns toward a hole four steps past the block it is about to fill, every side running its own depth-first search — and a side the water is not allowed to replace still votes on where the rest of it goes.

This page is the fluid model: the two registry objects behind each fluid, the block that carries them, what one fluid tick decides and where it sends the result. The queue that appointment sits in — booked, deduped, drained, saved and reloaded — is scheduled ticks, which this page borrows whole and never explains.

The cast

classwhat it decidesthread
Fluidthe registry object: its Fluid.stateDefinition, and the numbers every fluid has, the tick delay first, as overridable methodsbuilt at bootstrap, read anywhere
FluidStateone interned combination of its fluid’s properties — what a block reports; a tick names the Fluidimmutable
FlowingFluidthe whole algorithm: what a position should hold, whether to go down or sideways, and which sides win, with the slope numbersServer; the flow’s direction read on both sides
WaterFluidwater’s numbers, and whether two sources may make a thirdServer; its particles on the client
LavaFluidlava’s numbers, and the one override that turns liquid into rockServer; its particles and sounds on the client
LiquidBlockthe block form of a fluid, and where a fluid tick is booked from when the fluid is the blockServer
SimpleWaterloggedBlockthat a stair can be full of water without being a water blockServer
BucketItemwhere a source comes from, and the one attribute that stops it arrivingServer, with a client prediction

Two registry objects, one substance

Fluid is to FluidState what Block is to BlockState, and it is literally the same machinery (the state, a leaf): StateHolder, a StateDefinition built in the constructor, and one interned instance per combination of properties, so two FluidStates can be compared by identity. FlowingFluid puts FlowingFluid.FALLING on every state it defines, and the flowing subclasses — WaterFluid.Flowing, LavaFluid.Flowing — add FlowingFluid.LEVEL (BlockStateProperties.LEVEL_FLOWING, 1 to 8) on top of it. The static initialiser in Fluids then walks BuiltInRegistries.FLUID and pours every state of every fluid into Fluid.FLUID_STATE_REGISTRY, the one global id table.

Thirty-seven fluid states make up the game: one for Fluids.EMPTY, and eighteen each for water and lava (two from the source object, sixteen from its flowing twin).

Each fluid being two registry objects is not bookkeeping. Fluids.WATER is a WaterFluid.Source whose WaterFluid.Source.getAmount returns 8 and whose WaterFluid.Source.isSource returns true without consulting any property; Fluids.FLOWING_WATER is a WaterFluid.Flowing whose WaterFluid.Flowing.getAmount reads FlowingFluid.LEVEL and whose WaterFluid.Flowing.isSource returns false without consulting anything either. WaterFluid.isSame answers true for either, which is how the algorithm treats water as one substance no matter which object it is holding. The scheduler does not: a scheduled tick is keyed on the fluid object, so Fluids.WATER and Fluids.FLOWING_WATER are two different appointments and one of each can be pending at a single position (scheduled ticks).

The amount is also the height. FluidState.getOwnHeight delegates to FlowingFluid.getOwnHeight, which divides the amount by nine — so a full source stands 8/9 of a block tall, and FlowingFluid.getHeight has to special-case a same-fluid block overhead and return a flat 1 so that a submerged column is not full of seams.

The block underneath the water

Nothing in the world stores a FluidState. A chunk section stores BlockStates (sections and their four counters), and BlockBehaviour.BlockStateBase.getFluidState returns a field — BlockBehaviour.BlockStateBase.fluidState, filled once by BlockBehaviour.BlockStateBase.initCache at bootstrap by asking the block. Reading the fluid at a position is a block lookup and a field read, which is why the flow algorithm can afford to ask hundreds of times a tick.

For water and lava the block is LiquidBlock, whose single property is LiquidBlock.LEVEL (BlockStateProperties.LEVEL, 0 to 15). Its constructor builds LiquidBlock.stateCache, nine FluidStates: index 0 is the source, 1 through 7 are flowing at amount 8 minus the index, and index 8 is flowing at amount 8 with FlowingFluid.FALLING set. LiquidBlock.getFluidState clamps the level to 8, so block levels 8 through 15 all read back as falling and full, and FlowingFluid.getLegacyLevel, the encoder going the other way, is correspondingly lossy for falling flows — which costs nothing, because FlowingFluid.getNewLiquid only ever produces a falling state at amount 8.

LiquidBlock has a block tick of its own, and it spreads nothing. LiquidBlock.tick calls BubbleColumnBlock.updateColumn, and only when the fluid there is a full source in FluidTags.BUBBLE_COLUMN_CAN_OCCUPY; the tick was booked twenty ticks out by LiquidBlock.tryScheduleBubbleBlockColumn because soul sand or magma is underneath. Flow is entirely a fluid tick, in the other queue with its own budget — two queues, two appointments, one block.

Waterlogging runs the other way round: a block that implements SimpleWaterloggedBlock reports water of its own accord. SimpleWaterloggedBlock.canPlaceLiquid accepts Fluids.WATER and nothing else, and SimpleWaterloggedBlock.placeLiquid sets BlockStateProperties.WATERLOGGED and books the water tick itself rather than leaving it to LiquidBlock.onPlace. The state it reports is a source, which is why a waterlogged stair is never drained by a fluid tick: the first half of FlowingFluid.tick is skipped entirely for a source. Exactly one block in the game reports a source that is also falling — WaterloggedTransparentBlock, the copper grate — and FlowingFluid.FALLING is the flag that lets FlowingFluid.getFlow add a downward component to the current it pushes entities with.

A bucket, five ticks, four neighbours

sequenceDiagram
    box transparent the server
        participant BI as BucketItem
        participant SL as ServerLevel
        participant LB as LiquidBlock
        participant LTs as LevelTicks
        participant FF as FlowingFluid
    end
    box transparent the client
        participant CPL as ClientPacketListener
    end

    BI->>SL: setBlock of the source — neighbours, clients, immediate
    SL->>LB: the section write lands, then onPlace, which these flags do not skip
    LB->>SL: scheduleTick for Fluids.WATER, five ticks out
    SL->>LTs: schedule into this chunk's fluid container
    SL->>CPL: one ClientboundBlockUpdatePacket at broadcast time
    Note over BI,CPL: five ticks later, inside ServerLevel.tick
    LTs->>SL: the fluid queue hands the position back to tickFluid
    SL->>FF: FlowingFluid.tick, through FluidState.tick — a source skips the scan
    FF->>FF: spread tries down, stone refuses
    FF->>FF: spreadToSides scores the four sides
    FF->>SL: setBlock of flowing water at amount 7, four times
    SL->>LB: onPlace on each new block
    LB->>SL: each new block books its own tick
    SL->>LB: neighborChanged on the source, which books it again
    SL->>CPL: four more block changes, in one section packet when they share a section

A bucket emptied, and the five ticks before anything moves: the box on the right is the only lane on the other machine, and of the broadcast block changes it hears two, the source and then the four blocks the water reached — nothing about the appointment, the scan or the scoring crosses. The note bar is the whole delay, and the two self-messages under it are the search this page is really about.

BucketItem.use picks the position, then BucketItem.emptyContents asks two questions before it places anything. Is the block there a LiquidBlockContainer — the interface SimpleWaterloggedBlock narrows to water, and the waterlogging path — and does EnvironmentAttributes.WATER_EVAPORATES hold at this position, read through EnvironmentAttributeReader.getValue? The Nether’s dimension type sets that attribute, so there the bucket plays a hiss, throws eight smoke particles and returns success having placed nothing: this page’s whole trace never happens in the Nether (environment attributes). The first question only decides whether the position is a legal target; the evaporation branch returns before the waterlogging is done, so a bucket on a Nether stair hisses too. On overworld stone it destroys and drops whatever was there and calls Level.setBlock with the source’s FluidState.createLegacyBlock and the flag word 11 — Block.UPDATE_NEIGHBORS, Block.UPDATE_CLIENTS, Block.UPDATE_IMMEDIATE (the flag word, enumerated in block update flags).

The appointment is the block’s doing, not the fluid’s. LevelChunk.setBlockState writes the section and then, server-side and unless Block.UPDATE_SKIP_ON_PLACE is set, calls LiquidBlock.onPlace, which asks LiquidBlock.shouldSpreadLiquid (always true for water) and schedules a tick for Fluids.WATER at WaterFluid.getTickDelay, five ticks out. LiquidBlock.neighborChanged and LiquidBlock.updateShape book the same appointment when something next door moves, and so does SimpleWaterloggedBlock.placeLiquid — but the habit is not LiquidBlock’s alone. Sixty-three call sites across fifty-six classes schedule a fluid tick, because every waterloggable block books water’s tick from its own override of BlockBehaviour.updateShape, WaterloggedTransparentBlock included. Nothing in FlowingFluid books its own future except the single line in FlowingFluid.tick that follows a state change — and that booking is a duplicate the queue drops, because the write one line earlier has already booked the same appointment through LiquidBlock.onPlace.

The client is told none of this, because there is nothing to tell. The placing player’s client ran BucketItem.use itself inside MultiPlayerGameMode.startPrediction and wrote the source block at once — what the window holds is not the write but the old state, filed under a sequence number against the day the server disagrees (the six windows) — so the source appears locally with no round trip; the blocks the water later touches arrive the way any run of block changes does (one flush a tick, two audiences), which for a flow spreading inside one section means one ClientboundSectionBlocksUpdatePacket a tick. LevelChunk.setBlockState skips LiquidBlock.onPlace off the server, and the client’s fluid queue is a black hole (where an appointment waits), so no client ever runs the spread. What it does run is FluidState.animateTick from ClientLevel.doAnimateTick — ambient sound and particles, with Fluid.getDripParticle fetched separately just after — and FluidState.getFlow, which both the fluid mesher and shared entity physics need to know which way the surface leans. Flowing water on a client is a stream of block updates and a direction.

Five ticks on, LevelTicks drains the fluid queue and hands the position back through ServerLevel.tickFluid, which re-reads the block and fires FluidState.tick only if the fluid still there is the one the tick named. It is, and because it is a source, FlowingFluid.tick goes straight to FlowingFluid.spread.

What one fluid tick decides

For anything that is not a source, the first half of FlowingFluid.tick answers a question with nothing to do with spreading: what should this position hold, given its surroundings? FlowingFluid.getNewLiquid answers it, and the order of its three branches is most of a fluid’s character.

flowchart TD
    T["FlowingFluid.tick, on a state that is not a source"]
    T --> SCAN["FlowingFluid.getNewLiquid: one pass over the four horizontal neighbours"]
    SCAN --> B1{"two or more sources, conversion allowed, and a floor under this block"}
    B1 -- yes --> SRC["a source, not falling"]
    B1 -- no --> B2{"the same fluid directly above"}
    B2 -- yes --> FALL["flowing at amount 8, falling"]
    B2 -- no --> B3["the highest neighbour amount, minus FlowingFluid.getDropOff"]
    B3 --> Z{"zero or less"}
    Z -- yes --> EMPTY["Fluids.EMPTY"]
    Z -- no --> FLOW["flowing at that amount, not falling"]

Three ways for one fluid block to decide what it should now be, in the order they are asked. The order is the character: source conversion is asked first, so even a block with the same fluid overhead becomes a source when two sources and a floor allow it, and only a block that fails the first question can be made to fall.

That is only the answer. What is done with it is a second, shorter fork, and its first question is the one a reader guesses wrong: an empty answer never reaches the comparison the other two must pass.

flowchart TD
    NEW["the state the scan arrived at"]
    NEW --> E{"is it Fluids.EMPTY"}
    E -- yes --> AIR["plain air, neighbours and clients, nothing scheduled"]
    E -- no --> CMP{"the same state that is already here"}
    CMP -- yes --> KEEP["write nothing, schedule nothing"]
    CMP -- no --> SET["the new state, the same two flags, and LiquidBlock.onPlace books the next tick"]
    AIR --> SPREAD["FlowingFluid.spread, which returns at once on an empty state"]
    KEEP --> SPREAD
    SET --> SPREAD

What a tick does about the answer: only one of the three arms books another tick, which is how a flow stops on its own. All three arrive at FlowingFluid.spread, which returns at once for the block that just became air.

The scan that feeds the branches counts a neighbour only if FlowingFluid.canPassThroughWall says the face between the two positions is open.

Why the wall test is affordable

That test runs constantly — once per neighbour per scan, and again for every candidate of every slope search below — so it is built to be cheap twice over. FlowingFluid.canPassThroughWall short-circuits the easy cases first: either side a full cube is a no, both sides empty is a yes. Only past those does it merge the two collision shapes with Shapes.mergedFaceOccludes (shapes and collision), and it memoises that answer in FlowingFluid.OCCLUSION_CACHE, a thread-local 200-entry map keyed by FlowingFluid.BlockStatePairKey, which hashes both states by identity and the direction. A block with a dynamic shape (Block.hasDynamicShape) skips the cache entirely, because its answer cannot be keyed on the state.

The first branch is source conversion, and it is where infinite water lives: two source neighbours, a yes from WaterFluid.canConvertToSource — which reads GameRules.WATER_SOURCE_CONVERSION, true by default (game rules) — and something solid (BlockBehaviour.BlockStateBase.isSolid) or another source of the same fluid directly below the position being filled. LavaFluid.canConvertToSource reads GameRules.LAVA_SOURCE_CONVERSION, false by default, so infinite lava is one rule away rather than a property of lava. The second branch is the same fluid overhead, which always produces a falling state at full amount, however little is falling past. The fallback is the highest same-fluid neighbour minus FlowingFluid.getDropOff — one for water, two for lava outside the Nether — and zero or less is empty.

Two outcomes at the bottom of the figure matter more than the branches above them. Empty means the block becomes plain air and nothing is rescheduled: a fluid tick that decides a position should be dry books no follow-up, and the appointment book simply forgets it. And when the answer equals the state already there — the ordinary case for a settled flow — neither the write nor the booking happens, so a stable pool that is ticked one last time falls silently off the books.

Down first, then sideways

FlowingFluid.spread tries below before anything else. If the block there can hold the fluid, its existing FluidState.canBeReplacedWith allows the swap and the floor between them is open, the fluid falls: FlowingFluid.spreadTo fills the position below and FlowingFluid.spread returns — unless FlowingFluid.sourceNeighborCount finds three or more source neighbours around the position that is pouring, in which case it spreads sideways as well. That is the lip of a large pool draining into a hole: it pours down and keeps flooding outward in the same tick.

Under our bucket the block below is stone, so nothing passes. Because the state is a source, FlowingFluid.spreadToSides runs anyway (the other way in is a position whose floor is not a FlowingFluid.isWaterHole). Its first act is a gate, not a value: the amount minus the drop-off, or a flat 7 for a falling state, and if that is zero or less nothing spreads at all. What each side actually receives is whatever FlowingFluid.getNewLiquid computes for that side from scratch.

Four searches, and the losers still vote

FlowingFluid.getSpread returns a map from direction to the state that direction would get, and building it is the most expensive thing a fluid does. A direction is a candidate if FlowingFluid.canMaybePassThrough — not already a source of this fluid, a block that can hold fluid at all, and not walled off — and if the state it would receive can be held there. Each candidate is then scored by its distance to the nearest hole, where a hole is FlowingFluid.isWaterHole: a position whose floor will let the fluid through and whose lower neighbour will take it.

A candidate that is itself a hole scores 0 with no search at all. Otherwise FlowingFluid.getSlopeDistance searches outward from it: a depth-first walk that tries the three horizontal directions other than the one it arrived from, returns the pass number the moment it finds a hole, and recurses only while the pass is still below FlowingFluid.getSlopeFindDistance — 4 for water, 2 for lava, 4 for lava in the Nether. A search that finds nothing returns 1000. So a side’s score is 0 to 4, or 1000 for nowhere to fall from here, and a score of 4 means the hole is four steps past the block the water is about to fill. Three branches, four deep, is up to a hundred and twenty positions per side; FlowingFluid.SpreadContext, built once per FlowingFluid.getSpread call, caches block states and hole answers in two maps keyed by the candidate’s x and z offset from the origin packed into a short, so the overlap between the four searches is paid for once.

The winners are the sides holding the minimum score. A better score clears everything collected so far and a tie is kept, which is why on flat stone all four directions score 1000, all four survive, and the water goes out evenly. And here is the part nobody expects: the running minimum is updated by every scored candidate, but only a candidate whose existing fluid passes FluidState.canBeReplacedWith is put in the map. WaterFluid.canBeReplacedWith allows replacement only from directly above and only by something that is not water, so a neighbour that is already water refuses every sideways spread — and still clears the map, and still lowers the minimum. One unreplaceable near neighbour can therefore suppress every other direction. It is also why a pool that has finished spreading is quiet: the source’s four neighbours all refuse, the map comes back empty, and FlowingFluid.spreadToSides places nothing.

FlowingFluid.spreadTo does the placing and is deliberately dumb. A LiquidBlockContainer gets LiquidBlockContainer.placeLiquid; anything else that is not air (air, the usual target, is skipped) has FlowingFluid.beforeDestroyingBlock run over it — WaterFluid.beforeDestroyingBlock drops the block’s items through Block.dropResources, LavaFluid plays a fizz — and then LevelWriter.setBlockAndUpdate, flags 3. It schedules nothing itself (a LiquidBlockContainer.placeLiquid books its own). Every new flowing block books its own tick from its own LiquidBlock.onPlace, and the neighbour update at the tail of Level.setBlock reaches back to the source, whose LiquidBlock.neighborChanged books the source again before the shape pass’s booking is dropped as a duplicate.

How a flow stops

Two ways, and only one of them is dramatic. The quiet one is the gate. Water leaves a source at amount 7 and loses one per block, so the seventh block out holds amount 1, its FlowingFluid.spreadToSides gate computes zero, and the front simply stops. Behind it every block’s tick keeps getting the same answer from FlowingFluid.getNewLiquid, writes nothing and books nothing, and the pool falls off the books until a LiquidBlock.neighborChanged or a LiquidBlock.updateShape wakes it.

The loud one is what happens when the source is taken back. LiquidBlock.pickupBlock swaps it for air — level 0 only, which is why you cannot fill a bucket from flowing water — the flag word runs Level.updateNeighborsAt, each neighbouring LiquidBlock.neighborChanged books a fluid tick, and five ticks later those blocks compute FlowingFluid.getNewLiquid with no source in reach. Each block that ticks comes back with its highest same-fluid neighbour’s amount less the drop-off — lower than before, and empty once nothing beside it holds more than the drop-off — writes that, and books itself again unless it came back empty. So the pool re-levels repeatedly on its way out, and each block’s last act is to schedule nothing.

Lava is water with worse numbers, and three exceptions

waterlavalava under EnvironmentAttributes.FAST_LAVA
Fluid.getTickDelay53010
FlowingFluid.getDropOff121
FlowingFluid.getSlopeFindDistance424
how far a flow reaches7 blocks3 blocks7 blocks
source conversionGameRules.WATER_SOURCE_CONVERSION, trueGameRules.LAVA_SOURCE_CONVERSION, falseunchanged
Fluid.isRandomlyTickingnoyesyes

LavaFluid.isFastLava reads EnvironmentAttributes.FAST_LAVA through EnvironmentAttributeReader.getDimensionValue. The attribute is declared EnvironmentAttribute.Builder.notPositional, so it is a property of the whole dimension rather than of a place in it, and the Nether’s dimension type sets it — along with EnvironmentAttributes.WATER_EVAPORATES. Nether lava is not special-cased anywhere in LavaFluid; it is the same three methods reading one boolean.

On top of the numbers, LavaFluid.getSpreadDelay multiplies the delay by four, three times in four, whenever a non-falling flow is about to get deeper — but the booking it feeds is the one FlowingFluid.tick makes after its own write, which the queue drops, because the write has already booked Fluid.getTickDelay through LiquidBlock.onPlace. So lava keeps its steady delay, and the uneven creep the method is written to make never reaches the world.

The three places lava stops behaving like a fluid

All three end in a fizz and leave a block behind. LavaFluid.spreadTo intercepts a downward spread onto water: the fizz plays, nothing spreads down, and the target becomes Blocks.STONE — the method checks it is a LiquidBlock, which it always is, because lava cannot pour into anything else that holds water, and runs over a waterlogged stair as over any solid top. So a lavafall into a pool builds a plug rather than replacing the water. The other two share one method, LiquidBlock.shouldSpreadLiquid, called from LiquidBlock.onPlace and LiquidBlock.neighborChanged: for lava it walks LiquidBlock.POSSIBLE_FLOW_DIRECTIONS and tests each direction’s opposite, so the faces it inspects are the top and the four sides and never the bottom. Water at any of them turns this block into Blocks.OBSIDIAN if its own fluid is a source and Blocks.COBBLESTONE if it is not, and returns false so no tick is booked at all. The third exception shares that walk and is the only place the block below is read: Blocks.SOUL_SOIL underneath and Blocks.BLUE_ICE at one of those five opposites turns the lava into Blocks.BASALT, again with a fizz and again without booking a tick.

Lava’s random tick is fire rather than flow, and a selected position gets it twice — once as a block and once as a fluid — for reasons that belong to scheduled ticks.

Where to look

Where a source comes from and who books its tick: BucketItem.emptyContents · LiquidBlock.onPlace · LiquidBlock.shouldSpreadLiquid. Then one tick, in the order it decides: FlowingFluid.tick · FlowingFluid.getNewLiquid · FlowingFluid.spread · FlowingFluid.spreadToSides · FlowingFluid.getSpread · FlowingFluid.getSlopeDistance · FlowingFluid.spreadTo. The numbers files, and what makes lava lava: WaterFluid · LavaFluid · LavaFluid.spreadTo · LiquidBlock.shouldSpreadLiquid.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Game events and vibrations

Verified against Minecraft 26.3 · Part IV · A footstep reaches a sculk sensor.

You walk across a stone floor and, eight blocks away, a sculk sensor’s tendrils flick up and it pushes redstone power out of its side. Nothing scanned for you: the step itself posted a GameEvent.STEP into GameEventDispatcher.post, which walked the loaded chunk sections around you and called each listener inside that listener’s own radius — all but the catalyst’s inline — before Entity.move had finished. Nothing about that broadcast waits for a later tick — and yet what a player believes about sculk lives entirely in the delay, which the last stages put back. A sensor never acts on an event in the game tick that stamped it: VibrationSelector.chosenCandidate hands over a candidate only if it was recorded on an earlier tick — which costs a mob’s footstep a whole server tick and yours nothing, because the server handles a player’s movement packet before the level tick moves the clock on. Everything else a player thinks is stealth — the wool box, the crouch — is one of the gates between that footstep and that slot, and the crouch does not work the way the folklore says.

The cast

classwhat it decidesthread
GameEventhow far the news travels — one field, GameEvent.notificationRadius— (a record)
GameEventDispatcherwhich sections are walked, and who is told inline rather than sortedServer
EuclideanGameEventListenerRegistrywhich listeners in one section Y are close enough to be visitedServer
DynamicGameEventListenerwhich section a moving listener is registered inServer
VibrationSystem.Listenerthe gates between an event and a candidateServer
VibrationSelectorwhich of a tick’s candidates survives, and that none is used before the next tickServer
VibrationSystem.Tickerthe travel countdown, the particle, and the moment of arrivalServer, from the host’s own tick
SculkSensorBlockEntity.VibrationUserwhether this sensor takes a vibration, and what it does with oneServer

A game event is one number

GameEvent is a record of a single field, GameEvent.notificationRadius. That is the whole type. Sixty-one constants live in BuiltInRegistries.GAME_EVENT, fifteen of them GameEvent.RESONATE_1 through GameEvent.RESONATE_15, one per vibration frequency, so a resonating block can re-emit the frequency it heard. All but three take GameEvent.DEFAULT_NOTIFICATION_RADIUS, 16 blocks: GameEvent.JUKEBOX_PLAY and GameEvent.JUKEBOX_STOP_PLAY are 10, GameEvent.SHRIEK is 32. The radius is the dispatcher’s number, and each listener then applies its own. Everything else rides beside the event in a GameEvent.Context — the source entity and the affected block state, either of which may be absent.

The registry is a DefaultedMappedRegistry on step, which is narrower than it sounds. DefaultedMappedRegistry.getValue and DefaultedMappedRegistry.byId substitute GameEvent.STEP for an unknown key, so a raw lookup that misses becomes a footstep — but GameEvent.CODEC is a RegistryFixedCodec, which errors on an unknown name. Bad data in a pack fails loudly; only the raw lookup silently becomes a step.

Listeners live in the chunk, one registry per section

A GameEventListener is four methods: a PositionSource (BlockPositionSource for a block, EntityPositionSource for a mob, the latter resolving a stored UUID against the level when it is asked, until it finds the entity), a GameEventListener.getListenerRadius, GameEventListener.handleGameEvent and a GameEventListener.getDeliveryMode. They are stored per chunk section in LevelChunk.gameEventListenerRegistrySections (chunk anatomy), and LevelChunk.getListenerRegistry creates an EuclideanGameEventListenerRegistry for a section Y the first time anyone asks — including the dispatcher, merely walking past. One is dropped again through LevelChunk.removeGameEventListenerRegistry, but only when an EuclideanGameEventListenerRegistry.unregister empties it. ChunkAccess.getListenerRegistry answers GameEventListenerRegistry.NOOP: proto chunks and the client have none.

Block entities join on the chunk’s terms: LevelChunk.addGameEventListener runs from LevelChunk.addAndRegisterBlockEntity on placement and from LevelChunk.registerAllBlockEntitiesAfterLevelLoad when the chunk comes back, asking EntityBlock.getListener — whose default returns the listener of any block entity implementing GameEventListener.Provider (block entities). A sensor’s section is fixed for the life of the block. Entities move, so they carry a DynamicGameEventListener instead: a listener plus the last SectionPos it was filed under. Entity.updateDynamicGameEventListener is empty on Entity and overridden by Warden and by Allay, which hands over two. ServerLevel.EntityCallbacks drives it — DynamicGameEventListener.add, DynamicGameEventListener.remove, and DynamicGameEventListener.move on every section change. Its two halves are guarded separately and its record of where it was advances either way, so a move whose old chunk is not loaded to ChunkStatus.FULL leaves a stale registration behind, and one whose new chunk is not takes the listener out of the world entirely.

The broadcast is a nested loop, and one listener is the exception

ServerLevel.gameEvent is one line into GameEventDispatcher.post, and ServerLevel.gameEventDispatcher owns nothing between calls. GameEventDispatcher.post turns the radius into a range of sections — 3 by 3 by 3 for the default 16, 5 by 5 by 5 for a shriek — fetches each chunk column once with ServerChunkCache.getChunkNow, and visits each section’s registry. EuclideanGameEventListenerRegistry.visitInRangeListeners resolves each listener’s position, compares block-position distance squared against the listener’s own radius squared — a sphere inside the dispatcher’s cube — and calls GameEventListener.handleGameEvent there and then. No queue, no ordering, no next-tick delivery: the broadcast is a nested loop inside Entity.move, and the emitter’s method has not returned yet.

Two things follow. ServerChunkCache.getChunkNow returns null for a column that is not already loaded and GameEventDispatcher.post skips it, so an event at the edge of the loaded world is never delivered over the border. And because a listener can be told mid-walk, a registry sets EuclideanGameEventListenerRegistry.processing while it iterates and defers every EuclideanGameEventListenerRegistry.register and EuclideanGameEventListenerRegistry.unregister to the end of the visit.

One listener does wait. GameEventListener.getDeliveryMode is GameEventListener.DeliveryMode.UNSPECIFIED everywhere except SculkCatalystBlockEntity.CatalystListener, which answers GameEventListener.DeliveryMode.BY_DISTANCE. Those are collected as GameEvent.ListenerInfos, sorted by squared distance and delivered by GameEventDispatcher.handleGameEventMessagesInQueue after the walk, because a catalyst consumes the dead mob’s experience (LivingEntity.skipDropExperience) and LivingEntity.wasExperienceConsumed means only the first told gets any. Sorting is how the nearest one wins.

The gates between a footstep and a candidate

The cascade has a joint in it, and the prose above has already named it: the walk is the dispatcher’s, and every gate after GameEventListener.getDeliveryMode belongs to one listener deciding about itself. So it is two pictures. First, the walk.

flowchart TD
    A{"Entity.applyMovementEmissionAndPlaySound"}
    A -->|"mid-stride"| X1["no event is posted at all"]
    A -->|"a stride has passed"| C["ServerLevel.gameEvent posts GameEvent.STEP at the entity"]
    C --> D["GameEventDispatcher.post walks 3 by 3 by 3 sections"]
    D --> E{"ServerChunkCache.getChunkNow"}
    E -->|"not loaded"| X2["skipped in silence, never retried"]
    E -->|"loaded"| F{"EuclideanGameEventListenerRegistry.visitInRangeListeners"}
    F -->|"out of its radius"| X3["not visited"]
    F -->|"in range"| G{"GameEventListener.getDeliveryMode"}
    G -->|"BY_DISTANCE"| Q["queued, sorted by distance, delivered after the walk"]
    G -->|"UNSPECIFIED"| H["GameEventListener.handleGameEvent, inline"]

From a footstep to a listener that has been handed it: every exit on the left is silent — nothing is loaded, nothing is retried, nothing is told. The two right-hand ends are the only two ways the walk finishes, and the sculk catalyst is the only listener in the game that takes the queued one.

Both ends are a real delivery, and the difference is when. Everything below continues from the inline one, and narrows again: VibrationSystem.Listener.handleGameEvent is the vibration path, which Allay.JukeboxListener is not on at all.

flowchart TD
    H["VibrationSystem.Listener.handleGameEvent"]
    H --> B1{"a vibration already in flight"}
    B1 -->|"yes"| DROP["dropped, and nothing is remembered"]
    B1 -->|"no"| B2{"VibrationSystem.User.isValidVibration"}
    B2 -->|"fails"| DROP
    B2 -->|"passes"| B3{"the listener's PositionSource"}
    B3 -->|"does not resolve"| DROP
    B3 -->|"resolves"| B4{"VibrationSystem.User.canReceiveVibration"}
    B4 -->|"fails"| DROP
    B4 -->|"passes"| B5{"VibrationSystem.Listener.isOccluded"}
    B5 -->|"all six rays stopped"| DROP
    B5 -->|"one gets through"| L["VibrationSelector.addCandidate"]
    S["SculkSensorBlock.stepOn, standing on the sensor"] --> S2{"not a warden, and the sensor can activate"}
    S2 -->|"VibrationSystem.User.canReceiveVibration"| L

Five refusals in the order they run, all of them landing in one place, and a short arm that arrives at the answer having asked almost none of them. Read the order rather than the tests: the busy check is first and costs nothing, the six-ray occlusion cast is last and costs the most, and the arm down the right is a player standing on the block.

The order is not the one a player would guess. The busy check comes first, so a sensor already carrying a vibration ignores everything without evaluating a single tag, and the occlusion raycast — much the most expensive gate — comes last, after every cheap refusal has had its chance. Before any of it, Entity.applyMovementEmissionAndPlaySound decides whether there is an event to post, by accumulating Entity.moveDist and firing only when it passes Entity.nextStep — which is why walking emits a footstep per stride rather than per tick. Three gates ask the user rather than the system: VibrationSystem.User.getListenableEvents supplies the tag VibrationSystem.User.isValidVibration tests first, the user’s PositionSource must resolve, and SculkSensorBlockEntity.VibrationUser.canReceiveVibration refuses GameEvent.BLOCK_DESTROY and GameEvent.BLOCK_PLACE at the sensor’s own position — which is why placing a sensor does not set it off — refuses a frequency of VibrationSystem.NO_VIBRATION_FREQUENCY, and otherwise defers to SculkSensorBlock.canActivate: inactive only.

The short arm down the right of that figure is the gate a player most often meets. SculkSensorBlock.stepOn runs from Entity.applyEffectsFromBlocks every tick an entity stands on the block, and calls VibrationSystem.Listener.forceScheduleVibration directly: no section walk, no radius test, no occlusion raycast, and — the part that matters — no VibrationSystem.User.isValidVibration, which is where the sneaking tag lives. Crouching on a sensor does not quiet it. What the shortcut still asks is VibrationSystem.User.canReceiveVibration, so an already-active sensor stays quiet, and SculkSensorBlock.canActivate; and it still refuses a warden by name. It ends where everything else ends, at VibrationSelector.addCandidate, so it buys no time either.

The occlusion test is worth reading slowly. VibrationSystem.Listener.isOccluded takes the source block’s centre, nudges it a hundred-thousandth of a block along each of the six Direction values in turn, and runs BlockGetter.isBlockInLine with a ClipBlockStateContext looking for BlockTags.OCCLUDES_VIBRATION_SIGNALS. It reports occluded only if all six rays are stopped, and returns the moment one is not — but the six run a hundred-thousandth of a block apart to the same point, so a block of wool the straight line passes through stops all six, and only a line grazing a block’s edge can slip one past. A wool box is a box because vibrations come from every side.

One footstep, and the ticks it takes to arrive

Every gate above passes, and the vibration still does not arrive for eight ticks of game time. This is where they go.

sequenceDiagram
    participant Entity as Entity
    participant SL as ServerLevel
    participant VSL as VibrationSystem.<br/>Listener
    participant VSel as VibrationSelector
    participant VST as VibrationSystem.<br/>Ticker
    participant SSVU as SculkSensor<br/>BlockEntity.<br/>VibrationUser
    participant SSB as SculkSensorBlock

    Note over Entity,SSB: tick T, a mob moves in its own tick
    Entity->>SL: gameEvent, GameEvent.STEP at the entity's feet
    SL->>VSL: the dispatcher's walk reaches it, inline
    VSL->>VSel: addCandidate, a VibrationInfo stamped with game time T
    Note over VST,SSB: still tick T, whenever the sensor's block entity ticks
    VST->>VSel: chosenCandidate
    VSel-->>VST: nothing, the candidate is not from an earlier tick
    Note over Entity,SSB: tick T plus 1
    VST->>VSel: chosenCandidate
    VSel-->>VST: the VibrationInfo
    VST->>SL: sendParticles, one VibrationParticleOption
    VST->>VSel: startOver clears the slot
    Note over VST,SSB: the countdown starts now, a block a tick
    Note over Entity,SSB: tick T plus 8, and only now does it arrive
    VST->>SSVU: onReceiveVibration — the event, the entities, the distance
    SSVU->>SSB: activate, with the power and the frequency
    SSB->>SL: setBlock PHASE active with POWER, then scheduleTick 30
    Note over Entity,SSB: 30 ticks later deactivate, then 10 more before inactive

One footstep, and the eight ticks of game time it takes to be heard eight blocks away: a mob’s candidate stamped in tick T is refused in tick T and chosen in tick T plus 1, then travels a block a tick. That first wait is a whole server tick for a mob, which moves inside its tick, and none for a player, whose move is handled from its packet before the level tick moves the clock on.

VibrationSystem.Ticker.tick is the whole of the wait, and runs from whoever hosts the listener: SculkSensorBlock.getTicker, CalibratedSculkSensorBlock.getTicker and SculkShriekerBlock.getTicker for the blocks — server-side only, and already gated by Level.shouldTickBlocksAt on their own chunk — and Warden.tick and Allay.tick for the mobs. One call does three things in order: select, if nothing is in flight; send or re-send the particle; then decrement the travel time and arrive if it has reached zero.

That particle is the only thing the client is told. VibrationParticleOption carries the destination PositionSource and the remaining tick count, and the client animates the flight from that alone: ClientLevel.gameEvent is an empty method and there is no vibration packet. When the block entity was loaded from disk, VibrationSystem.Data.shouldReloadVibrationParticle answers yes and the ticker re-sends the particle from a point interpolated along the path covered.

One slot, one tick late, and one refusal that waits

VibrationSelector holds at most one candidate — a VibrationInfo, which carries the event, the source position, the distance and the entity behind it — stamped with the game time it arrived. VibrationSelector.addCandidate takes an empty slot unconditionally; against a candidate from the same tick it takes the closer one, breaking a distance tie in favour of the higher frequency; and against one held over from an earlier tick it does nothing at all, because that one is already waiting to be consumed. Reading the slot does not empty it: VibrationSelector.startOver does, and the ticker calls it in the same breath as the read. That is the whole of “the nearest event wins” — per listener, per tick, one slot.

The latency falls out of the read side. VibrationSelector.chosenCandidate returns the candidate only if its stamp is strictly less than the current game time, so one added during tick T is invisible for the rest of tick T however the ordering falls, and the earliest it can be selected is the tick after — the next server tick for a mob, and the same server tick for a player, whose move is handled from its packet before the level tick moves the clock on. Travel is measured from there: VibrationSystem.User.calculateTravelTimeInTicks is the floor of the distance — one block per tick — and the countdown’s first decrement happens inside the same call that selected the vibration, so a source n whole blocks away arrives n minus 1 ticks after selection, and anything closer than two blocks arrives on the selecting tick itself.

Arrival is one call — VibrationSystem.User.onReceiveVibration, handed the event, the source entity, the projectile’s owner and the distance from the source’s block to the listener’s at arrival — and it can be refused. VibrationSystem.User.requiresAdjacentChunksToBeTicking is true for both sculk blocks, and VibrationSystem.Ticker will not deliver unless all nine columns of the 3 by 3 around the listener are loaded and pass Level.shouldTickBlocksAt — tickets and loading is what “ticking” means. It does not drop the vibration: the travel time is floored at zero, so the ticker asks again every tick until the neighbourhood ticks. And the whole of VibrationSystem.Data is written to disk under listener through VibrationSystem.Data.CODEC, so a vibration survives a reload with its countdown intact.

What arrival costs the block

VibrationSystem.VIBRATION_FREQUENCY_FOR_EVENT maps game events to frequencies 1 to 15 and returns 0 for anything unmapped: a step, a swim and a flap are 1, a death or an explosion 15. That number is kept by SculkSensorBlockEntity.setLastVibrationFrequency and is what a comparator reads out of an active sensor — SculkSensorBlock.getAnalogOutputSignal answers 0 in any other phase. The redstone power is a different number: VibrationSystem.getRedstoneStrengthForDistance is the larger of 1 and 15 minus the floor of 15 times the distance over the listener’s radius, using a distance recomputed from the two block positions at arrival, not the float stored when the candidate was made.

SculkSensorBlock.activate sets SculkSensorBlock.PHASE to SculkSensorPhase.ACTIVE with that power, schedules a block tick SculkSensorBlock.getActiveTicks out (30 for a plain sensor, the number SculkSensorBlock.ACTIVE_TICKS names, and 10 for a calibrated one), runs SculkSensorBlock.tryResonateVibration — which, for each of the six neighbours in BlockTags.VIBRATION_RESONATORS, posts the matching GameEvent.RESONATE_1 … GameEvent.RESONATE_15 at the neighbour’s position, so an amethyst block beside a sensor rebroadcasts the frequency it heard — and then emits GameEvent.SCULK_SENSOR_TENDRILS_CLICKING, the single entry in GameEventTags.SHRIEKER_CAN_LISTEN. Shriekers do not hear your footsteps; only standing on one sets it off directly. They hear sensors hearing you.

Coming down takes two scheduled ticks. At 30, SculkSensorBlock.tick calls SculkSensorBlock.deactivate, which drops the power to zero, moves the phase to SculkSensorPhase.COOLDOWN and schedules another tick SculkSensorBlock.COOLDOWN_TICKS (10) out; only that tick returns the block to SculkSensorPhase.INACTIVE. In between the sensor is dark and still refuses every vibration, because SculkSensorBlock.canActivate tests for inactive.

The other listeners

listenerradiuswhat it listens towhat a vibration does
SculkSensorBlockEntity8GameEventTags.VIBRATIONSactivates for 30 ticks, power by distance, frequency on the comparator
CalibratedSculkSensorBlockEntity16GameEventTags.VIBRATIONSthe same, but active for 10 ticks rather than 30, and when the block behind CalibratedSculkSensorBlock.FACING gives a redstone signal, only that exact frequency is accepted
SculkShriekerBlockEntity8GameEventTags.SHRIEKER_CAN_LISTENneeds a player behind the event, then SculkShriekerBlockEntity.tryShriek raises the warning level on a shrieker that can summon; when the shriek ends, darkness, and a warden at level 4
Warden16GameEventTags.WARDEN_CAN_LISTENanger through Warden.increaseAngerAt, a 40-tick MemoryModuleType.VIBRATION_COOLDOWN, and a disturbance location for WardenAi
Allay16GameEventTags.ALLAY_CAN_LISTEN, note blocks onlyAllayAi.hearNoteblock stores MemoryModuleType.LIKED_NOTEBLOCK_POSITION, after which it accepts that block and no other
Allay.JukeboxListener10not a vibration at alla plain GameEventListener for GameEvent.JUKEBOX_PLAY and GameEvent.JUKEBOX_STOP_PLAY — it makes the allay dance
SculkCatalystBlockEntity.CatalystListener8not a vibration at allon GameEvent.ENTITY_DIE, takes the mob’s experience as sculk cursors and blooms

The tags are where the personalities live, and they are data (tags): GameEventTags.WARDEN_CAN_LISTEN covers shrieks and tendril clicks that GameEventTags.VIBRATIONS leaves out, and leaves out the flap that sensors hear. The warden is also the one entity whose Entity.dampensVibrations is unconditionally true — a dropped item answers true too, but only while it holds something in ItemTags.DAMPENS_VIBRATIONS — so the warden is invisible to every vibration listener, wardens included, and the one entity SculkSensorBlock.stepOn refuses by name. Its brain and the allay’s are Part VI.

Questions players ask

Does sneaking make me silent? It makes six events silent. GameEventTags.IGNORE_VIBRATIONS_SNEAKING holds GameEvent.STEP, GameEvent.SWIM, GameEvent.HIT_GROUND, GameEvent.PROJECTILE_SHOOT, GameEvent.ITEM_INTERACT_START and GameEvent.ITEM_INTERACT_FINISH, and the test is Entity.isSteppingCarefully — whether the sneak key is down. Open a chest while crouched and the sensor hears it. Sneak past one as a player and CriteriaTriggers.AVOID_VIBRATION notes the advancement, because sensors and wardens answer VibrationSystem.User.canTriggerAvoidVibration.

Why did my wool floor not stop it? Wool underfoot and wool in the way are different tags doing different jobs. BlockTags.DAMPENS_VIBRATIONS on the block being walked on kills the event at VibrationSystem.User.isValidVibration, and ItemTags.DAMPENS_VIBRATIONS does the same for a dropped item through ItemEntity.dampensVibrations. BlockTags.OCCLUDES_VIBRATION_SIGNALS is the six-ray test, and it has to stop all six.

Why does a sensor near the edge of the world miss? Two reasons, both silent: GameEventDispatcher.post skips any column ServerChunkCache.getChunkNow does not already have, and a sensor whose 3 by 3 neighbourhood is not ticking holds a finished vibration until it is. The second is visible on the debug channel; the first is not, because the broadcast happens only where a listener was visited — DebugSubscriptions.GAME_EVENTS and DebugSubscriptions.GAME_EVENT_LISTENERS, broadcast through ServerLevel.debugSynchronizers (debugging the running game).

Where to look

The broadcast, from the event to the door of a listener: GameEvent · GameEventDispatcher.post · EuclideanGameEventListenerRegistry.visitInRangeListeners · LevelChunk.getListenerRegistry. Then the gates, in the order the cascade asks them: VibrationSystem.Listener.handleGameEvent · VibrationSystem.User.isValidVibration · VibrationSystem.Listener.isOccluded · VibrationSelector.addCandidate · VibrationSystem.Ticker.tick · VibrationSelector.chosenCandidate · SculkSensorBlock.activate. And the two doors out of this page: SculkSensorBlock.stepOn for the shortcut, DynamicGameEventListener.move for how a listener that walks stays filed.

The other index the world keeps about itself — where things worth walking to are, rather than what just happened — is points of interest. The brain behind a warden or an allay belongs to goals and brains.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Points of interest

Verified against Minecraft 26.3 · Part IV · A villager claims a bed.

A villager standing in the middle of a field at noon decides which bed is its own. It is not near it, it is not looking at it, and it will not walk to it for another six thousand ticks. AcquirePoi asked the index for the beds with a free ticket within 48 blocks, took the five nearest, asked PathNavigation for a path to all five at once, and the moment Path.canReach came back true it called PoiManager.take and decremented that bed’s ticket. Night has nothing to do with it. Hours later SleepInBed will finally put it in the bed and set AbstractBedBlock.OCCUPIED, and because both occupied variants of a bed head are in PoiTypes.BEDS and map to the same PoiTypes.HOME, that block change does not touch the record at all: the claim and the occupied flag speak in one direction only. Going to sleep tells the index nothing, and of the three behaviours that read the flag back, the only one that turns it into a change in the record can do just one thing with it — take a claim away. Neither is what a player would call ownership.

The cast

classwhat it decidesthread
PoiTypehow many tickets a kind of block hands out and how close is close enough — a record of PoiType.matchingStates, PoiType.maxTickets, PoiType.validRangestatic, built at bootstrap
PoiTypesthe catalogue, and the block-state → type map PoiTypes.TYPE_BY_STATE that every lookup goes throughstatic
PoiRecordone position’s type and its PoiRecord.freeTickets counterServer
PoiSectionthe records in one 16³ section, by section-relative position and by type, plus the Valid flag that decides whether the section is rebuilt on loadServer
PoiManagerthe index: a SectionStorage over the poi/ region files, the whole query family, and the village distance graphServer
ServerLevelthat a block change happened at all — ServerLevel.updatePOIOnBlockStateChange is the door every ordinary write goes throughServer, or the worldgen executor
AcquirePoiwhich POI a mob claims, and when to stop asking about one it cannot reachServer
ValidateNearbyPoi · SleepInBedwhether a remembered POI is still real, and what to do on arrivalServer

A ticket is a claim nothing enforces

A point of interest is a block state the game has decided is worth going to. There are twenty-one kinds, none data-pack-driven: PoiTypes.bootstrap registers them into BuiltInRegistries.POINT_OF_INTEREST_TYPE and files every one of their block states into PoiTypes.TYPE_BY_STATE, throwing at startup if two types claim one state. PoiTypes.forState and PoiTypes.hasPoi are the only questions anyone asks of that map.

A ticket is the claim. A PoiRecord starts with PoiType.maxTickets free and hands them out one at a time: PoiRecord.acquireTicket decrements, PoiRecord.releaseTicket increments, both refuse to run past their end of the range, and both mark the section dirty. PoiRecord.hasSpace asks whether any are left, PoiRecord.isOccupied whether any are gone — the second question being the one the village graph reads. Neither says anything about who: a record does not know its holder, a holder is not told when its record disappears, and no state on the block corresponds to a claim.

stateDiagram-v2
    [*] --> Free : block placed, PoiManager.add, PoiType.maxTickets free
    Free --> Held : PoiManager.take, then PoiRecord.acquireTicket
    Held --> Free : PoiManager.release, from one of four behaviours or the villager's end
    Held --> Gone : the block changes, so PoiManager.remove
    Free --> Gone : the block changes, so PoiManager.remove
    Gone --> [*]
    note right of Held : occupied, so its section is a village centre if the type is in PoiTypeTags.VILLAGE
    note right of Gone : nothing is released, and the claimant is not told

The whole life of one ticket, and the thing to look for is what is missing. The only way back to Free is a villager letting go, through a behaviour in its brain or its end, and the two arrows into Gone bypass the claim entirely — the block changed, the record went, and the note says what nobody hears about it.

The asymmetry in that figure is deliberate on the release side and merely survivable on the removal side. PoiManager.release throws when the section is not there and PoiSection.release throws when the record is not, which is why four of the five that release a ticket check PoiManager.getType or PoiManager.exists first. Those five are ValidateNearbyPoi, Villager.releaseAllPois (through Villager.releasePoi, which checks the type and then tests it against Villager.POI_MEMORIES before it dares), SetWalkTargetFromBlockMemory, GoToPotentialJobSite and VillagerMakeLove. The last checks nothing, and gets away with it because the position it releases is one PoiManager.take handed back a moment earlier. PoiSection.remove on a missing record only logs an error, so the removal path is allowed to be wrong and the release path is not.

Erasing a memory is not a release, which is what makes the job-site behaviours noisy and harmless: PoiCompetitorScan awards a contested site to whichever villager has the higher Villager.getVillagerXp and makes the loser erase MemoryModuleType.JOB_SITE, YieldJobSite has an unemployed villager hand its MemoryModuleType.POTENTIAL_JOB_SITE to a neighbour whose profession the site belongs to, and AssignProfessionFromJobSite promotes the potential site to the real one. None of those three touches a ticket — though GoToPotentialJobSite does, GoToPotentialJobSite.stop giving back the ticket AcquirePoi took on the potential site.

The catalogue

typesthe blockPoiType.maxTicketsPoiType.validRange
PoiTypes.ARMORER PoiTypes.BUTCHER PoiTypes.CARTOGRAPHER PoiTypes.CLERIC PoiTypes.FARMER PoiTypes.FISHERMAN PoiTypes.FLETCHER PoiTypes.LEATHERWORKER PoiTypes.LIBRARIAN PoiTypes.MASON PoiTypes.SHEPHERD PoiTypes.TOOLSMITH PoiTypes.WEAPONSMITHone work block each, in the same order — blast furnace, smoker, cartography table, brewing stand, composter, barrel, fletching table, all four cauldrons, lectern, stonecutter, loom, smithing table, grindstone11
PoiTypes.HOMEthe bed’s head half only — PoiTypes.BEDS filters AbstractBedBlock.PART to BedPart.HEAD11
PoiTypes.MEETINGthe bell326
PoiTypes.BEEHIVE PoiTypes.BEE_NESThive and nest01
PoiTypes.NETHER_PORTALthe portal block01
PoiTypes.LODESTONEthe lodestone01
PoiTypes.LIGHTNING_RODthe rod, every facing01
PoiTypes.TEST_INSTANCEthe test instance block01

Six of the twenty-one types are locatable but unclaimable. PoiRecord.hasSpace is false for them forever, so nothing can PoiManager.take one, and PoiRecord.isOccupied — which asks whether the free count has moved off PoiType.maxTickets — is false forever too. Not that it would matter: none of the six is in PoiTypeTags.VILLAGE, so none was ever a candidate for a village centre. They are indexed so something can find one fast: the bee’s hive search asks for PoiTypeTags.BEE_HOME within 20 blocks and then filters through Bee.doesHiveHaveSpace, which asks the BeehiveBlockEntity whether it is full. The index answers where and something else answers whether. Three tags cut across the catalogue (tags): PoiTypeTags.ACQUIRABLE_JOB_SITE for the thirteen professions, PoiTypeTags.BEE_HOME for the two hives, and PoiTypeTags.VILLAGE for those thirteen plus PoiTypes.HOME and PoiTypes.MEETING — fifteen types whose occupied records are what a village is.

Where the index lives, and how it repairs itself

PoiManager extends SectionStorage, so it is the third of the three region stores a dimension keeps (four folders, three of them the same shape), and the unit of storage is a chunk section and the unit of file a region: ChunkMap builds it on the dimension’s poi/ folder, PoiManager‘s own constructor naming DataFixTypes.POI_CHUNK, and PoiSection.Packed is the on-disk shape — a Valid boolean and a list of Records, each a position, a type and a free_tickets count. Tickets survive a restart, and so do the villagers’ memories of them, and nothing on load reconciles the two.

ChunkMap.tick runs PoiManager.tick under the profiler’s poi, which writes dirty chunks for as long as the tick has time and then settles the village graph (the level tick owns the budget). Reads are the interesting half. The query family is a dozen shapes of the same walk — PoiManager.getInSquare over a chunk range, PoiManager.getInRange narrowing it to a sphere, and PoiManager.findClosest, PoiManager.getRandom, PoiManager.getCountInRange, PoiManager.exists and PoiManager.getType above them — and every one bottoms out in SectionStorage.getOrLoad, where a section whose prefetch has not finished is read from disk synchronously, on the Server thread, blocking. That is why ChunkMap.scheduleChunkLoad fires SectionStorage.prefetch beside the chunk’s own parse and joins the two before either is used.

The repair runs on every chunk read. SerializableChunkData.read calls PoiManager.checkConsistencyWithBlocks once per section with block data. If the section is in storage, PoiSection.refresh rebuilds it — but only if its Valid flag is false, and the rebuild reuses the existing PoiRecord objects for positions that still have a POI, so a repair does not reset anybody’s tickets. If it is not in storage, one is created and scanned. Both scans are short-circuited by PoiManager.mayHavePoi, which asks LevelChunkSection.maybeHas whether the palette holds any state PoiTypes.hasPoi recognises (the palette and the ladder it climbs), so a section of plain stone is dismissed without one block read. The Valid flag’s codec defaults to false: anything not explicitly written as validated gets rescanned.

A record appears when a block changes, sometimes a task late

Level.setBlock calls Level.updatePOIOnBlockStateChange last, after the neighbour updates, and on a ServerLevel that override compares PoiTypes.forState of the old and the new state. Equal types mean nothing happens — exactly the bed case, since a bed head that gains AbstractBedBlock.OCCUPIED is still PoiTypes.HOME. Different types mean a PoiManager.remove for the old and a PoiManager.add for the new, each wrapped in a BlockableEventLoop.execute on the server.

That wrapper exists because WorldGenRegion.setBlock calls the same hook from the worldgen executor and the index is Server-thread-only. What it does not do is defer the ordinary case: MinecraftServer.scheduleExecutables is false when the caller is already the Server thread and not inside a task the loop is running (ReentrantBlockableEventLoop.runningTask), so a block placed by a player, a command or a mob during the tick body gets its record synchronously. Deferral is the worldgen and nested-task case — there the record appears a task later than the block it describes, and a read in between gets the old answer.

Noon, and a bed forty-eight blocks away

A bed is placed, a villager claims it, and thousands of ticks later it sleeps in it. The index is written when the bed goes down and asked, and a ticket taken, when the villager claims it — not the time you would guess — and at night asked only whether the record is still there.

sequenceDiagram
    participant SL as ServerLevel
    participant PM as PoiManager
    participant Brain as Brain
    participant AP as AcquirePoi
    participant PN as PathNavigation
    participant SIB as SleepInBed

    Note over SL,PM: any tick, Server thread
    SL->>SL: setBlock puts the bed head down, and the two states differ
    SL->>PM: add — a PoiRecord with one free ticket, the section dirty
    Note over Brain,SIB: Activity.CORE, any hour of the day, HOME absent
    Brain->>AP: at priority 10, and this evaluation is due
    AP->>PM: findAllClosestFirstWithType HOME, HAS_SPACE, 48 blocks
    PM-->>AP: the nearest five, past the retry cache
    AP->>AP: VillagerGoal<br/>Packages.<br/>validateBedPoi<br/>re-reads all five
    AP->>PN: createPath to all five at once, reach range 1
    PN-->>AP: a Path whose canReach is true, getTarget is one bed
    AP->>PM: take at that position, and one free ticket becomes zero
    AP->>Brain: MemoryModuleType.HOME set to a GlobalPos
    AP->>SL: broadcastEntityEvent 14
    Note over Brain,SIB: thousands of ticks later, tick 12000, Activity.REST
    Brain->>Brain: SetWalkTarget<br/>FromBlockMemory<br/>writes WALK_TARGET
    Brain->>Brain: ValidateNearbyPoi, within 16 blocks: is the record still HOME
    Brain->>SIB: within 2 blocks, and the bed is not OCCUPIED
    SIB->>SL: setBlock, AbstractBedBlock.OCCUPIED true
    SL->>SL: the state is HOME either way, so nothing is queued
    Note over Brain,SIB: morning, REST leaves the brain, SleepInBed.stop or WakeUp clears the flag, the ticket stays

A bed placed and a bed slept in: the index is written at the top, asked, and a ticket taken, at the claim in the middle, and at night asked only whether the record still exists. The last self-message is the page’s hook drawn as a non-event — sleeping wrote AbstractBedBlock.OCCUPIED straight to the block, and because the before and after are both PoiTypes.HOME the manager was never asked about it.

The scan is cheap and the path is not. PoiManager.findAllClosestFirstWithType turns a 48-block radius into a chunk radius of four, walks every section of those chunks, filters by PoiManager.Occupancy.HAS_SPACE and sorts by distance; AcquirePoi takes the first five and only then runs VillagerGoalPackages.validateBedPoi, which re-reads each block to confirm it is in BlockTags.VILLAGERS_CAN_SLEEP_ON_BED and not already AbstractBedBlock.OCCUPIED. Then AcquirePoi.findPathToPois hands all five positions to PathNavigation.createPath as one target set, at the reach range from PoiType.validRange — one, for a bed. A villager’s constructor raises PathNavigation.setRequiredPathLength to 48 so that this search can span the scan range (pathfinding owns that budget). Beds that failed before are held off by AcquirePoi.JitteredLinearRetry, whose delay is cumulative: each attempt adds another 40 to 79 ticks to that position’s own counter, capped at AcquirePoi.JitteredLinearRetry.MAX_RETRY_PATHFINDING_INTERVAL, 400. A bed behind a wall is checked ever more rarely until the gap reaches the cap, twenty seconds, and then the marker, untouched that long, is dropped, so the next check starts the saw again from the beginning. A successful claim clears the whole cache.

The claim itself is five statements. PoiManager.take — alone among the radius searches in having no PoiManager.Occupancy parameter, because it always means HAS_SPACE — is called with radius 1 around the path’s target and a filter accepting only that exact position, and calls PoiRecord.acquireTicket on what it finds. Then MemoryModuleType.HOME is set to a GlobalPos, then ServerLevel.broadcastEntityEvent sends event 14, EntityEvent.VILLAGER_HAPPY — the green particle burst, and the only thing an ordinary client learns of any of this. The last two statements clear the retry cache and tell the debug channel. The memory is set inside PoiManager.getType’s ifPresent, not inside PoiManager.take’s — the take’s result is never consulted.

After the claim: the night shift

Brain.updateActivityFromSchedule, run from UpdateActivityFromSchedule at priority 99 and only when more than twenty ticks have passed since it last did anything, reads the villager’s schedule attribute — EnvironmentAttributes.VILLAGER_ACTIVITY for an adult, EnvironmentAttributes.BABY_VILLAGER_ACTIVITY for a child — at the villager’s own position, and Timelines.VILLAGER_SCHEDULE puts the Activity.REST keyframe at tick 12000 of a 24000-tick period (environment attributes owns the mechanism, goals and brains the pull that reads it; the old Schedule class is gone). Only then does the bed half of the brain exist at all: VillagerGoalPackages.getRestPackage is where SetWalkTargetFromBlockMemory, ValidateNearbyPoi for PoiTypes.HOME and SleepInBed live, while the core package validates the job site and not the bed. So between dawn and dusk a villager whose bed was mined keeps pointing at a position with no record, and nothing tells it otherwise.

What the three of them do to the record

At night the three run in priority order, and only two of them ever touch a ticket. SetWalkTargetFromBlockMemory at priority 2 is the walking half — where it sends a villager and how it picks an intermediate target belong to goals and brains; what belongs here is its one effect on the index, which is that it gives up, releasing the ticket and erasing the memory, once MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE has stood for more than 1200 ticks, the memory points into another dimension, or, with the bed more than 150 blocks off, a thousand tries find no step toward it. ValidateNearbyPoi at priority 3 does nothing at all unless the bed is within 16 blocks and in this dimension: then it erases the memory if PoiManager.exists no longer agrees on the type, and if the bed is AbstractBedBlock.OCCUPIED while this villager is not asleep — asleep anywhere, not asleep here — it erases the memory. Whether it also releases the ticket turns on a second question, asked of the world rather than of the block: is a Villager actually sleeping inside that block’s box? If one is, the memory goes and the ticket stays, the sleeper being presumed to hold it. If none is, the ticket goes too. The two can disagree — a player asleep in the bed sets the same flag — and that disagreement is the whole reason the second question exists. SleepInBed, also priority 3, is the one that touches no ticket at all. It needs the villager within 2 blocks, the bed unoccupied and SleepInBed.COOLDOWN_AFTER_BEING_WOKEN ticks since MemoryModuleType.LAST_WOKEN; it calls LivingEntity.startSleeping, which is what actually sets the flag. That is the second of the three reads of AbstractBedBlock.OCCUPIED — a gate on entering the bed, not a change to the record — and the third is VillagerGoalPackages.validateBedPoi, the filter AcquirePoi runs over its best five.

Morning ends it twice over: when Activity.REST goes inactive, SleepInBed.stop wakes the villager as the behaviour ends, and WakeUp, at priority 0 in the core package, calls LivingEntity.stopSleeping on any villager still asleep outside it. Either way the flag clears and the ticket does not.

What makes a village

PoiManager.DistanceTracker is a SectionTracker — the same DynamicGraphMinFixedPoint flood the ticket system’s two graphs use (two graphs, one store), and the only one of the five outside server/level — over chunk sections instead of chunks. Its sources are the sections where PoiManager.isVillageCenter holds: at least one record whose type is in PoiTypeTags.VILLAGE and whose PoiManager.Occupancy is IS_OCCUPIED. They sit at level 0 and the flood runs out to PoiManager.MAX_VILLAGE_DISTANCE, six sections, past which the level is simply absent from the map.

A village is made of the sections in memory only. Alone in the query family, PoiManager.isVillageCenter reads through SectionStorage.get rather than SectionStorage.getOrLoad — the getter that answers null for a section not in memory instead of pulling it off the disk — and it treats that null as not a centre. So the graph is not a picture of what is in the save; it is a picture of what has been read into memory this run, which nothing evicts. That is deliberate: the flood is settled every tick from PoiManager.tick, and a version of it that could touch the disk would turn the village graph into an IO source in the middle of the level tick.

An unclaimed bed makes no village. A hundred empty beds are a hundred records and zero sources; one villager taking one ticket lights the section up. PoiManager.setDirty and PoiManager.onSectionLoad re-seed the source, PoiManager.tick settles the flood every tick, and PoiManager.sectionsToVillage settles it again before answering. ServerLevel.isVillage is that distance being one section or less; ServerLevel.isCloseToVillage takes the distance as an argument and refuses anything past six.

That boolean is load-bearing far outside this system. BadOmenMobEffect starts a raid only where ServerLevel.isVillage is true, VillageSiege needs one to put zombies in, PatrolSpawner refuses a patrol when its chosen player stands within two sections of one and CatSpawner insists on being within two. Raid re-checks it as the raid runs, and Raids sets the raid’s centre to the average position of the occupied PoiTypeTags.VILLAGE records within 64 blocks.

Other readers of the index

whowhat it asks forradiusPoiManager.Occupancy
PortalForcer.findClosestPortalPositionPoiTypes.NETHER_PORTAL, after PoiManager.ensureLoadedAndValid drags the chunks in16 going to the Nether, 128 coming backANY
BeePoiTypeTags.BEE_HOME, then Bee.doesHiveHaveSpace for the real occupancy20ANY
ServerLevel.findLightningRodPoiTypes.LIGHTNING_ROD standing at the surface height128ANY
LodestoneTrackerone position — is PoiTypes.LODESTONE still there——
LocateCommandPoiManager.findClosestWithType for a type or a tag256ANY
Raidsthe records it averages into a raid’s centre64IS_OCCUPIED
CatSpawnermore than four claimed PoiTypes.HOME nearby — asked only after ServerLevel.isCloseToVillage at two sections has already said yes48IS_OCCUPIED
WanderingTraderSpawnera PoiTypes.MEETING near a player, to arrive at48ANY
NearestBedSensorPoiTypes.HOME for MemoryModuleType.NEAREST_BED, babies only, no ticket taken48ANY

PoiManager.ensureLoadedAndValid in the first row is the only caller that forces the chunks themselves to load, where every other query reads the POI file alone: for any nearby section missing or invalid it pulls the chunk in at ChunkStatus.EMPTY, once per chunk per server run since PoiManager.loadedChunks never forgets. The index is on the debug channel too — LevelDebugSynchronizers.registerPoi, LevelDebugSynchronizers.updatePoi and LevelDebugSynchronizers.dropPoi feed DebugSubscriptions.POIS and DebugSubscriptions.VILLAGE_SECTIONS.

Questions players ask

Why is a villager sleeping in the bed I built for someone else? Because the claim is a number in a file and the bed is a block, and neither knows the other. PoiManager.take decremented a counter 48 blocks away hours before anyone walked anywhere, and SleepInBed only ever checks that the bed is not currently occupied — never who holds the ticket.

Why do villagers stop breeding when I take a bed away? VillagerMakeLove.tryToGiveBirth calls VillagerMakeLove.takeVacantBed first — a PoiManager.take for PoiTypes.HOME within 48 blocks, filtered by reachability. No free ticket, no baby, and the pair get entity event 13, EntityEvent.VILLAGER_ANGRY, instead. If the birth then fails the ticket is released; if it succeeds, VillagerMakeLove.giveBedToChild writes the baby’s MemoryModuleType.HOME directly, with no AcquirePoi involved. This is the raw PoiManager.take, which finds the first reachable match in section order rather than the nearest, so the baby’s bed need not be the closest.

Why does the same bed get claimed twice after I break and replace it? Breaking it runs PoiManager.remove and the record ceases to exist with its ticket count — no release, no notification. The villager keeps its GlobalPos until ValidateNearbyPoi next runs, which needs Activity.REST active and the villager within 16 blocks, or SetWalkTargetFromBlockMemory gives it up. Replace the bed first and there is a brand-new record with a full ticket: another villager can claim it, while the one whose memory already points there keeps pointing, so two villagers hold one bed.

Where to look

The record and the three things that happen to one: PoiRecord.acquireTicket · PoiManager.add · PoiManager.take · PoiManager.release. Where they live and how they are repaired: PoiManager.checkConsistencyWithBlocks · PoiSection.refresh · ServerLevel.updatePOIOnBlockStateChange. The claim, from the brain’s side: AcquirePoi.create · ValidateNearbyPoi.create. And the flood that turns claimed records into a village: PoiManager.DistanceTracker · ServerLevel.isVillage.

The other index in this corner of the tree — the fire-and-forget broadcast sculk sensors listen to — is game events and vibrations. The brain belongs to goals and brains, death and conversion to the entity lifecycle, the bed block to blocks and states.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

V · Blocks

Verified against Minecraft 26.3 · Part V · Everything that happens at the moment one block state replaces another: the click that asks for it, the write that performs it, and the four kinds of answer a block can give back.

You open a door and both halves swing. You flip a lever and the lamp across the room stays dark for a moment longer than you expected. Both are the same event underneath — a position in a chunk section stops holding one block state and starts holding another — and everything here is either choosing the state that goes in, performing the write, or being a block that answers one. A block has four ways to answer that this part teaches (a fifth, the game event, is Part IV’s), and the part is organised around them rather than around the three hundred block classes that use them: a neighbour update (something near you changed), a shape update (your neighbour’s state is now this — do you still fit?), a block event (do this at a named moment later in the tick) and a scheduled tick (do this in n ticks). A reader who has those four, and Part IV’s game events, can read any block in the game.

Two of them are the two channels a write can leave by, and that is where the door and the lamp part company: the neighbour channel exists only on the server, and the shape channel runs on both sides. The door’s other half comes down the channel your own client also runs, so it moves before the server has been asked; the lamp waits on the channel that exists only on the server. Half the surprises in this part are that sentence in another costume.

The shape of the part

Counting the two packages the atlas lists for this part, that is 484 files and 58,507 lines — and seven lectures is the fewest of any part with as many lines, on purpose. Most of those files are one Block subclass each, filling in a few of the hooks BlockBehaviour and Block declare (blocks and states).

Part V is a hub and six spokes. The hub is blocks-and-states, and what each spoke takes from it is not the state table — it is a piece of the write, drawn there in two flowcharts and pointed at from every other page. The six arrows out of the hub below are labelled with what it hands that spoke; the five between spokes are the places two lectures share a mechanism.

flowchart TD
    BS["1 · Blocks and states"]
    BI["2 · Block interaction"]
    BB["3 · Block breaking"]
    BE["4 · Block entities"]
    SD["5 · Signal and dust"]
    PE["6 · Pistons and block events"]
    DO["7 · Diodes and the observer"]
    BS -- "the shape channel" --> BI
    BS -- "the tail of an UPDATE_ALL write" --> BB
    BS -- "when a block entity lives and dies" --> BE
    BS -- "the neighbour channel" --> SD
    BS -- "the flag word's rarer bits" --> PE
    BS -- "an UPDATE_CLIENTS write's side effects" --> DO
    BI -- "one click each, one ledger" --> BB
    SD -- "what powers a piston" --> PE
    SD -- "what a diode reads" --> DO
    BE -- "redstone's one loose int" --> DO
    BE -- "the moving block's entity" --> PE

The part as a hub and six spokes, numbered to the watch order. Six arrows leave blocks-and-states carrying the piece of a write that lecture is about; the five arrows between spokes are the mechanisms two spokes share, and every one of them runs from an earlier lecture to a later one.

Before you start

Five pages are load-bearing, each for one reason. Chunk anatomy is the first half of every write here. The level tick matters because half of this part’s surprises are really claims about which phase of a tick something ran in, and the server tick sits behind it for the two claims about what happens outside one: a packet handler runs before the levels do, a connection is flushed after. Scheduled ticks is how a block gets a turn later, which each of the diode lecture’s three blocks uses. And fluids owns the FluidState that shares a StateHolder with every block state. Identifiers and registries is assumed rather than used: the hub rests throughout on every block state having been built into one table before any world existed.

One dependency runs the other way. Prediction and acknowledgement is Part X, and the two click lectures use three of its six windows — but its own scenario is a block placed against a wall, which needs this part’s vocabulary. So watch Part V first: both click pages state the contract in the same four sentences near their top, which is all either lecture needs. Part X is also where the third thing a player notices about this part lives, after the door and the lamp above — the block that appears under the crosshair before the server has heard about it.

Watch in this order

  1. Blocks and states — a right-click on stone puts one of oak stairs’ eighty states into the world. Every state the game will ever have was built before the world was, and the world only ever points into that table. The second half — what the write does — is the pair of figures the other six lectures point back at.
  2. Block interaction — the right click, in full: a door opened by hand. It fires no neighbour update at all, and the top half follows anyway.
  3. Block breaking — the same lecture’s other half: two clocks that agree without either telling the other its count, because the one comparison that matters happens in the packet drain rather than in the tick. Finish too early and the block usually comes back, then vanishes again, and nothing the player does with the button stops it.
  4. Block entities — a furnace smelts while nobody is looking. It tells nobody anything: the fire is a block state, the arrow is four ints from a menu, and both are a tick late by construction.
  5. Signal and dust — a lever, two dust, and the cascade. A line turning off is visited again at every intermediate value it passes through, none of which is ever sent to anybody, and the game ships a second implementation behind a flag that does not do it at all.
  6. Pistons and block events — the part’s deferral with no delay in it: the work waits for one named phase of the level tick rather than for a number of ticks, and usually gets it in the same tick — and the one place the client is handed a re-simulation rather than a result.
  7. Diodes and the observer — the part’s closer. Three blocks that learn about their neighbours three different ways, and the one whose entire job is noticing change turns out not to be listening on the channel that carries it.

Where the part stops

Part V owns the write’s tail and three of the four answers; it does not own most of the blocks. 47% of the part’s lines are named on no page in the book, and of the blocks that are taught, many are taught elsewhere: a block is usually the place some other system surfaces, so it is taught where its scenario is — the sculk family with game events and vibrations, LiquidBlock with fluids, the containers with Part VII, signs and chests as things drawn with Part XI, the structure block with jigsaw and templates, the command block with Part XIII. What comes back here is the moment any of them writes a state.

Of what belongs to nobody, three lecture-sized mechanisms are declared here, and this is the sentence that says so rather than leaving the silence to be found. The hopper is the first — HopperBlockEntity is named on one other page, for its cooldown, and the transfer itself is explained on none. Sculk spread is the second: SculkSpreader’s charges walking through SculkBehaviour, SculkBlock and SculkVeinBlock, where the page that owns the catalyst owns it only as a listener. The third, and the largest, is a family — BeaconBlockEntity, ConduitBlockEntity and the trial-spawner and vault sub-packages, the last two of which other pages name in passing while their state machines go unexplained. Each waits for a second edition.

Reference this part uses

Block update flags is the one to open first, because every page here spends at least one flag word: the ten bits of Level.setBlock’s argument and what reads each. Then registries for Registries.BLOCK and Registries.BLOCK_ENTITY_TYPE, packets for every block update, block event and acknowledgement in one table, data components for DataComponents.TOOL, game rules for GameRules.BLOCK_DROPS, and math and primitives for BlockPos, Direction and the packings every page here assumes. The glossary defines the four answers and the two things they act on, all but the scheduled tick from pages in this part, and diagram lanes explains the abbreviations these figures use.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Blocks and states

Verified against Minecraft 26.3 · Part V · A player right-clicks the top of a stone block holding oak stairs: one of the stair’s eighty pre-built states is chosen, and then written — and that write, drawn in two figures, is what the rest of this part points back at.

You are standing on stone with a stack of oak stairs, and you right-click the top of the block. A moment later a stair is up there, facing away from you, sitting on the bottom half of its cube. Nothing was constructed to make that happen. Oak stairs have four properties — facing, half, shape, waterlogged — and all eighty combinations of them were built before any world existed, in the class initialiser of Blocks, and numbered into one flat table, Block.BLOCK_STATE_REGISTRY. What a chunk stores points into that table: each section keeps a small palette of those very states, and uses the table’s own numbers once it holds more than 256 different ones. Both of the surprises about choosing a state fall out of that single decision. Choosing a property allocates nothing: StateHolder.setValue reads one cell out of a table of neighbours computed at startup and hands back a state that already existed. And the table’s numbers are not always checked: Block.getId answers 0 for a state its table has never seen, and Block.stateById answers Blocks.AIR’s default state for a number it does not know — so wherever the game reaches the table through that pair, a state the two sides disagree about raises nothing at all. It quietly becomes air.

Then the state has to go in, and that is the page’s second half and its wider job: the write is where a block state stops being a value and becomes an event, and the two channels it can leave by are not the same channel on both sides of the game. Six other lectures in this part are applications of that write, so it is drawn here in full.

The cast

classwhat it decidesthread
Blockone kind of thing: which properties it has, what its default state is, and — through its static helpers — the id lookups, the drops and the shape-update helpers the rest of the game callsbuilt at class-initialisation, read from every thread after
BlockBehaviourmost of the hooks a block may override, from BlockBehaviour.onPlace to BlockBehaviour.updateShape. Block extends it and adds registration and hooks of its own, Block.getStateForPlacement among themas above
BlockBehaviour.Propertieshardness, sound, map colour, whether it ticks — and the ResourceKey without which no block can be built at allkept by the BlockBehaviour constructor, which copies about a third of its values out; each state copies others, hardness and map colour among them
StateDefinitionthe table: which properties this block has, in which order, and the full product of their valuesbuilt in the Block constructor
Propertyone axis — a name, a value type, and where a value sits in that axisimmutable, shared between blocks
StateHolderone state’s property values, and the table that answers what state am I if this property becomes that valuefilled once by StateDefinition, read-only after
BlockBehaviour.BlockStateBaseevery hook asked of a state and forwarded to its block, and the caches that make collision and occlusion cheaphalf-built in its constructor, finished by BlockBehaviour.BlockStateBase.initCache
Block.BLOCK_STATE_REGISTRYthe integer a state is on the wire and in a section’s global palette — never on disk, where a state is its name and its propertiesappended once per state, in the Blocks class initialiser

Eleven classes and one Cartesian product

classDiagram
    class BlockBehaviour.Properties {
        hardness, sound, map colour, whether it ticks
        setId first, or the constructor throws
    }
    class BlockBehaviour {
        <<abstract>>
        most hooks a block may override
    }
    class Block {
        the registry holder and the static helpers
        one StateDefinition, one default state
        BLOCK_STATE_REGISTRY, over every state of every block
    }
    class Property {
        <<abstract>>
        a name, a value type, an index per value
    }
    class BooleanProperty {
        two values, true at index 0
    }
    class IntegerProperty {
        min to max, min never below zero
    }
    class EnumProperty {
        any StringRepresentable enum
    }
    class StateDefinition {
        propertiesByName, sorted by name
        states, the whole Cartesian product
    }
    class StateHolder {
        <<abstract>>
        this state's own property values
        the neighbours table
    }
    class BlockBehaviour.BlockStateBase {
        <<abstract>>
        every hook, forwarded to the block
        the caches initCache fills
    }
    class BlockState {
        a constructor, asState, two public codecs
    }
    BlockBehaviour.Properties ..> BlockBehaviour : kept by the constructor, a third copied out
    BlockBehaviour <|-- Block
    Property <|-- BooleanProperty
    Property <|-- IntegerProperty
    Property <|-- EnumProperty
    Block --> StateDefinition : builds exactly one, in its constructor
    StateDefinition --> Property : one axis each, collected by the Builder
    StateDefinition --> BlockState : one per cell, built once and never again
    StateHolder <|-- BlockBehaviour.BlockStateBase
    BlockBehaviour.BlockStateBase <|-- BlockState
    StateDefinition ..> StateHolder : fillNeighborsForState fills the table

The eleven classes a block state is made of, and the two hierarchies that meet in it: a kind three classes deep down the left and a state three classes deep down the right. StateDefinition joins them, building one BlockState per cell of the product and filling each state’s table of neighbours.

Two things in that picture are the section’s: Property is extended by those three classes and by nothing else, and BlockState inherits only down the state side, though its parent class is nested inside BlockBehaviour. The rest of this section is what the product is and how big it gets.

The kind, three classes deep

A block is a kind of thing — oak stairs, stone, water. A block state is one exact configuration of that kind, and it is a block state, never a block, that a chunk section stores, that a block update carries, that a model is chosen for. The kind is spread over three classes, and the third is the one a reader does not expect. BlockBehaviour is the base and holds most of the hooks; Block extends it and adds the registry holder, the state table, hooks of its own and the statics everything else in the game reaches for. The third is BlockBehaviour.Properties, which both are constructed from — a builder that must first be given an identity: BlockBehaviour.Properties.setId supplies the ResourceKey, the loot table and the translation key are derived from it, and the BlockBehaviour constructor throws Block id not set without one. So every block the game builds goes through Blocks.register, which takes the id from BlockItemIds or BlockIds and hands it to the builder on the way past. None of that is data: a block has no codec at all, so hardness, sound and map colour never serialise.

The table, sorted by name

Each Block constructor calls its own Block.createBlockStateDefinition, collecting properties through StateDefinition.Builder.add — which rejects a name outside lower-case, digits and underscore, a value name that breaks the same pattern, a property with fewer than two values, and a duplicate name — and then StateDefinition.Builder.create builds every state the block will ever have. Zero properties gives one singleton state, one property gives a row, and two or more gives the full Cartesian product of every property’s values, each cell constructed through a StateDefinition.Factory which for blocks is the BlockState constructor.

Eighty states of oak stairs: four facings, two halves, five shapes, two waterlogged values, every one of them a distinct object built before any world existed.

StateDefinition.propertiesByName is a sorted map, so the axes are ordered by property name, not by the order the block added them — for stairs the two orders happen to coincide, at facing, half, shape, waterlogged. Three things follow. The order of the global state ids follows it, because the product is built by walking that map. And so does the field order of BlockState.CODEC and StateDefinition.propertiesCodec — which is not the same as saying a state is written alphabetically on disk: NBT is a hash map there, and the alphabetical forms are text: the command syntax BlockStateParser writes and the one StateHolder.toString prints, neither of which goes near the codec, and any SNBT print, which sorts every compound’s keys. And StateDefinition.any is the first cell of the product, which the Block constructor installs as the default state unless the block calls Block.registerDefaultState itself. Since BooleanProperty.VALUES lists true before false, a block that does not override its default gets true for every boolean it has. That is why StairBlock sets StairBlock.WATERLOGGED to false explicitly: the alternative is stairs that are born full of water.

There are exactly three concrete kinds of Property and no DirectionProperty — facing is an EnumProperty over Direction (naming drift). BlockStateProperties is the shared pool of 124 of them, and several share a serialised name while being different objects: BlockStateProperties.FACING, BlockStateProperties.FACING_HOPPER and BlockStateProperties.HORIZONTAL_FACING are all facing on disk.

That pool, the four property classes and the twenty-four StringRepresentable enums of its own that EnumPropertys range over — ChestType, NoteBlockInstrument, StairsShape and the rest — are nearly the whole of the state/properties sub-package: no behaviour, just the axes and their values. The rest is two records that describe a family of blocks, BlockSetType and WoodType, and RotationSegment’s arithmetic for sixteen-way rotations. Two classes beside it read a state rather than being part of one, and are worth a name because their scenarios are elsewhere: BlockPattern with BlockPatternBuilder matches a three-dimensional arrangement of BlockInWorld, which is how the game recognises a built wither, and BlockStatePredicate is a StateDefinition turned into a test.

The state, a leaf

StateHolder is the generic state, shared with FluidState (fluids). It holds its owner, two parallel arrays of property keys and values, and StateHolder.neighbors — a two-dimensional table, property index by value index, answering what state am I if this property becomes that value. StateHolder.setValue walks the key array comparing references to find the row, asks Property.getInternalIndex for the column, and returns the object already sitting in that cell. It allocates nothing and it never constructs. The table is installed once by StateHolder.initializeNeighbors, and a second call throws. Because every state is built once, StateHolder.equals is final and identity-based: two states are the same only if they are the same object.

That reference comparison is why a property lookup can throw on a property that looks right. States match their properties by identity and properties match each other by value: Property.equals compares the value class and the name, refined by IntegerProperty and EnumProperty to compare the value list too. So two separately constructed properties can be equal to one another and still make StateHolder.setValue throw Cannot set property … as it does not exist. The constant in BlockStateProperties is the object the table was built with; a look-alike is not.

BlockBehaviour.BlockStateBase extends it and is the state-to-block hop — BlockBehaviour.BlockStateBase.getShape, BlockBehaviour.BlockStateBase.canSurvive and the rest each forward to the owning block with the state as the first argument. It is also where the caches live, and they arrive in two waves. Its constructor copies the flat values out of the block’s BlockBehaviour.Properties, and asks the block one question, BlockBehaviour.useShapeForLightOcclusion. Everything else that has to ask a virtual question — the fluid state, whether it random-ticks, the occlusion shape and its six faces, sky-light propagation, light dampening, and the BlockBehaviour.BlockStateBase.Cache of collision shape and sturdy faces built for every block without a dynamic shape — is filled later, by BlockBehaviour.BlockStateBase.initCache, because those questions may look at other blocks and so cannot be answered until every block exists.

BlockState itself is almost empty: a constructor, a BlockState.asState that returns this, and two public codecs — BlockState.FULL_CODEC, and BlockState.CODEC, which writes a default state as the bare block id. It exists so the generic plumbing has a concrete type to name. BlockBehaviour.BlockStateBase is the class people mean when they say block state.

The Blocks class initialiser is that second wave and the only caller of BlockBehaviour.BlockStateBase.initCache: it walks BuiltInRegistries.BLOCK, adds each state to Block.BLOCK_STATE_REGISTRY and finishes it. Note what makes the result safe to share between the Server thread, the Render thread, the chunk workers and the meshing pool — it is not immutability, because those cached fields are non-final and written long after the constructor. It is that the writes happen inside a class initialiser, and every thread that later reaches a BlockState reaches it through Blocks.

The id that answers air

That registry is where the opening’s second surprise lives, and it is worth being exact about how far it goes, because the tolerance is a property of two static methods and not of the id. Block.getId answers 0 for a state the table has never seen and Block.stateById answers Blocks.AIR’s default state for a number it does not know, so a disagreement between the two sides raises nothing and quietly becomes air. That pair is behind block-break particles, the falling-block spawn packet and EntityDataSerializers.OPTIONAL_BLOCK_STATE. The single-block update is stricter in both directions: ClientboundBlockUpdatePacket.STREAM_CODEC reads and writes the same table through ByteBufCodecs.idMapper, which uses IdMap.byIdOrThrow and IdMap.getIdOrThrow and fails the connection instead. The section update is not: ClientboundSectionBlocksUpdatePacket writes through the tolerant Block.getId and decodes with IdMapper.byId, which answers null.

Four decisions, four lookups

BlockItem.getPlacementState asks the block for a state and refuses if it cannot have one. The default Block.getStateForPlacement returns the block’s default state; StairBlock overrides it and makes four decisions, each of them one StateHolder.setValue into the table above.

StairBlock.FACING is UseOnContext.getHorizontalDirection, which is Entity.getDirection — the way the player is facing, so the tall side ends up away from them. StairBlock.HALF is Half.BOTTOM when the clicked face is the top, Half.TOP when it is the bottom, and otherwise decided by whether the hit point is in the upper or lower half of the clicked block. StairBlock.WATERLOGGED is whether the fluid already at the target position is Fluids.WATER. Then StairBlock.SHAPE is computed by StairBlock.getStairsShape from the partly built state: it looks at the neighbour in the direction the stair faces, and a stair there of the same half with a perpendicular facing gives StairsShape.OUTER_LEFT or StairsShape.OUTER_RIGHT; failing that it looks at the neighbour in the opposite direction for StairsShape.INNER_LEFT or StairsShape.INNER_RIGHT; failing both, StairsShape.STRAIGHT. In each case StairBlock.canTakeShape vetoes the corner if the stair on the far side is already aligned with this one. The same routine runs again in StairBlock.updateShape every time a horizontal neighbour changes, which is how a straight stair turns into a corner when you build next to it.

Everything in front of that — the click, the reach check, the block-then-item ordering, the packet and the ack — belongs to block interaction and prediction and acks. One sentence of it matters here: the client runs the identical BlockItem.place under a prediction, so the write below happens twice, once on each side, from the same code. Almost everything that differs is inside the write; what BlockItem.place itself does differently afterwards is to skip the block-entity tag and the advancement trigger on the client, and the state that lands is not affected by either.

BlockItem.placeBlock calls Level.setBlock with flags 11, Block.UPDATE_ALL_IMMEDIATE.

The two update channels

This is the shape the rest of Part V refers back to. A write is two half-writes with a re-read between them: LevelChunk.setBlockState changes the world and runs the side effects that belong to the position (and one that reaches its neighbours, below), then Level.setBlock’s tail runs the side effects that belong to the neighbourhood — and only if the state it reads back is the one it asked for.

A write reads its flag word by bit, so here are the eight bits it gates on before you meet them: 1 is Block.UPDATE_NEIGHBORS, 2 Block.UPDATE_CLIENTS, 4 Block.UPDATE_INVISIBLE, 16 Block.UPDATE_KNOWN_SHAPE, 32 Block.UPDATE_SUPPRESS_DROPS, 64 Block.UPDATE_MOVE_BY_PISTON, 256 Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS and 512 Block.UPDATE_SKIP_ON_PLACE. Placement’s 11 is therefore neighbours, clients and immediate — the combination Block.UPDATE_ALL_IMMEDIATE — so the stair’s write passes every flag test below, and the steps it skips, it skips for other reasons: there is no block entity to remove or make, and a stair has no analog output. The figures are the order; the table under them is what each step waits for.

flowchart TD
    IN["Level.setBlock"]
    IN -- "out of bounds, or debug" --> FALSE["returns false"]
    IN --> EMPTY

    subgraph CHUNK["LevelChunk.setBlockState"]
        EMPTY{"air into an empty section"}
        SEC["write the section"]
        NOOP{"this exact state already"}
        HM["the four live heightmaps"]
        LIGHT["tell the light engine what moved"]
        PRE["BlockEntity.preRemoveSideEffects"]:::server
        AFT["BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval"]:::server
        GUARD{"is the new block still there"}
        ONP["BlockBehaviour.BlockStateBase.onPlace"]:::server
        BE["create, keep or replace the block entity"]
        NOTHING["hand back nothing"]
        EMPTY -- "yes" --> NOTHING
        EMPTY -- "no" --> SEC --> NOOP
        NOOP -- "yes" --> NOTHING
        NOOP -- "no" --> HM
        HM --> LIGHT --> PRE --> AFT --> GUARD
        GUARD -- "no" --> NOTHING
        GUARD -- "yes" --> ONP
        ONP -- "the state has a block entity, and the block is still there" --> BE
    end

    NOTHING --> FALSE
    BE --> READ{"re-read: is it the state we wrote"}
    ONP -- "otherwise" --> READ

The first half-write: what LevelChunk.setBlockState does belongs to the position, bar the outgoing block’s word to its neighbours, and it has three ways out that write nothing further — the two no-ops at the top and the guard on the block, which all land in the same dead end. The three steps drawn in the server colour are the server’s alone.

The re-read at the foot is the joint the whole section turns on. Only if the state that comes back is the one that went in does the second half run, and that second half is where most of a write’s reach past the position lives.

flowchart TD
    READ{"re-read: is it the state we wrote"}
    READ -- "no, and the tail is skipped" --> TRUE["Level.setBlock returns true"]
    READ -- "yes" --> DIRTY

    subgraph TAIL["the tail of Level.setBlock"]
                DIRTY["Level.setBlocksDirty"]:::client

        SEND["Level.sendBlockUpdated"]
        NB["Level.updateNeighborsAt"]:::server
        SHAPE["three shape passes"]
        POI["Level.updatePOIOnBlockStateChange"]:::server
        DIRTY --> SEND --> NB --> SHAPE --> POI
    end

    POI --> TRUE

The second half-write, which with the first is what the part’s other six lectures apply. Both edges out of the diamond return true — a write that fails its re-read skips the whole tail and still says it succeeded.

The figures are the order and the table is the gate: every step in them runs unconditionally unless a row here says otherwise, and the flag numbers are the ones the lead-in paired with their names.

stepsideflagsalso needs
write the sectionboth——
the four live heightmapsboth—the two worldgen heightmaps are never touched
tell the light engine what movedboth—the section’s emptiness flipped (LevelLightEngine.updateSectionStatus), or the light properties differ (LevelLightEngine.checkBlock is queued)
BlockEntity.preRemoveSideEffectsserver256 clearthe block changed, and the new state does not keep the old entity. The removal after it runs on both sides
BlockBehaviour.BlockStateBase.affectNeighborsAfterRemovalserver1 set, or 64 setthe block changed, or the new block is a BaseRailBlock
BlockBehaviour.BlockStateBase.onPlaceserver512 clear—
create, keep or replace the block entityboth—the new state has a block entity, and the block is still there after BlockBehaviour.BlockStateBase.onPlace; then, either way, ChunkAccess.markUnsaved
Level.setBlocksDirtyclient—Empty on Level; the client re-meshes through LevelExtractor.setBlockDirty
Level.sendBlockUpdatedboth2 set, and 4 clear on the clienton the server, a chunk at FullChunkStatus.BLOCK_TICKING or better
Level.updateNeighborsAtserver1 setempty on Level, overridden on ServerLevel
Level.updateNeighbourForOutputSignalserver1 setthe new state has an analog output
three shape passesboth16 clear, with 1 and 32 masked out of what they pass onthe update limit, which starts at Block.UPDATE_LIMIT, still positive. Indirect for the old state, direct for the new, indirect for the new
Level.updatePOIOnBlockStateChangeserver—empty on Level, overridden on ServerLevel

Inside the chunk write

The section write, the four heightmaps and the light checks are the same on both sides, and belong to chunk anatomy. Three things after them are not.

BlockEntity.preRemoveSideEffects is the block entity’s last word before it is unregistered — the chest scattering its contents, say. It needs the server, and it needs Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS clear; the removal that follows it happens either way, on both sides (block entities). BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval is how the outgoing block tells its neighbours it is gone — a piston head taking its base, a broken lever telling the block it powered. It is not how a door drops its other half: DoorBlock does not override it, and the top half goes down the shape channel instead (block interaction). It is easy to put in the wrong place: it runs inside the chunk write, before the new state is even confirmed, not in Level.setBlock’s tail with the other neighbour work. It needs the server, it needs Block.UPDATE_NEIGHBORS set or the moved-by-piston bit, and it also needs the block to have changed — a write that only changes a property does not fire it, unless the new block is a BaseRailBlock. Rails are the exception because a rail carries its own geometry in a property: changing just the shape leaves the block the same and still moves the track, and BaseRailBlock.affectNeighborsAfterRemoval is what tells the positions that geometry reaches — above it when the old shape was a slope, and its own and the one below for the rail kinds that never curve (powered, activator and detector rails).

Then the chunk re-reads its own section. If a side effect has already replaced the block just written with another, LevelChunk.setBlockState returns nothing at all and Level.setBlock reports false. Otherwise BlockBehaviour.BlockStateBase.onPlace runs — server-side, with Block.UPDATE_SKIP_ON_PLACE clear — and then, if the new state has a block entity and BlockBehaviour.BlockStateBase.onPlace has not replaced the block, the block entity is created, kept or replaced. A block entity that disagrees with the new state is logged as mismatched and thrown away.

Note that the diamond on the block and the re-read after it are not the same test asked twice. The one inside the chunk write asks whether the block just written is still there and answers false when it is not; the one back in Level.setBlock asks the stricter question, whether the very state is, and answers true anyway — it has a write to report, and what it skips is the whole tail. So false means the chunk had nothing to change or a side effect of the removal replaced the block, and true with nothing visible happening means something later, BlockBehaviour.BlockStateBase.onPlace included, changed the state or even the block first. Those three statements are the only ones that return false: a position out of bounds, the server side of a debug world, and the chunk write coming back with nothing.

Back in Level.setBlock’s tail

The first two steps of the tail are how the change becomes visible. Level.setBlocksDirty is empty on Level itself; on the client it reaches LevelExtractor.setBlockDirty, which re-meshes only if ModelManager.requiresRender says the two states look different. The broadcast that follows is gated on Block.UPDATE_CLIENTS, and then on opposite conditions per side: the client also needs Block.UPDATE_INVISIBLE clear, the server also needs the chunk to be at FullChunkStatus.BLOCK_TICKING or better, so a write into a chunk that is loaded but not yet simulating tells nobody. Worldgen is silent for a different reason again: it never reaches Level.setBlock at all, writing through WorldGenRegion instead.

The next two are the two update channels proper, and the difference between them is the fact the rest of this part rests on. (The fifth and last step of the tail is neither: Level.updatePOIOnBlockStateChange, which keeps the village’s index of interesting blocks in step with the world and belongs to points of interest.)

Neighbour updates are server-only. Level.updateNeighborsAt and Level.neighborChanged are empty methods on Level, overridden only by ServerLevel. Gated on Block.UPDATE_NEIGHBORS, the server hands the position to its NeighborUpdater — a CollectingNeighborUpdater on every real level, the alternative InstantNeighborUpdater being used by nothing the game ships — which visits the six neighbours in NeighborUpdater.UPDATE_ORDER (west, east, down, up, north, south), calling each one’s BlockBehaviour.neighborChanged. Beside it, Level.updateNeighbourForOutputSignal reaches the comparators in the four horizontal directions, directly or through one redstone conductor (diodes and the observer).

Shape updates run on both sides. Level.neighborShapeChanged is implemented on Level, and both a ServerLevel and a ClientLevel own a CollectingNeighborUpdater. Unless Block.UPDATE_KNOWN_SHAPE is set, Level.setBlock runs three passes with a decremented limit and with Block.UPDATE_NEIGHBORS and Block.UPDATE_SUPPRESS_DROPS masked out of the flags it propagates: BlockBehaviour.BlockStateBase.updateIndirectNeighbourShapes for the old state, then BlockBehaviour.BlockStateBase.updateNeighbourShapes for the new, then the indirect pass again for the new. The middle one is the familiar one: six neighbours in BlockBehaviour.UPDATE_SHAPE_ORDER — west, east, north, south, down, up, a different order from the neighbour channel — each asked for a new state through BlockBehaviour.BlockStateBase.updateShape and then handed to Block.updateOrDestroy. The indirect passes are the hook a block uses to reach past its six neighbours. Block.UPDATE_LIMIT, 512, is the budget that stops this cascade — a recursion depth, and not the far larger per-request budget over the neighbour channel (block interaction).

And there is the catch. Block.updateOrDestroy writes the new state on either side — but when the new state is air its destroy branch is server-gated, going through Level.destroyBlock, which writes with flags 3 and posts GameEvent.BLOCK_DESTROY. So a shape update that turns a block into nothing deletes it on the server, with a neighbour fan-out of its own, and does nothing at all on the client, which then waits to be told.

The flag word

Those eight bits are not the whole word. There are ten, and what each is called, what reads it and how the four named combinations decompose are the catalogue’s: block update flags. One number on this page is not a bit at all — Block.UPDATE_LIMIT is 512 like Block.UPDATE_SKIP_ON_PLACE and means something entirely different.

Where to look

Follow the trace: Blocks.register and BlockBehaviour.Properties.setId build a block, StateDefinition.Builder.create builds its table and StateDefinition.StateCollection.fillNeighborsForState fills the cells StateHolder.setValue will later read. Block.BLOCK_STATE_REGISTRY is where the numbers come from and BlockBehaviour.BlockStateBase.initCache is what finishes each state. Then StairBlock.getStateForPlacement chooses one, BlockItem.placeBlock writes it, and LevelChunk.setBlockState and Level.setBlock are the two halves of the write. Block.updateOrDestroy is the end of a shape update, and NeighborUpdater.executeShapeUpdate — not named above — is where the updater hands a queued one to the block.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Block interaction

Verified against Minecraft 26.3 · Part V · A player right-clicks the bottom half of an oak door, and the top half opens without a single neighbour update.

You are standing in front of a closed oak door, crosshair on its bottom half, and you press the use key. Before the tick is over the door is open in your client’s world, both halves of it, on your screen a frame or so later, and a packet is on its way to a server that has not yet been asked. The obvious guess about how the top half found out is wrong. Opening a door fires no neighbour updates at all — DoorBlock writes with flags 10, and the neighbour bit is not among them — and the top half follows anyway, down the shape channel, which is the half of the update machinery the client also runs. That is why a door feels instant on a laggy server and a redstone lamp does not.

The contract both halves run under. The client acts at once and remembers the state it overwrote, under a sequence number it sends with the action. The server’s ClientboundBlockChangedAckPacket is a receipt for that number and not a verdict — once the server considers the client loaded, it is sent for actions the server refused exactly as for actions it allowed — and correctness comes from ordering instead: any correction the server means to send travels in the same tick and earlier in the stream than the receipt. A correction replaces what the client remembered rather than being weighed against it, so when the receipt arrives the client writes back whatever the entry now holds — and only where that differs from what is on screen. Prediction and acknowledgement owns that machinery; this page and block breaking are its two applications.

The cast

classwhat it decidesthread
Minecraftthat a use-key press becomes one Minecraft.startUseItem, and that the main hand is tried before the off handRender
MultiPlayerGameModethe client’s copy of the whole decision, wrapped in one predictionRender
InteractionResultwhether the caller stops here, who swings, and what the hand ends up holdinga sealed interface of records, no thread
BlockBehaviour.BlockStateBasewhich of the two block hooks a state answers with, and when its six neighbours are asked to re-fiteither side
DoorBlockwhether this door opens by hand, what it writes, and what the other half becomeseither side
CollectingNeighborUpdaterthe order queued updates run in, and where a runaway cascade is cutwhichever thread wrote the block
ServerGamePacketListenerImplthe gate list the packet must pass, and what each refusal answers withServer
ServerPlayerGameModethe same inner order as the client, plus the advancement triggersServer

The whole click, both sides

sequenceDiagram
    box Client
        participant MC as Minecraft
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
        participant DB as DoorBlock
        participant CNU as Collecting<br/>NeighborUpdater
    end

    Note over MC,CNU: one client tick, all of it before the packet leaves
    MC->>MPGM: useItemOn, main hand first, with the BlockHitResult
    MPGM->>MPGM: startPrediction opens sequence n
    MPGM->>DB: useWithoutItem, after useItemOn returned TRY_WITH_EMPTY_HAND
    DB->>CL: setBlock, the lower half with OPEN cycled, flags 10
    CL->>CNU: shapeUpdate, six directions, limit 511
    CNU->>DB: updateShape on the upper half, direction DOWN
    CNU->>CL: setBlock, the upper half through Block.updateOrDestroy
    DB->>CL: playSound, the clicker as except

The client’s share of one click up to the moment the use packet is sent. The two writes are the page’s subject: the lower half is written by the door, the upper half by the shape pass that the first write set off, and no neighbour update happens at all.

Only now does the use packet leave the machine. It carries the hand, the hit and the sequence number, and the server runs the identical inner order against its own world.

sequenceDiagram
    box Client
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
    end
    box Server
        participant SGPL as ServerGamePacket<br/>ListenerImpl
        participant SPGM as ServerPlayer<br/>GameMode
        participant DB as DoorBlock
        participant SL as ServerLevel
    end

    MPGM->>SGPL: ServerboundUseItemOnPacket, hand, hit and n
    Note over MPGM,SGPL: no swing packet goes up: SUCCESS is predicted, so the client has already swung, and the server swings for the trackers itself
    Note over MPGM,SL: server tick, packets drained before the levels tick
    SGPL->>SGPL: ackBlockChangesUpTo n, then reach, hit box, height, spawn protection
    SGPL->>SPGM: useItemOn, the same three-step order
    SPGM->>DB: useWithoutItem
    DB->>SL: setBlock lower half, then the shape pass writes the upper half
    DB->>SL: playSound, everyone but the clicker, and gameEvent BLOCK OPEN
    SGPL-->>CL: two ClientboundBlockUpdatePackets, the clicked position and the face neighbour
    Note over MPGM,SL: still this tick, levels tick, then connections tick
    SL-->>CL: ClientboundSectionBlocks<br/>UpdatePacket, both halves in one section
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet for n
    CL->>CL: handleBlock<br/>ChangedAck<br/>ends prediction n

The server’s copy of the same click, and the three things that come back. The receipt arrives last, and by the time it does the client has already been corrected twice — which is why closing the prediction, ClientLevel.handleBlockChangedAck, writes nothing.

One press, one hand at a time

Minecraft.handleKeybinds runs from the client tick and only when no screen and no overlay is open. Every queued press of the use key becomes its own Minecraft.startUseItem, unthrottled; Minecraft.rightClickDelay gates the held-down auto-repeat alone, and Minecraft.startUseItem is what sets it, to four ticks. The whole of that method sits behind one test — MultiPlayerGameMode.isDestroying — so a use press arriving mid-dig does not merely skip the delay, it is discarded entirely. A player already using an item (drawing a bow, eating) gets no further either: Minecraft.handleKeybinds drains the queued presses and throws them away before Minecraft.startUseItem is reached. One level in, LocalPlayer.isHandsBusy stops a player who is paddling a boat.

Inside, after LocalPlayer.isHandsBusy, the hands are tried in the order InteractionHand.MAIN_HAND then InteractionHand.OFF_HAND. Each hand’s stack must pass ItemStack.isItemEnabled — a disabled item aborts the whole loop, not just its own hand — and then, for a BlockHitResult, the hand goes to MultiPlayerGameMode.useItemOn. A InteractionResult.Success or an InteractionResult.Fail ends the loop; only an InteractionResult.Pass falls through to MultiPlayerGameMode.useItem (right-click air, ServerboundUseItemPacket) and then to the off hand. The door returns success on the main hand, so the off hand is never asked.

MultiPlayerGameMode.useItemOn sends a ServerboundSetCarriedItemPacket first if the hotbar selection has moved (MultiPlayerGameMode.ensureHasSentCarriedItem), refuses outright if the target is outside the world border, and otherwise opens the prediction with MultiPlayerGameMode.startPrediction, which allocates sequence n, runs the whole client-side interaction inside it, and sends the ServerboundUseItemOnPacket the interaction returned. The ledger that prediction writes into belongs to the level rather than to the game mode: ClientLevel.setBlock hands every state it overwrites while predicting to a BlockStatePredictionHandler, so both halves of the door are recorded, and both are restored together if the receipt arrives with the entries unchanged.

Block, then empty hand, then item

MultiPlayerGameMode.performUseItemOn and ServerPlayerGameMode.useItemOn run the same three-step order. First the block is offered the item: BlockBehaviour.BlockStateBase.useItemOn, whose BlockBehaviour.useItemOn default answers InteractionResult.TRY_WITH_EMPTY_HAND. That sentinel — and only when the hand is InteractionHand.MAIN_HAND — routes to BlockBehaviour.BlockStateBase.useWithoutItem, whose BlockBehaviour.useWithoutItem default is InteractionResult.PASS. If the block consumed nothing, the item gets its turn through ItemStack.useOn, provided the stack is non-empty and not held back by ItemCooldowns.isOnCooldown. Sneaking skips the first two steps, but only when some hand holds something: the guard is Player.isSecondaryUseActive and a non-empty main or off hand, so an empty-handed sneak still opens the door.

Three things differ between the two copies, and none of them is the inner order. The server tests the block’s BlockBehaviour.requiredFeatures through FeatureElement.isEnabled as its very first statement — a feature flag narrowing the registry the click may reach (identifiers and registries) — while the client tests the same thing through ClientPacketListener.isFeatureEnabled inside the not-sneaking branch. A spectator gets a flat InteractionResult.CONSUME on the client, but on the server is routed to BlockBehaviour.BlockStateBase.getMenuProvider and may end up with an open container. And the advancement triggers exist only on the server: CriteriaTriggers.ITEM_USED_ON_BLOCK when the item did it, CriteriaTriggers.DEFAULT_BLOCK_USE when the empty-hand hook did, and CriteriaTriggers.ANY_BLOCK_USE from the packet handler for anything that consumed.

Those two hooks are how every right-clickable block in the game is written, and the split between them is lopsided: 25 block classes override BlockBehaviour.useItemOn, the ones that care what you are holding — ComposterBlock taking something compostable, LecternBlock taking a book, ChiseledBookShelfBlock reading the hit vector to pick a slot, JukeboxBlock, CakeBlock, RespawnAnchorBlock — while 52 override BlockBehaviour.useWithoutItem, the ones that answer an empty hand, of which the door is one. Twelve override both, all six just named among them. A block that does neither leaves the click to the item in the hand. That is the whole family, and this door is an instance of it rather than a special case.

The result is the vocabulary the whole pipeline turns on. InteractionResult is a sealed interface of four records — InteractionResult.Success, InteractionResult.Fail, InteractionResult.Pass and InteractionResult.TryEmptyHandInteraction — and the swing is part of it, not a separate decision. InteractionResult.SUCCESS, InteractionResult.SUCCESS_SERVER and InteractionResult.CONSUME are all InteractionResult.Success values differing only in InteractionResult.SwingSource: the client animates at once and the server animates for everyone else, the server animates for everyone including the clicker, or nobody does. InteractionResult.consumesAction is what both copies of the inner order test, and the server asks InteractionResult.Success.shouldSwing before it swings; the client’s hand loop in Minecraft.startUseItem never asks it, and matches on the record types instead. InteractionResult.Success carries two more answers besides — InteractionResult.Success.wasItemInteraction, which decides whether Stats.ITEM_USED is awarded, and InteractionResult.Success.heldItemTransformedTo, which both game modes use to swap the stack the hand ends up holding.

The door writes ten

DoorBlock.useWithoutItem asks BlockSetType.canOpenByHand and, if the answer is no, returns InteractionResult.PASS and lets the item try. For oak it is yes: StateHolder.cycle flips DoorBlock.OPEN, and Level.setBlock is called with flags 10 — Block.UPDATE_CLIENTS and Block.UPDATE_IMMEDIATE, with Block.UPDATE_NEIGHBORS clear. Then DoorBlock.playSound, LevelAccessor.gameEvent with GameEvent.BLOCK_OPEN or GameEvent.BLOCK_CLOSE (posting is a nested loop inside the caller’s own method), and InteractionResult.SUCCESS. Exactly this code runs on both sides.

Ten is the whole story of the page, so it is worth spelling out as bits: 2 + 8, with 1 absent. Bit 2 is Block.UPDATE_CLIENTS and it broadcasts. Bit 8 is Block.UPDATE_IMMEDIATE, and on the client LevelExtractor.blockChanged reads it not as immediate but as a player did this, which can buy the section a priority remesh — LevelRenderer acts on that mark only when the Chunk Builder option is set to prioritise nearby or player-affected sections, which the fancy and fabulous graphics presets do, and fancy is the preset a new client starts on. Bit 1 is Block.UPDATE_NEIGHBORS, and because it is absent Level.setBlock never reaches its neighbour fan-out — which on the client would be a no-op anyway. Nothing sets bit 16, Block.UPDATE_KNOWN_SHAPE, and that omission is what the shape channel’s section below is about. What the flags then feed, and the rest of what a write does, is the pair of flowcharts on blocks and states; everything below is the part of it the door walks.

The sound only you hear

DoorBlock.playSound passes the clicking player as the except entity, and the two sides read that word oppositely. ClientLevel.playSeededSound plays the sound only when the except entity is the local player; ServerLevel.playSeededSound broadcasts a ClientboundSoundPacket to everyone in range but them. So you hear your own prediction and never the server’s copy — and since each side draws its own pitch from its own Level.getRandom and its own seed from Level.soundSeedGenerator, your door is genuinely a different sound from the one your friend heard. Every deferral in this part that is not predicted does the opposite: a lever passes a null except entity, so the clicker hears the server’s packet like everybody else (signal and dust).

The shape channel, which both sides run

With Block.UPDATE_KNOWN_SHAPE clear and the update limit still positive, the tail of Level.setBlock calls BlockBehaviour.BlockStateBase.updateNeighbourShapes, which walks all six of BlockBehaviour.UPDATE_SHAPE_ORDER — west, east, north, south, down, up — and asks each neighbour, one top-level cascade at a time, whether it still fits. Note which direction travels: for the block above, the level is handed Direction.DOWN, the direction pointing from that neighbour back at the door. Each hop costs one from the limit, so the upper half is written at 511 and asks its own neighbours at 510.

That is why the door is predictable and the redstone lamp is not: the shape channel is the half of the update machinery a ClientLevel genuinely runs, and the neighbour channel is the half whose methods are empty there (blocks and states).

DoorBlock.updateShape answers four of its six callers with the state it was given: the whole method is behind a test for the vertical axis, so the four horizontals fall through to BlockBehaviour.updateShape, which returns the state unchanged. The other two directions are where the door lives, and besides leaving the state as it is there are three outcomes between them. Asked from the matching vertical direction — up for a lower half, down for an upper — it returns the neighbour’s own state with DoorBlock.HALF swapped to its own, so open, facing, hinge and powered are copied wholesale, which is why the top half is already open by the time it is written. Asked from that same direction when the neighbour is not the other half, it returns Blocks.AIR. And a lower half asked from Direction.DOWN returns air when DoorBlock.canSurvive fails — the block beneath must be face-sturdy upward. Block.updateOrDestroy then compares: a different non-air state becomes a Level.setBlock at the inherited limit, and air becomes a Level.destroyBlock — which the hub shows is the one branch of a shape update the client does not run (blocks and states). That is the whole of “break the bottom and the top pops”.

The updater underneath: a stack, drained depth-first

Every Level builds one Level.neighborUpdater, a CollectingNeighborUpdater, in its constructor — on the client too. Requests arrive as four small implementations of CollectingNeighborUpdater.NeighborUpdates, three of them records: CollectingNeighborUpdater.ShapeUpdate for the door’s case, CollectingNeighborUpdater.SimpleNeighborUpdate and CollectingNeighborUpdater.FullNeighborUpdate for a single neighbour, and CollectingNeighborUpdater.MultiNeighborUpdate, one request that walks up to six directions in NeighborUpdater.UPDATE_ORDER — the neighbour channel’s own order, and not the shape one this door walks.

CollectingNeighborUpdater.addAndRun decides where a request goes by whether a cascade is already running. The first one is pushed on CollectingNeighborUpdater.stack and drained immediately by CollectingNeighborUpdater.runUpdates; anything requested from inside a running hook lands in CollectingNeighborUpdater.addedThisLayer and is pushed on top of the stack before the current record’s remaining work, so the drain is depth-first — a cascade finishes its children before its siblings. NeighborUpdater.executeShapeUpdate and NeighborUpdater.executeUpdate do the actual calls and wrap any throw in a crash report.

The chain limit is coarser than it looks and gentler than folklore says. CollectingNeighborUpdater.count increments once per request, not per depth level and not per block touched — a CollectingNeighborUpdater.MultiNeighborUpdate is one request that can expand to six calls — and past CollectingNeighborUpdater.maxChainedNeighborUpdates further requests are silently dropped after a single logged error, never a crash. The count is reset when the outermost cascade unwinds, so the budget is per top-level cascade. It is not a game rule: it is the max-chained-neighbor-updates line in server.properties (DedicatedServerProperties.maxChainedNeighborUpdates, default one million, read through DedicatedServer.getMaxChainedNeighborUpdates), with MinecraftServer.getMaxChainedNeighborUpdates hard-coding the same number for the integrated server and ClientLevel passing the literal. Keep it distinct from Block.UPDATE_LIMIT, the 512 that bounds nested writes: that one counts recursion depth and is what the door’s 511 and 510 come from.

The gate list, and what each refusal answers with

ServerGamePacketListenerImpl.handleUseItemOn tests in this order, and the interesting column is the second one — the refusals do not answer alike, and one of them lies. The reach the third gate measures is an attribute, and players have two of them — one for blocks and a shorter one for entities (player anatomy).

the gatewhat the client gets when it fails
ServerGamePacketListenerImpl.hasClientLoadednothing, not even the receipt
ItemStack.isItemEnabled on the held stacknothing
Player.isWithinBlockInteractionRange, with 1.0 of slacknothing
the hit location lying within one block of the clicked block’s centre on every axis — a 2×2×2 box, not the blocknothing, plus a server-side log line naming the player
above LevelHeightAccessor.getMaxY or below LevelHeightAccessor.getMinYServerPlayer.sendBuildLimitMessage — an action-bar line, and no block update
MinecraftServer.isUnderSpawnProtectionServerPlayer.sendSpawnProtectionMessage, plus both block updates
a pending teleport, or ServerLevel.mayInteract refusing for the world borderServerPlayer.sendBuildLimitMessage — you are told you are building too high, whatever the real reason — plus both block updates
everything passedServerPlayerGameMode.useItemOn runs, plus both block updates

The two block updates are a ClientboundBlockUpdatePacket for the clicked position and one for the block on its clicked face, sent whatever the interaction returned — and they sit inside the branch below the build-height test, which is why a click that was out of reach or off the block is answered with silence while a click into spawn protection is answered with the truth. The door’s other half is in neither: it reaches the client with everyone else’s copy, through ChunkHolder.broadcastChanges, which turns two changed positions in one section into a single ClientboundSectionBlocksUpdatePacket (and into two ClientboundBlockUpdatePackets when the halves straddle a section boundary). ServerGamePacketListenerImpl.ackBlockChangesUpTo was called after the first gate and before the rest, and ServerGamePacketListenerImpl.tick emits the receipt when MinecraftServer.tickChildren reaches connections — after the levels have already broadcast (the server tick).

The other half of the same lecture

Left-click is this contract with a different pipeline: Minecraft.startAttack reaches MultiPlayerGameMode.startDestroyBlock, which opens its own prediction and sends a ServerboundPlayerActionPacket instead, and the block hook is BlockBehaviour.BlockStateBase.attack. Nothing about the ledger changes; everything about the clock does, because breaking the next page’s stone takes eight ticks where opening takes none. Block breaking takes it from there, and the two pages are deliberately the same shape.

Questions players ask

Why can’t I open an iron door by hand? Because DoorBlock.useWithoutItem’s first question is BlockSetType.canOpenByHand, false on BlockSetType.IRON and BlockSetType.GOLD and true on BlockSetType.COPPER. Nothing on this path reads BlockTags.WOODEN_DOORS — the copper door proves it, since it opens by hand and is not in that tag. The tag only feeds other tags, the axe’s and the doors mobs may open among them. Brain-driven mobs ask that one: InteractWithDoor reads BlockTags.MOB_INTERACTABLE_DOORS, the wooden doors and the copper ones, while the older goals read DoorBlock.isWoodenDoor, which is BlockSetType.canOpenByHand under another name.

Why does opening a door lag a busy server when nothing is powered? Because the write still reaches ServerLevel.sendBlockUpdated, which compares the old and new collision shapes and, when they differ, walks every navigating mob in the level asking whether the change is worth a re-route — the one place the world pushes back on pathfinding, and unbounded by distance (pathfinding). A door changes shape every time it moves. None of that exists on the client, which is one more reason your half of the click is the fast half.

Where to look

The click comes in at Minecraft.handleKeybinds and Minecraft.startUseItem, reaches the level through MultiPlayerGameMode.useItemOn and MultiPlayerGameMode.performUseItemOn, and is answered in InteractionResult. The two block hooks a state offers are BlockBehaviour.BlockStateBase.useItemOn and BlockBehaviour.BlockStateBase.useWithoutItem, and DoorBlock.useWithoutItem is this door’s. Level.setBlock and BlockBehaviour.BlockStateBase.updateNeighbourShapes are the write and the shape cascade, CollectingNeighborUpdater.addAndRun the queue underneath it, and DoorBlock.updateShape with Block.updateOrDestroy is where the top half is decided. On the server, start at ServerGamePacketListenerImpl.handleUseItemOn for the gates and ChunkHolder.broadcastChanges for how the other half reaches everyone else.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Block breaking

Verified against Minecraft 26.3 · Part V · A survival player holds left-click on stone with an iron pickaxe for eight ticks: two clocks that agree without talking, one loot roll, one cobblestone.

Hold the button on a stone block and two programs start counting. The client adds a fraction to MultiPlayerGameMode.destroyProgress every client tick and paints the crack; the server sets ServerPlayerGameMode.destroyProgressStart to the tick the dig began and recomputes, from scratch, how far along it ought to be. Between the first packet and the last, neither clock is ever mentioned on the breaker’s own connection — a ServerboundPunchPacket goes up and a ClientboundLevelEventPacket comes down every tick, for the swing and for the particles and the sound, and neither says how far along the dig is; the only progress that goes on the wire at all is the server’s, to other players, so that they see your cracks — and on the eighth tick the two answers are the same number. That agreement is what the whole design rests on, and it is also why the failure mode is so strange: once the client has finished, releasing the button cancels nothing. A client that finishes too early gets a deferral, not a rejection. The receipt for the STOP goes out in the same tick, the client dutifully puts the stone back — and then watches it vanish a second time when the server’s deferred break lands, with nothing the player can do in between.

The contract both halves run under. The client acts at once and remembers the state it overwrote, under a sequence number it sends with the action. The server’s ClientboundBlockChangedAckPacket is a receipt for that number and not a verdict — once the server considers the client loaded, it is sent for actions the server refused exactly as for actions it allowed — and correctness comes from ordering instead: any correction the server means to send travels in the same tick and earlier in the stream than the receipt. A correction replaces what the client remembered rather than being weighed against it, so when the receipt arrives the client writes back whatever the entry now holds — and only where that differs from what is on screen. Prediction and acknowledgement owns that machinery; block interaction and this page are its two applications.

The cast

classwhat it decidesthread
Minecraftthat the button is down and the crosshair is on a block, and whether this tick starts a dig or continues oneRender
MultiPlayerGameModethe client’s clock: accumulated progress, the five-tick pause after a break, when to predict the removal and send STOPRender
ClientLevelthe predicted air, and the crack overlays — every breaker’s, including the local player’sRender
ServerGamePacketListenerImplwhich of the nine ServerboundPlayerActionPacket.Actions this is, and when the receipt for its sequence is flushedServer
ServerPlayerGameModethe server’s clock, the reach and permission gates, the 0.7 verdict, and the deferralServer
BlockBehaviour.BlockStateBasehardness, whether the block needs the right tool for drops, and the per-tick fractioneither
Toolhow fast this stack mines this block, and whether it drops — two separate answers from one rule listeither
Blockthe particles and sound event, the stat, the exhaustion and the loot rollServer, and the breaker’s own client for the particles and sound

One dig, end to end

sequenceDiagram
    box Client
        participant MC as Minecraft
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
    end
    box Server
        participant SGPL as ServerGamePacket<br/>ListenerImpl
        participant SPGM as ServerPlayer<br/>GameMode
        participant SL as ServerLevel
    end

    Note over MC,SL: client tick 1: the button goes down, and the same lap runs the first continue too
    MC->>MPGM: startDestroyBlock, the block is not air
    MPGM->>CL: destroyBlockProgress, stage -1 at progress 0
    MPGM->>SGPL: ServerboundPlayerAction<br/>Packet START, N
    MC->>MPGM: continueDestroyBlock adds the first 0.133
    MPGM->>CL: destroyBlockProgress, my own crack at stage 1
    SGPL->>SPGM: handleBlockBreakAction, the start tick recorded
    SPGM->>SL: destroyBlockProgress, everyone else's crack within 32 blocks
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet N, nothing to reconcile

    loop client ticks 2-8, beside server ticks
        par the client adds
            MC->>MPGM: continueDestroyBlock adds 0.133
            MPGM->>CL: destroyBlockProgress, my own stage
            MC->>SGPL: ServerboundPunchPacket, no fields at all
        and the server recomputes the whole dig and throws the answer away
            SPGM->>SPGM: incrementDestroy<br/>Progress
            SPGM->>SL: destroyBlockProgress, only when the tenth changes
            SPGM->>SL: levelEvent with no source, sound every fourth tick
            SL-->>CL: ClientboundLevelEventPacket, the face but no progress
        end
    end

The two clocks, with the machines boxed and the loop in parallel compartments that have no order between them: the client adds a fraction, the server recomputes the lot, and what crosses between the boxes, a punch up and a level event down, carries neither clock’s count, which is the page’s whole argument.

The eighth add is where the two stop being symmetrical. It reaches about 1.067 on the client, and the client acts on its own number first.

sequenceDiagram
    box Client
        participant MPGM as MultiPlayer<br/>GameMode
        participant CL as ClientLevel
    end
    box Server
        participant SGPL as ServerGamePacket<br/>ListenerImpl
        participant SPGM as ServerPlayer<br/>GameMode
        participant SL as ServerLevel
        participant Block as Block
    end

    Note over MPGM,Block: still client tick 8: the eighth add reaches about 1.067
    MPGM->>CL: setBlock to air under flags 11, inside prediction M
    MPGM->>SGPL: ServerboundPlayerAction<br/>Packet STOP, M
    Note over MPGM,Block: a server tick, STOP handled off the task queue
    SGPL->>SPGM: handleBlockBreakAction STOP, its own 1.067 clears the 0.7 bar
    SPGM->>Block: playerWillDestroy, event 2001 to all but the breaker
    SPGM->>SL: removeBlock, the fluid-or-air state under flags 3
    SPGM->>Block: playerDestroy, once ItemStack.mineBlock has spent a durability point
    Block->>SL: the blocks/stone roll, then addFreshEntity with the ItemEntity
    Note over MPGM,Block: same tick, later: the levels broadcast, then the connections flush
    SL-->>CL: ClientboundBlockUpdatePacket air, absorbed
    SGPL-->>CL: ClientboundBlockChangedAck<br/>Packet M, air is already there

The break itself, in the order the server runs it. The client writes the air first and on its own authority, and everything that comes back — the block update and the receipt — finds a world that already agrees.

Two clocks, and the plus one that makes them agree

BlockBehaviour.getDestroyProgress is the shared formula, and both sides call it through BlockBehaviour.BlockStateBase.getDestroyProgress with the same arguments. It answers the fraction of the block broken per tick: the player’s speed, divided by the block’s hardness, divided by 30 when Player.hasCorrectToolForDrops says yes — which it does for every block that does not require a tool at all — and by 100 when it says no. Hardness −1 returns zero forever. Hardness zero is not special-cased, so an instabreak block divides by zero and returns infinity: that, and not a branch on hardness, is what sends the START handler down its insta-mine path on the first tick.

Player.getDestroySpeed builds the numerator from Inventory.getSelectedItem in one pass, and the surprising parts are all constants rather than data. The stack’s ItemStack.getDestroySpeed gives the base. If that is above 1.0 — only then — Attributes.MINING_EFFICIENCY is added, which is where Enchantments.EFFICIENCY lands, at level² + 1. Haste and conduit power are read together through MobEffectUtil.hasDigSpeed and MobEffectUtil.getDigSpeedAmplification, which returns the greater of the two amplifiers — a beacon and a conduit are interchangeable and do not stack — and multiply by 1 + 0.2 × (amplifier + 1). Mining fatigue is not an attribute at all but a literal power of 0.3, with the amplifier plus one as the exponent — 0.3, 0.09, 0.027 and on down for anything higher. Then Attributes.BLOCK_BREAK_SPEED, then Attributes.SUBMERGED_MINING_SPEED (0.2 by default) if the eyes are in FluidTags.WATER, and finally divide by five if the player is not on the ground.

For stone at hardness 1.5 and an iron pickaxe at 6.0, that is 6 ÷ 1.5 ÷ 30 = 0.133 per tick, so the eighth tick is the one that passes 1.0.

Why the two answers match

The two clocks count differently and still land on the same number, and the reason is a matter of when the server is asked rather than what it computes. The client accumulates: MultiPlayerGameMode.continueDestroyBlock adds one tick’s fraction each time it runs, and the first of those runs in the same client tick as the Minecraft.startAttack that opened the dig through MultiPlayerGameMode.startDestroyBlock, so after k client ticks the client holds k fractions. The server keeps no accumulator. For a live dig, ServerPlayerGameMode.incrementDestroyProgress multiplies the per-tick fraction by elapsed ticks plus one, and that plus one is exactly the client tick that both started and continued.

Now the timing. ServerPlayerGameMode.tick increments its own tick counter before recomputing, so the number it reaches inside the level tick is one fraction ahead of the client’s — and it uses that number only for the crack stage other players see, because the live branch throws the value away and only the delayed branch compares a value with 1.0. What the live branch sends every tick is a level event with no source entity, so the breaker is sent it too: LevelEvent.PARTICLES_DESTROY_PROGRESS, or LevelEvent.PARTICLES_AND_SOUND_DESTROY_PROGRESS on every fourth, naming the face being dug and not the progress. The comparison that decides anything happens somewhere else: the STOP is handled in the packet drain, before that tick’s increment (the server tick), so ServerPlayerGameMode.handleBlockBreakAction measures elapsed ticks over the same span the client counted and arrives at the same 1.067. The agreement is not a coincidence in the arithmetic; it is the packet drain running before the levels do.

Recomputing rather than accumulating has a second consequence: lose haste mid-dig and the server rescales the whole dig retroactively, while the client keeps the progress it already banked. Swapping tools is different: the client sees a new stack in MultiPlayerGameMode.sameDestroyTarget, sends an ABORT and a START, and both clocks begin again.

They agree without talking because almost every input is either static data both sides loaded — hardness, the block tags, the Tool component travelling with the stack — or a syncable attribute, or a synced effect. The inputs that could drift are the ones the client reports rather than shares, where the player stands and, sharpest of all, which slot is selected: MultiPlayerGameMode.ensureHasSentCarriedItem runs at the top of every MultiPlayerGameMode.continueDestroyBlock to send a ServerboundSetCarriedItemPacket the moment it changes.

The button is not the switch

Seventy per cent of the server’s own clock is what a STOP must have run before the block breaks immediately (ServerPlayerGameMode.handleBlockBreakAction).

For stone that is about two ticks of slack. Below the bar the STOP is not refused: unless a delayed dig is already pending, when it does nothing at all, the handler sets ServerPlayerGameMode.hasDelayedDestroy, copies the position and the original start tick into ServerPlayerGameMode.delayedDestroyPos and ServerPlayerGameMode.delayedTickStart, and leaves the rest to the delayed branch of ServerPlayerGameMode.tick. The sequence is acknowledged regardless — ServerGamePacketListenerImpl.handlePlayerAction calls ServerGamePacketListenerImpl.ackBlockChangesUpTo for all four break actions, unconditionally, after the game mode has run. The delayed branch does not count the ticks since the dig began: it multiplies the per-tick fraction by the start tick itself, plus one, and that tick is the game mode’s count since the player joined or respawned, so the answer does not grow with waiting. For any dig begun more ticks after that than it takes, the product clears 1.0 on the branch’s first run, later in the same server tick, and ServerPlayerGameMode.destroyBlock runs — after the level has broadcast that tick’s block changes. So the receipt still arrives with no correction in front of it, the client settles prediction M against the stone it recorded, and ClientLevel.syncBlockState puts the stone back. The block is visibly there again for a tick, until the next broadcast carries the air as an ordinary block update. The prediction was not wrong — it was undone and then redone.

Letting go changes nothing: a client that has sent its STOP is no longer digging, so releasing the button sends nothing, and an ABORT from a later dig clears ServerPlayerGameMode.isDestroyingBlock and erases the crack without touching ServerPlayerGameMode.hasDelayedDestroy — and ServerPlayerGameMode.tick tests the delayed dig first, before the live one. A delayed dig begun too soon after the player joined or respawned never clears on its own, and then starting a dig on a different block does not help either: the START is processed normally, but the delayed branch keeps winning the tick. The delayed path re-checks almost nothing on its way through — not reach, not MinecraftServer.isUnderSpawnProtection, not ServerLevel.mayInteract, not that the player is still in the same room. It calls ServerPlayerGameMode.destroyBlock directly rather than ServerPlayerGameMode.destroyAndAck, so two of the three ways it can fail there are silent, with no corrective block update. Inside the tick it ends only when the block turns to air, the one condition ServerPlayerGameMode.tick tests before recomputing progress, or when that recomputation, made with whatever the player now holds and has, reaches 1.0; otherwise it lasts until the player leaves or respawns.

What a real refusal looks like

The refusals that are refusals differ in what they send back, and the difference is observable. A failed Player.isWithinBlockInteractionRange check — which allows a full block of slack — sends nothing but the receipt, and it guards STOP as well as START; only an abort gets past it, and only while a dig is live, so an abort from too far away still ends one. Being above LevelHeightAccessor.getMaxY, failing ServerLevel.mayInteract or failing Player.blockActionRestricted each answer with a ClientboundBlockUpdatePacket carrying the true state. Spawn protection answers with an overlay message from ServerPlayer.sendSpawnProtectionMessage and no block update whatsoever. Every one of those exits is named in a string behind SharedConstants.DEBUG_BLOCK_BREAK, which is the best map of this state machine there is.

Worth holding the two click lectures side by side here, because the same two gates answer oppositely. Breaking above the build height sends the true state back; placing above it sends an action-bar line and no block update at all. Breaking inside spawn protection sends the message and no block update; placing there sends the message and both updates (block interaction). The rule is not the gate but the pipeline. A placement’s two corrective updates sit in one branch below the build-height test and go out for every outcome that reaches it, refusal or not. A break has no such branch: each of its five exits decides for itself. Three send the true state back — the build height, ServerLevel.mayInteract and Player.blockActionRestricted. Spawn protection sends its message and no state. And being out of reach sends nothing but the receipt, which puts the block back like any other; what a STOP refused for reach leaves wrong is the server, whose live dig goes on sending the breaker its level events until a START or ABORT it accepts, or until the block turns to air.

Speed and drops are two scans of one list

DataComponents.TOOL holds a Tool: a list of Tool.Rule, a Tool.defaultMiningSpeed, a Tool.damagePerBlock and a Tool.canDestroyBlocksInCreative. Each rule names a set of blocks and carries an optional speed and an optional drop verdict, and the game builds its rules in three shapes, each with a factory that says so: Tool.Rule.minesAndDrops carries both, Tool.Rule.deniesDrops carries only the verdict and Tool.Rule.overrideSpeed only the speed. Tool.getMiningSpeed and Tool.isCorrectForDrops are two independent walks of the same list, each taking the first rule that both matches the block and carries the field it came for. ToolMaterial.applyToolProperties builds every pickaxe, axe, shovel and hoe from exactly two rules, in this order:

the iron pickaxe’s rules, in orderwhat Tool.getMiningSpeed doeswhat Tool.isCorrectForDrops does
Tool.Rule.deniesDrops on BlockTags.INCORRECT_FOR_IRON_TOOL — no speed fieldskips itobsidian matches, answers no
Tool.Rule.minesAndDrops on BlockTags.MINEABLE_WITH_PICKAXE at 6.0obsidian matches, answers 6.0never reached
nothing matchedTool.defaultMiningSpeed, 1.0false

That is why an iron pickaxe mines obsidian and drops nothing. The speed scan falls through the deny rule — it has no speed — and takes the full pickaxe 6.0; the drop scan stops at the deny. The block still takes forever, but for a different reason: Player.hasCorrectToolForDrops is false, so BlockBehaviour.getDestroyProgress divides by 100 instead of 30. No item in the game uses all three shapes at once, and the two that never share a Tool are Tool.Rule.deniesDrops and Tool.Rule.overrideSpeed: the four mining tools pair a deny rule with a minesAndDrops rule, while shears and the sword — which deny nothing — are the only users of Tool.Rule.overrideSpeed. The sword is also one of exactly three whose Tool.canDestroyBlocksInCreative is false; the other two are the mace and the trident.

The cracks belong to everyone but you

ServerLevel.destroyBlockProgress sends a ClientboundBlockDestructionPacket to every player in the level within 32 blocks whose entity id is not the breaker’s. You are never sent your own cracks. What you see is MultiPlayerGameMode writing straight into ClientLevel.destroyBlockProgress each tick — the same method the packet handler calls, reached by a different road. The same asymmetry runs through the break itself: Block.playerWillDestroy posts level event 2001, which ServerLevel.levelEvent broadcasts within 64 blocks excluding the breaker, because the breaker already played it locally inside MultiPlayerGameMode.destroyBlock.

BlockDestructionProgress is a plain holder — id, position, progress, a last touched tick — and BlockDestructionProgress.setProgress clamps only the top, at 10. The 0–9 window everybody quotes is enforced by the caller: ClientLevel.destroyBlockProgress stores a stage only for values in [0, 10), and reads anything else — including the −1 that MultiPlayerGameMode.getDestroyStage returns at zero progress — as an instruction to remove that breaker’s entry. Entries are indexed twice, by breaker id and by position, the latter into a sorted set so the deepest crack at a position wins; LevelExtractor collects those within 32 blocks of the camera each frame. Entries untouched for 400 ticks are swept every twentieth tick, which is what eventually clears the cracks left by someone who disconnected mid-dig.

Remove, damage, roll, drop

ServerPlayerGameMode.destroyBlock runs a short gauntlet before it writes anything: ItemStack.canDestroyBlock, then GameMasterBlock against Player.canUseGameMasterBlocks, then Player.blockActionRestricted. It captures the BlockEntity first, because the write is about to destroy it. Then Block.playerWillDestroy — particles and sound to everyone else, piglins angered for BlockTags.GUARDED_BY_PIGLINS, and a GameEvent.BLOCK_DESTROY posted for sculk, and delivered before this method returns (the broadcast is a nested loop).

The write itself is Level.removeBlock, not Level.destroyBlock. It puts the fluid that was in the block back — water for a waterlogged block, air here — under flags 3, and everything that follows from those flags is the pair of flowcharts on blocks and states.

Drops come last and in a fixed order. If Player.preventsBlockDrops (creative) the method returns here. Otherwise the tool is copied, Player.hasCorrectToolForDrops is asked once and remembered, and ItemStack.mineBlock is called unconditionally — though what it does is conditional four ways over: a DataComponents.TOOL at all, the server side, a non-zero hardness and a damage-per-block above zero. Not one of those four is the drop verdict, which is the whole point and belongs to items and stacks. Only then, and only if the write succeeded and the remembered answer was yes, does Block.playerDestroy run: Stats.BLOCK_MINED, 0.005 of food exhaustion, and Block.dropResources.

The loot side is thin. Block.getDrops supplies LootContextParams.ORIGIN at the block centre, LootContextParams.TOOL and LootContextParams.THIS_ENTITY, BlockBehaviour.getDrops adds LootContextParams.BLOCK_STATE, and the set is LootContextParamSets.BLOCK. The table key is not looked up by name at break time: BlockBehaviour.Properties.effectiveDrops resolves the block’s id under blocks/ once, when the block is constructed. What blocks/stone then does is two lines of JSON — a silk-touch alternative, else cobblestone if it survives an explosion — rolled from a seeded per-table sequence rather than the level random (loot tables). Each surviving stack goes to Block.popResource, which respects GameRules.BLOCK_DROPS, jitters the position ±0.25 on all three axes around the block centre, gives the ItemEntity its small upward kick in the constructor and a ten-tick pickup delay. Ores add their experience afterwards, in BlockBehaviour.BlockStateBase.spawnAfterBreak.

Questions players ask

Why does my pickaxe lose durability on obsidian, which drops nothing, but not on short grass, which does? Durability is spent by ItemStack.mineBlock, which runs before the drop verdict is consulted and does not care about it — what it cares about is that the block’s hardness is non-zero and the tool has a damage-per-block above zero. Obsidian is hard and drops nothing: you pay. Short grass is Blocks.SHORT_GRASS, hardness zero: you never pay, whatever it drops. Shears are the exception at both ends — ShearsItem is the only override of Item.mineBlock in the game, and it tests neither hardness nor drops: the one thing it asks of the block is that it is not in BlockTags.FIRE. Shearing grass costs a point.

Why can’t I break blocks with a sword in creative? Because Tool.canDestroyBlocksInCreative is false on the sword’s component and ItemStack.canDestroyBlock checks it on both sides. It is a property of the item, not a special case in the game mode — which is why the client refuses first, and why the correcting block update the server sends back is a no-op: the client predicted nothing to correct.

For a 1.21-era reader. ServerboundSwingPacket is gone; on a dig, ServerboundPunchPacket does its work, and it names no hand.

Where to look

The two clocks are the thing to read first, and they are three methods: BlockBehaviour.getDestroyProgress is the shared formula, MultiPlayerGameMode.continueDestroyBlock is the client’s accumulator, and ServerPlayerGameMode.incrementDestroyProgress is the server’s recomputation. ServerPlayerGameMode.handleBlockBreakAction is the state machine behind all four break actions and the 0.7 bar, with ServerPlayerGameMode.tick for the delayed destroy it can leave behind. Then the ends: Minecraft.startAttack and Minecraft.continueAttack for where a dig begins each tick, ServerPlayerGameMode.destroyBlock for the gauntlet, and Block.playerWillDestroy with Block.playerDestroy and Block.popResource for what a broken block leaves behind. Tool.getMiningSpeed and Tool.isCorrectForDrops are the two walks of one rule list.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Block entities

Verified against Minecraft 26.3 · Part V · A furnace smelts raw iron while nobody is watching, and the player who opens it learns the fire from a block state and the arrow from a menu — never from the block entity itself.

You drop raw iron in the top slot of a furnace, coal in the bottom, and walk away. Two hundred ticks later there is an iron ingot in a box nobody is looking at. A block state is one of a fixed table, shared by every position that has it, so the moment a position needs something of its own — an inventory, a timer, a name — it gets a BlockEntity: a plain object owned by its chunk, keyed by its position, created by the block and destroyed with it. The surprise is how little that object says for itself. A furnace tells nobody anything: BlockEntity.getUpdatePacket returns null for it, BlockEntity.setChanged sends nothing at all, and the two things a player does see — the fire in the world and the arrow in the GUI — are a block state and four ints from a menu, both of which arrive on the tick after the smelting step that produced them, because block entities tick in the level’s last content phase, after the broadcast and the players’ ticks have gone by (the level tick).

The cast

classwhat it decidesthread
BlockEntitythe position, the cached state, the components, and four defaults every subclass inherits: two for disk, and two for the network that both amount to say nothingwhichever thread owns the level
BlockEntityTypewhich blocks the entity is legal on, and what constructs itimmutable once the registry is built
EntityBlockwhether a block has an entity at all, which one, and which ticker per level—
LevelChunkthe position-to-entity map, the ticker wrapper per position, and create / keep / replace / remove on every writethe chunk’s owning thread
Levelthe flat list of tickers and the two gates over itServer thread, or the Render thread
AbstractFurnaceBlockEntitythree slots, four ints, a cached recipe check, and when the block’s lit state has to changeServer thread only — its ticker is null on the client
ChunkHolderwhich positions changed since the last drain, and the call to BlockEntity.getUpdatePacket for each broadcast position that has a block entityServer thread, chunk-source phase
FurnaceMenuwhat an open screen is allowed to see of all that: three slots and four intsServer thread, mirrored on the client

A furnace tells nobody anything

BlockEntity has four hooks a subclass is expected to fill in, and they come in two pairs. BlockEntity.saveAdditional and BlockEntity.loadAdditional are the disk pair, and the base class’s versions do nothing because what it saves of its own, the id, the position and the components, is written by the shells around them. BlockEntity.getUpdateTag and BlockEntity.getUpdatePacket are the network pair, and their emptiness is a decision rather than an absence: the tag comes back empty and the packet comes back null. The base class declines to be synced, and a subclass that wants to be must say so.

Nineteen classes say so, and every one of them answers with ClientboundBlockEntityDataPacket.create of itself: signs, banners, beacons, skulls, spawners and trial spawners, conduits, end gateways, structure and jigsaw blocks, campfires, decorated pots, vaults, shelves, brushable blocks, creaking hearts, copper golem statues and the two test blocks. Counting those declarations in the decompile and mapping each class onto the 49 registrations in BlockEntityTypes gives twenty synced types out of forty-nine, because HangingSignBlockEntity is a type of its own that inherits SignBlockEntity’s override and adds nothing.

The overriders of the packet and the overriders of the tag are not the same list — there are nineteen of those too, and it is a different nineteen — and the two classes that differ are instructive. PistonMovingBlockEntity overrides BlockEntity.getUpdateTag but not the packet, so its state travels only in a chunk send (pistons and block events). CopperGolemStatueBlockEntity overrides the packet but not the tag, so what it broadcasts is the base class’s empty tag.

Everything else a client knows about a block entity (an operator’s query, a command block’s screen and the debug channel aside) it knows by consequence: the block state it can see, a menu it has been given, and a block event — the third channel, and the one that swings a chest lid without either side saying what is inside (pistons and block events). The trace below is what the first two cost.

One save hook, four ways out

Saving is a tree, not a chain, and the branch a caller picks decides how much metadata rides along. Only BlockEntity.saveAdditional belongs to the subclass; everything else is bookkeeping the base class adds.

what runswhat it writeswho calls it
BlockEntity.saveAdditionalthe subclass’s own fields, and nothing elsenobody directly
BlockEntity.saveCustomOnlythat alonethirteen of the tag overrides, the pick-block path, /clone and a few others
BlockEntity.saveWithoutMetadatathat plus componentsthe two below, plus BlockInput, the falling block and a few others
BlockEntity.saveWithIdthat plus ida structure template, which records a relative position of its own, and the adventure-mode block check’s cache
BlockEntity.saveWithFullMetadatathat plus id, x, y and zLevelChunk.getBlockEntityNbtForSaving, /data, the block predicates, a text component’s block NBT and a few others

Reading back is BlockEntity.loadWithComponents (fields plus components) or BlockEntity.loadCustomOnly (fields only) over a ValueInput (codecs, NBT and JSON) — but something has to decide which class to construct first, and that cannot come through a ValueInput, because no entity exists yet to own one. So BlockEntity.loadStatic reads id off the raw CompoundTag with BlockEntity.TYPE_CODEC, calls BlockEntityType.create, and only then wraps the same tag in a ValueInput and loads it. Any of those three steps failing logs and returns null, and the position has no entity until a plain read builds a blank one from the block state.

The network joins that path at the end rather than reusing it whole: ClientPacketListener.handleBlockEntityData never reads id — it asks for the entity by position and type, which a plain read builds from the block state if it is missing, and hands the tag to BlockEntity.loadWithComponents. There is no separate network deserialiser. Where the chunk’s block_entities list is written and read is chunk storage.

Create, keep, replace, remove

A block entity that appears because a block appeared is created and destroyed inside one method: LevelChunk.setBlockState, after the section write, the heightmaps and the light checks (what a write does). That is the lifecycle path, and it makes two decisions — but in the opposite order to the one this section is named for, because the old entity has to go before BlockBehaviour.BlockStateBase.onPlace runs and the new one is built after it. Removal first, then creation. It is not the only way one comes into being: a chunk arriving from disk builds its entities from saved tags, one arriving from the network from its block states and then their tags, and LevelChunk.getBlockEntity in its immediate mode constructs a missing one on a plain read — which is the mode every Level.getBlockEntity asks for.

Removal happens only when the block changed, the old state had an entity, and the new state does not claim it through BlockBehaviour.BlockStateBase.shouldChangedStateKeepBlockEntity — which exactly two blocks in 26.3 override, CopperChestBlock and CopperGolemStatueBlock, both keeping the entity when the old state was another block of the same family, so oxidising or waxing a copper chest does not empty it. Removal is two halves with different gates. The side effects — BlockEntity.preRemoveSideEffects, which for anything implementing Container drops the contents through Containers.dropContents — run only on the server and only with Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS clear. The bookkeeping, LevelChunk.removeBlockEntity, runs regardless — though only the last of its steps is itself unconditional. The map entry going, the game-event listener being unregistered, the debug view being told and the entity being flagged removed all sit behind is this chunk loaded in a level, which on the server a LevelChunk is not before its promotion finishes or once it is unloading; the rebind of the ticker to LevelChunk.NULL_TICKER happens either way.

Creation happens after BlockBehaviour.BlockStateBase.onPlace, and only if the new state has a block entity and BlockBehaviour.BlockStateBase.onPlace has not replaced the block. The chunk looks for an existing one without creating it, and if what it finds does not pass BlockEntity.isValidBlockState for the new state it logs a mismatched block entity warning, removes it and builds a fresh one from EntityBlock.newBlockEntity — the block’s own factory, not BlockEntityType.create. Only a surviving match is kept, with its cached state refreshed and LevelChunk.updateBlockEntityTicker re-asking the block for a ticker. That is why flipping a furnace’s lit property costs almost nothing: same block, valid state, same object — a fresh LevelChunk.BoundTickingBlockEntity rebound into the wrapper is the whole of the expense.

Chunk load and unload use the ends of the same machinery. ChunkStatusTasks.full runs LevelChunk.runPostLoad to turn the saved tags into entities, then LevelChunk.setLoaded and LevelChunk.registerAllBlockEntitiesAfterLevelLoad, which attaches listeners and tickers to entities built before the chunk belonged to a level. ServerLevel.unload calls LevelChunk.clearAllBlockEntities: every entity flagged removed, every ticker pointed at LevelChunk.NULL_TICKER.

Two hundred ticks nobody watches

sequenceDiagram
    participant SL as ServerLevel
    participant LC as LevelChunk
    participant AFBE as AbstractFurnace<br/>BlockEntity
    participant CH as ChunkHolder
    participant FM as FurnaceMenu
    participant CPL as ClientPacketListener
    Note over SL,CPL: tick N, chunk-source phase: the broadcast drain runs now, with nothing yet to send
    Note over SL,CPL: tick N, entities phase: the players' menus, with nothing new yet
    Note over SL,CPL: tick N, block-entities phase, the level's last content phase
    SL->>LC: LevelChunk.<br/>BoundTickingBlockEntity.tick
    LC->>AFBE: serverTick, quickCheck finds the smelting recipe
    Note over AFBE: fuel consumed, both lit fields take 1600, the cooking timer at one
    AFBE->>SL: setBlock, LIT true under flags 3
    SL->>LC: setBlockState, same block, the entity kept and the ticker rebound
    SL->>CH: blockChanged, which only marks the holder dirty
    AFBE->>SL: updateNeighbourForOutputSignal, the comparator poke
    Note over SL,CPL: tick N plus 1, chunk-source phase, the broadcast drain
    CH->>CPL: ClientboundBlockUpdatePacket, the fire appears
    Note over CH: BlockEntity.<br/>getUpdatePacket<br/>answers nothing
    Note over SL,CPL: tick N plus 1, entities phase, the players tick
    Note over CH,FM: AbstractContainerMenu.<br/>broadcastChanges checks the four data slots
    FM->>CPL: ClientboundContainerSet<br/>DataPacket per changed slot

Two consequences of one smelting tick, both a tick late because the block entity ticks in the level’s last content phase, after the drain that turns a dirty holder into a packet and after the players whose ticks reconcile the menu. The two routes out are the block state and the menu’s four ints, and they arrive by different machinery.

The furnace’s ticker is handed out by AbstractFurnaceBlock.createFurnaceTicker only when the level is a ServerLevel — on the client it is null, so no furnace anywhere ever ticks there. AbstractFurnaceBlockEntity.serverTick is therefore the whole of smelting: burn down AbstractFurnaceBlockEntity.litTimeRemaining, ask AbstractFurnaceBlockEntity.quickCheck (a RecipeManager.CachedCheck, which retries last tick’s match before scanning the type) for a recipe on the input slot, check that the result slot can take the output, and, if the fire is out but fuel is present, light it: both lit fields take AbstractFurnaceBlockEntity.getBurnDuration for that item — what its DataComponents.COOKING_FUEL component resolves to in this furnace, 1600 for coal — and one fuel item is consumed. Then AbstractFurnaceBlockEntity.cookingTimer advances by one. Where the recipe comes from is recipes.

The two hundred is the recipe’s number rather than the furnace’s: AbstractFurnaceBlockEntity.cookingTotalTime holds the recipe’s cookingtime — 200 in raw iron’s recipe, a field every cooking recipe must name — divided by the speed multiplier the fuel sets, which is one in a furnace. Nor is the timer a stopwatch that merely stops. With the fire out and either slot empty it runs backwards, two a tick, clamped at zero, so an interrupted smelt loses its progress in half the time it took to earn it.

Two writes leave the block entity, and neither leaves the server this tick. The first is the fire: lit-ness is a block state, so the ticker calls Level.setBlock on its own position with AbstractFurnaceBlock.LIT flipped. ServerChunkCache.blockChanged marks the holder dirty — and the drain that turns dirty holders into packets, ServerChunkCache.broadcastChangedChunks, lives in the chunk-source phase, which ran before entities and long before block entities. The second is BlockEntity.setChanged, which the tick calls only when it lit, finished or went out, as it did here: it marks the chunk unsaved and pokes comparators through Level.updateNeighbourForOutputSignal — the unconditional half of that walk, and the reason a comparator notices a hopper filling a chest (one int, and the fan-out that exists to deliver it) — and that is all it does. It sends nothing.

So a viewer sees both a tick late, by two different routes. Next tick’s drain sends the ClientboundBlockUpdatePacket and then — for every broadcast position whose state has a block entity, including each position inside a ClientboundSectionBlocksUpdatePacket — calls BlockEntity.getUpdatePacket, one of the game’s two call sites (the other resends the blocks under a player the server thinks is standing on air), and gets null from the furnace. Next tick’s entity phase runs ServerPlayer.tick, which runs AbstractContainerMenu.broadcastChanges, which compares the menu’s four data slots against the values last sent and emits a ClientboundContainerSetDataPacket per difference. Those four ints are the furnace’s entire GUI: the flame is data 0 over data 1 and the arrow data 2 over data 3, read by AbstractFurnaceMenu.getLitProgress and AbstractFurnaceMenu.getBurnProgress. The tick that lights the fire moves three of them, because data 1 is the fuel’s total burn duration; after that only 0 and 2 change, so a steady smelt is two packets a tick per open screen and none at all with no viewer. How a menu is opened, synchronised and closed is containers and menus.

On the fire’s two-hundredth tick, AbstractFurnaceBlockEntity.burn moves the ingot into the result slot and AbstractFurnaceBlockEntity.setRecipeUsed adds one to a counter map — not to a recipe object, which is why AbstractFurnaceBlockEntity.getRecipeUsed returns null. The experience is paid out on collection: FurnaceResultSlot.checkTakeAchievements, reached from a take or a shift-click, calls AbstractFurnaceBlockEntity.awardUsedRecipesAndPopExperience, which pops the orbs at the player and unlocks the recipes.

Loaded is not enough to tick

Three things about that list are worth separating: the gates over it, the way it maintains itself, and the fact that what a ticker does between calls is nobody’s business but its own.

The two gates, and the distance that is not view distance

Level.tickBlockEntities walks one flat list, Level.blockEntityTickers, under two gates the tickers themselves never see. TickRateManager.runsNormally is the first, so /tick freeze stops every block entity in the game. Level.shouldTickBlocksAt is the second, and it is where the interesting asymmetry lives: on Level it is always true, on ServerLevel it is DistanceManager.inBlockTickingRange — the simulation chunk tracker, not the loading one (tickets and loading). A chunk your view distance keeps loaded and your simulation distance does not reach holds furnaces that do not smelt, and nothing about the block entity records this: it is simply never called. Below the gates, LevelChunk.BoundTickingBlockEntity adds its own — not removed, adopted by a level, inside the world border, and then, on a ServerLevel only, the chunk at FullChunkStatus.BLOCK_TICKING with its entities loaded — before re-reading the live state and ticking only while BlockEntityType.isValid still holds, logging once and skipping while it does not.

The list that prunes itself

The list is never searched. Removal rebinds the chunk’s LevelChunk.RebindableTickingBlockEntityWrapper to LevelChunk.NULL_TICKER, whose TickingBlockEntity.isRemoved is permanently true, and the next pass of Level.tickBlockEntities drops it on the way past — no search, on a list that is walked every tick anyway. Additions made during the walk go to Level.pendingBlockEntityTickers and are folded in at the top of the next pass, so a block entity created by another block entity’s tick starts ticking one tick later. The client runs the same method from Minecraft.tick, after its entity pass and before ClientLevel.tick, and only while unpaused.

Three cadences the gates know nothing about

A ticker’s cadence is its own business, and the hopper is the first worth knowing: HopperBlockEntity moves one item and then sets a cooldown of eight of its own ticks, the number HopperBlockEntity.MOVE_ITEM_SPEED names, so a hopper chain runs at two and a half items a second at twenty ticks a second. The two spawners are the other two cadences worth knowing, and they are the block entities that keep their whole personality outside themselves. SpawnerBlockEntity holds an anonymous BaseSpawner and hands it the tick; the delay, the spawn count, the required player range and the rolled SpawnData all live on the BaseSpawner, which is not a BlockEntity at all. Both sides tick it, and they run different methods: BaseSpawner.serverTick does the spawning, and BaseSpawner.clientTick runs its own copy of the countdown to spin the cube and drop smoke and flame — a local animation, never a packet, and both are gated on BaseSpawner.isNearPlayer, so a spawner with nobody in range neither spins nor fires. TrialSpawnerBlockEntity is the same arrangement with a state machine on top: its TrialSpawner holds a TrialSpawnerStateData, the TrialSpawnerState it moves through is a property of the block state, and the block entity holds the TrialSpawner (entity lifecycle owns what either one spawns and under which reason). Every other block entity in the sub-package is one of the shapes on this page — four hooks, a ticker handed out per level, the four save shells, and the create-keep-replace-remove lifecycle — with different fields in the middle.

Questions players ask

Why doesn’t the client know what is in a chest until I open it? ChestBlockEntity overrides neither sync hook, so a chunk send carries its type and position with no tag at all (an empty update tag is sent as none) and the client builds its chest from the block state the packet’s sections already decoded, with an empty container inside. What ticks on the client is animation only: ChestBlock.getTicker hands out ChestBlockEntity.lidAnimateTick on the client and null on the server, the exact mirror of the furnace.

Why does a shulker box keep its contents when every other container drops them? Because dropping is the base class’s behaviour, not the block’s: BlockEntity.preRemoveSideEffects drops the contents of anything that implements Container. Eight classes override that hook, and ShulkerBoxBlockEntity is the one that overrides it to do nothing whatever. The furnace overrides it too and calls the base version first, so it drops like everything else and then pops the experience owed for uncollected smelts at the block — awarding the recipes to nobody.

Why does the arrow only move when the screen is open? Because the arrow is not a property of the furnace. It is data slot 2 of an AbstractContainerMenu that exists only while a player has that screen open, reconciled once per tick by the player who owns it. Close the screen and the menu is gone, and the furnace goes on smelting with no packets at all.

Where to look

Start with the two hooks that decide everything else: BlockEntity.getUpdatePacket and BlockEntity.setChanged, neither of which sends anything for a furnace. BlockEntity.loadStatic is the one place a class is chosen from a tag, and BlockEntity.preRemoveSideEffects is the last word before one is unregistered. The lifecycle is LevelChunk.setBlockState with LevelChunk.updateBlockEntityTicker and LevelChunk.NULL_TICKER, and the ticking is Level.tickBlockEntities over Level.shouldTickBlocksAt. Then AbstractFurnaceBlockEntity.serverTick for the smelt itself and ChunkHolder.broadcastBlockEntity for the call site that asks a block entity for a packet whenever its position is broadcast. EntityBlock.getTicker and BaseEntityBlock.createTickerHelper are not named above and are the door into how any block hands out a ticker at all.

How a block entity is drawn — and why a chest’s block model is empty — is block-entity rendering, in Part XI.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Signal and dust

Verified against Minecraft 26.3 · Part V · A lever on the floor is flipped on and then off again, and two redstone dust to the east of it go to 15 and 14 — and then count their way back down.

You flip a lever, and the dust beside it turns bright. Flip it back and the line goes dark — in one tick, but not in one pass. Each wire recomputes its own strength from scratch, writes it, and then hand-issues seven Level.updateNeighborsAt calls: its own position and all six neighbours. Each of those seven visits six neighbours of its own, so one wire whose power changed costs forty-two neighbour updates (DefaultRedstoneWireEvaluator.updatePowerStrength). And because the recursion stops on value rather than on distance, the wire at the far end is reached once for every intermediate value the near end passes through on the way down. That staircase is the whole cost of redstone dust, and nobody has ever seen it: the cascade finishes inside one packet handler, and the packet a client is sent is built later in the same tick from whatever the position holds by then. The game ships a second implementation of exactly this computation, behind a feature flag, which walks the whole connected network in two ordered phases and does not produce the staircase at all.

The cast

classwhat it decidesthread
SignalGetterevery question about power: what a position emits, what reaches it, and the direction order the answers are gathered ina Level interface, either side
BlockBehaviour.BlockStateBasethe three power answers this trace asks a state for — is it a source, what is its weak signal per face, what is its strong signal — beside whether it conducts and whether a wire connects to it. The analog pair is the comparator’seither side
LeverBlockthe trace’s source: 15 in every direction, and 15 strongly into one block onlyserver — the client’s copy writes nothing
RedstoneWireBlockwhich sides a wire connects to, what it emits through them, and the mutable flag that stops it counting itselfserver for power; its connections on both sides
RedstoneWireEvaluatorminus one per block: what the neighbouring wires are worth to this oneserver
DefaultRedstoneWireEvaluatorone wire at a time, recursively, with the fan-out issued by handserver
ExperimentalRedstoneWireEvaluatorevery wire the change reaches, at once, in two phases, everything off before anything on, allocated fresh per callserver
Orientationin the experimental mode only, where an update came from, so the fan-out can be ordered relative to itserver

What one neighbour update to a wire costs

Redstone dust has no scheduler and no graph. It is what happens when blocks answer two questions about their neighbours — how much signal do you give me and did something near you change — through the same BlockBehaviour.neighborChanged fan-out every neighbour update uses. This is the whole of the default implementation, from one update arriving to the next batch leaving.

flowchart TD
    IN["RedstoneWireBlock.neighborChanged"]
    CLIENT{"is this a ClientLevel"}
    NOTHING["nothing at all"]
    CALC["DefaultRedstoneWireEvaluator.calculateTargetStrength"]
    BLK["RedstoneWireBlock.getBlockSignal, with RedstoneWireBlock.shouldSignal off"]
    WIRE["RedstoneWireEvaluator.getIncomingWireSignal"]
    TARGET["the target: the block signal, or the larger of the two"]
    SAME{"is the target the POWER already stored"}
    STOP["no write, no fan-out: the cascade ends here"]
    WRITE["Level.setBlock under Block.UPDATE_CLIENTS alone"]
    FAN["seven Level.updateNeighborsAt calls: this position and its six neighbours"]
    OUT["forty-two neighbour updates, queued a layer at a time"]
    IN --> CLIENT
    CLIENT -- "yes" --> NOTHING
    CLIENT -- "no" --> CALC
    CALC --> BLK
    BLK -- "under 15" --> WIRE --> TARGET
    BLK -- "15, so the wires are never asked" --> TARGET
    TARGET --> SAME
    SAME -- "yes" --> STOP
    SAME -- "no" --> WRITE --> FAN --> OUT
    OUT -- "every wire among them, again" --> IN

One neighbour update to one wire, and the loop at the bottom is the page: forty-two updates leave, some of them land on wires, and each of those re-enters at the top. The cascade ends only where a recomputed target equals the RedstoneWireBlock.POWER already stored, which is a test on the value and not on the distance.

The first branch is belt and braces. RedstoneWireBlock.neighborChanged opens with a not-client test, and it would never be reached on a ClientLevel anyway: Level.updateNeighborsAt and Level.neighborChanged are empty methods there, so nothing on the client ever dispatches a neighbour update to a block at all (blocks and states).

The two facts that make the staircase are both in that figure. The write uses Block.UPDATE_CLIENTS alone — flag 2, the broadcast bit and nothing else (block update flags) — so the fan-out is not the one Level.setBlock would have done: it is issued afterwards, by hand, over seven positions rather than one. And the recursion terminates on value, not on distance: a wire whose recomputed strength equals what it already holds writes nothing and tells nobody. A line going dark therefore re-enters every wire again at every step of the descent, and each visit is the last one only when the value has stopped moving. None of those intermediate writes is ever sent: Level.sendBlockUpdated records the position and not the state, and the once-a-tick flush reads the level back to build the packet (what the client is told).

What a block answers when it is asked for power

Three questions, all on BlockBehaviour.BlockStateBase, all answered by the block. BlockBehaviour.BlockStateBase.isSignalSource is whether the block emits at all. BlockBehaviour.BlockStateBase.getSignal is the weak signal it offers to a given face. BlockBehaviour.BlockStateBase.getDirectSignal is the strong one, and the difference between them is entirely a matter of who is allowed to pass it on. Conduction is BlockBehaviour.BlockStateBase.isRedstoneConductor, from BlockBehaviour.Properties.isRedstoneConductor, which defaults to is this state’s collision shape a full block.

SignalGetter is where the two meet, and the join is easy to get backwards. SignalGetter.getSignal reads the block’s own weak signal and then, only if that block is a redstone conductor, takes the larger of it and SignalGetter.getDirectSignalTo — the strongest signal being pushed into that position from any of its six neighbours. It is a maximum, not a choice between two modes: a powered conductor offers the greater of what it emits itself and what is being forced into it. That single line is what “strongly powered” means, and it is why a block with a lever on it powers the dust beside it. A conducting block a wire merely points into is strongly powered too — that is how a line lights a lamp through the block at its end — but no other dust can see it, for a reason that has nothing to do with this line and everything to do with RedstoneWireBlock.shouldSignal, below.

The lever shows both halves at once. LeverBlock.ownSignal is 15 in every direction when powered — that is the weak signal, and it is what the dust next to the lever reads. LeverBlock.getDirectSignal is 15 only into the one block the lever is attached to. So a conducting block behind a lever is strongly powered, and everything touching that block sees 15 too.

The other sources answer the same three questions and differ mostly in what they answer and when they change their mind: ButtonBlock books a scheduled tick to turn itself off, BasePressurePlateBlock and its two subclasses re-read what is standing on them, DetectorRailBlock watches for entities and TripWireHookBlock for its tripwire, DaylightDetectorBlock reads the sky, and RedstoneTorchBlock and RedstoneWallTorchBlock invert whatever they are attached to. None of these needs a section of its own, because the contract above is nearly the whole of what a circuit sees; the repeater and the observer, which also answer for themselves whether a wire connects, belong to the part’s last lecture.

One of them keeps state the contract cannot see, and it is the reason a fast clock burns out. RedstoneTorchBlock.RECENT_TOGGLES is a weak map from level to a list of toggles; RedstoneTorchBlock.tick prunes anything older than 60 ticks off the front of it, and RedstoneTorchBlock.isToggledTooFrequently burns the torch out on the eighth surviving entry for that position, to try again 160 ticks later. Those are the numbers RedstoneTorchBlock.MAX_RECENT_TOGGLES, RedstoneTorchBlock.RECENT_TOGGLE_TIMER and RedstoneTorchBlock.RESTART_DELAY name: 8, 60 and 160.

Three direction orders, and only one of them is about reading

Three fixed direction orders run through this page and they are not interchangeable. Two decide who gets told something; the third decides what a block reads, and it is the one this page uses.

arrayorderwhat it governs
SignalGetter.DIRECTIONSdown, up, north, south, west, eastwhat a block reads. Only SignalGetter.getBestNeighborSignal walks the array; SignalGetter.getDirectSignalTo and SignalGetter.hasNeighborSignal are written out by hand in the same order
NeighborUpdater.UPDATE_ORDERwest, east, down, up, north, southwhich neighbour is told first about a change, on the neighbour channel
BlockBehaviour.UPDATE_SHAPE_ORDERwest, east, north, south, down, upwhich neighbour is asked first to re-fit, on the shape channel

Only the first row is about reading, and the three reading methods in it stop early, not on the same thing: the two that return a number — SignalGetter.getBestNeighborSignal and SignalGetter.getDirectSignalTo — stop at a 15, while SignalGetter.hasNeighborSignal, which returns a boolean, stops at the first answer above zero. That is not a micro-optimisation with no consequences: a position saturated from one side never reads the sides after it. The other two rows are orders in which something is told, and a fan-out never stops early. SignalGetter.DIRECTIONS is plain Direction order, which is the only reason its first entry is down.

Dust, and how far it reaches

RedstoneWireBlock.POWER is the number, 0 to 15. Four RedstoneSide properties — one per horizontal, each NONE, SIDE or UP — record how the wire is drawn and, more importantly, which sides it talks through. What a wire is worth to its neighbours is RedstoneWireEvaluator.getIncomingWireSignal, and the minus one per block everyone knows lives in its last line: it takes the best of the wires it can see and subtracts one, floored at zero. That subtraction is the whole of the reach: in a line whose first wire holds 15, the fifteenth holds 1 and the sixteenth 0, which is a dark block rather than a shorter line. The wires it can see are the four beside it, plus the wire on top of a conducting neighbour when nothing conducts above this position, plus the wire below a non-conducting neighbour — which is the power half of “dust climbs a block and falls down one”. The drawing half is RedstoneWireBlock.getConnectingSide below, which asks BlockBehaviour.BlockStateBase.isFaceSturdy for the climb where this one asks about conduction.

Two asymmetries follow from RedstoneWireBlock.getSignal and are worth stating plainly, because they are the two questions every redstone build eventually asks. A wire returns zero when the direction asked about is Direction.DOWN — so dust never powers the block above it. It returns its full power without any connection test when the direction is Direction.UP — so dust always powers the block below it. In every other direction it answers only if its connection on the opposite side is made.

Connection itself is two rules and a completion pass. RedstoneWireBlock.shouldConnectTo is the real one, and it asks the neighbour’s own BlockBehaviour.shouldRedstoneWireConnectTo: another wire always, a RepeaterBlock along its own axis, an ObserverBlock only on its output side, opposite the one it watches, and by default any block that says it is a signal source — that last clause only when a direction is supplied, which the vertical rules do not do, so up and down connect to wire and to nothing else. RedstoneWireBlock.getConnectingSide adds the vertical cases — up over a face-sturdy neighbour, down past a non-conducting one. And then RedstoneWireBlock.getConnectionState runs a completion pass that produces most of the confusion: if a wire has no north or south connection, west and east are set anyway, and the same the other way round. That is why a lone dust is drawn as a cross, and why a wire fed from the west appears to point firmly into whatever is on its east — a piston, say — which satisfies neither real rule. The piston is not a source and, by Blocks.pistonProperties, not a conductor. It gets powered anyway, because the wire’s east side is SIDE by completion and RedstoneWireBlock.getSignal asks about the side, not about the neighbour.

RedstoneWireBlock.shouldSignal is the oddest thing on the page: a mutable boolean on the block singleton, flipped false for the duration of RedstoneWireBlock.getBlockSignal so that a wire does not count itself or its neighbouring wires as sources while it works out its block power. It works because the Server thread is the only writer and never re-enters the method. It is also the answer to the question left open above: a conducting block a wire merely points into is strongly powered, and no other dust can see that, because while any wire is asking what its block power is, every wire in the world temporarily answers that it is not a source at all.

The lever, two dust, and a powered piston

sequenceDiagram
    participant LevB as LeverBlock
    participant SL as ServerLevel
    participant CNU as Collecting<br/>NeighborUpdater
    participant RWB as RedstoneWireBlock
    participant DRWE as DefaultRedstone<br/>WireEvaluator
    participant PBB as PistonBaseBlock
    Note over LevB,PBB: one call stack, inside one packet handler, before the level ticks
    LevB->>SL: setBlockAndUpdate, POWERED with flags 3
    SL->>CNU: updateNeighbors<br/>AtExceptFromFacing<br/>drained at once
    CNU->>RWB: neighborChanged at the first dust
    RWB->>DRWE: updatePowerStrength, and the lever answers 15
    DRWE->>SL: setBlock POWER 15 with flag 2, then seven updateNeighborsAt
    CNU->>RWB: neighborChanged at the second dust, ahead of the lever's other directions
    RWB->>DRWE: updatePowerStrength, block signal 0 and a wire at 15
    DRWE->>SL: setBlock POWER 14 with flag 2, then seven more
    CNU->>PBB: neighborChanged, and the wire answers from its east side
    Note over CNU,PBB: the remaining dozens of updates change nothing
    LevB->>SL: updateNeighborsAt, two more fan-outs by hand

The same cascade in time, and the thing to watch is the order: the second dust’s whole turn — its recompute, its write and its seven fan-outs — finishes before the lever has issued its own remaining directions. That is the queue being drained depth-first, inside one call stack, with no tick anywhere in it.

The lever is the place in this trace where the client does not even try. LeverBlock.useWithoutItem writes no state on a ClientLevel — it spawns a particle, and only when the lever is going on — so unlike a door, a lever is not predicted, and the client’s dust changes colour only when the block updates arrive. The dust and the piston do nothing on the client either, but for the ordinary reason: nothing ever calls their BlockBehaviour.neighborChanged there. The sound follows the same split for a different reason: LeverBlock.pull is handed a null player, so nobody is excluded and the clicker hears the server’s ClientboundSoundPacket like everyone else. Compare block interaction, where the door passes the clicker as except and they hear their own prediction instead.

Two details in the diagram are worth naming. The lever writes through LevelWriter.setBlockAndUpdate, which is flags 3, Block.UPDATE_ALL, so Block.UPDATE_NEIGHBORS fans out once before LeverBlock.updateNeighbours fans out twice more, at the lever’s own position and at the block it stands on — and because nothing was running when the first one was queued, it drains the entire dust cascade before the other two are even issued. And the ordering of the seven positions a wire updates is fixed by no array: they come out of a hash set. The depth-first drain of CollectingNeighborUpdater is the subject of block interaction, and it is what puts the second dust’s whole cascade ahead of the lever’s remaining directions.

Placing and breaking a wire take a wider path again: RedstoneWireBlock.onPlace and RedstoneWireBlock.affectNeighborsAfterRemoval both call RedstoneWireBlock.updateNeighborsOfNeighboringWires, which walks the four horizontals and then the diagonals — reaching over a conducting neighbour and under a non-conducting one — and calls RedstoneWireBlock.checkCornerChangeAt on each. That is another seven Level.updateNeighborsAt per wire found. Which write does what is blocks and states.

The second implementation

FeatureFlags.REDSTONE_EXPERIMENTS is a feature flag (identifiers and registries), turned on by a built-in data pack whose entire content is the line that enables it (the resource system), and RedstoneWireBlock.useExperimentalEvaluator asks the level for it on every call — RedstoneWireBlock.evaluator is always the default one, and an ExperimentalRedstoneWireEvaluator is a fresh object per update, because it carries working state. What changes is the semantics, and the cost with it.

Run the same lever back the other way — off, so the line has to go dark, which is where the default evaluator does its counting down — and follow the two dust through ExperimentalRedstoneWireEvaluator.calculateCurrentChanges instead. Nothing is written until the whole connected network has been computed. Phase one drains ExperimentalRedstoneWireEvaluator.wiresToTurnOff, and this is the phase that kills the staircase: a wire whose recomputed power is lower than what it holds goes to zero in a working map rather than to its new value, so the two dust do not count down past each other, 13 and 12, 11 and 10, on the way to nothing — each goes straight to nothing, and each is re-queued for the second phase only if it has block power of its own or a stronger wire beside it. Phase two drains ExperimentalRedstoneWireEvaluator.wiresToTurnOn and raises each survivor to its true value, which reaches the world in one write. Both phases spread through ExperimentalRedstoneWireEvaluator.propagateChangeToNeighbors and ExperimentalRedstoneWireEvaluator.enqueueNeighborWire, recording every wire they reach in ExperimentalRedstoneWireEvaluator.updatedWires, an insertion-ordered map of position to a packed orientation and power — which is the working state that makes the evaluator a fresh object per call. The write pass at the end drops any entry whose stored power already matches, so what reaches the world is exactly the wires that really changed — each with Block.UPDATE_CLIENTS and, for every wire the pass touches, Block.UPDATE_SKIP_SHAPE_UPDATE_ON_WIRE, the bit that makes NeighborUpdater.executeShapeUpdate skip any shape update whose target is dust. (The one wire exempted from that bit is the one an evaluation started from, and then only when the caller was RedstoneWireBlock.onPlace, which wants the shape updates around a newly placed wire.)

The fan-out is the second difference, and it is what the seven positions become. ExperimentalRedstoneWireEvaluator.causeNeighborUpdates issues one Level.neighborChanged per connected side per changed wire — the four horizontals the wire’s own state records, plus Direction.DOWN unconditionally and Direction.UP never — in Orientation.getDirections order, which is derived from where the update came from rather than from a fixed array, and then five more at any side that is a redstone conductor. So the piston east of our two dust is still told, and told once, without the seven-position scattergun. The recursion closes because RedstoneWireBlock.neighborChanged ignores wire-sourced updates entirely in this mode; the default evaluator does not, which is what opens it.

For a 1.21-era reader. BlockBehaviour.neighborChanged now takes a nullable Orientation rather than a source BlockPos — which is what lets the experimental evaluator order a fan-out relative to where the update came from — and BlockBehaviour.affectNeighborsAfterRemoval, which replaced the old removal hook, does not take one at all. RedStoneWireBlock is now RedstoneWireBlock, and the repeater and observer tests that RedstoneWireBlock.shouldConnectTo held are gone from it: each block answers BlockBehaviour.shouldRedstoneWireConnectTo for itself.

Where to look

The three questions a block answers are BlockBehaviour.BlockStateBase.isSignalSource, BlockBehaviour.BlockStateBase.getSignal and BlockBehaviour.BlockStateBase.getDirectSignal; SignalGetter.getSignal combines the last two with BlockBehaviour.BlockStateBase.isRedstoneConductor, the strong half through SignalGetter.getDirectSignalTo, and SignalGetter.getBestNeighborSignal over SignalGetter.DIRECTIONS is how a position is read. The trace starts at LeverBlock.pull with LeverBlock.updateNeighbours, enters the wire at RedstoneWireBlock.neighborChanged, and the number is decided by RedstoneWireBlock.getBlockSignal against RedstoneWireEvaluator.getIncomingWireSignal, with DefaultRedstoneWireEvaluator.updatePowerStrength doing the write and the fan-out. For the drawing rules read RedstoneWireBlock.getConnectionState and BlockBehaviour.shouldRedstoneWireConnectTo; for what leaves a wire, RedstoneWireBlock.getSignal. The second implementation is ExperimentalRedstoneWireEvaluator.calculateCurrentChanges and ExperimentalRedstoneWireEvaluator.causeNeighborUpdates. SignalGetter.getControlInputSignal is not named above and is the door into what a diode reads from its sides.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Pistons and block events

Verified against Minecraft 26.3 · Part V · A powered piston pushes one stone block, and the client is never told where the moving blocks are.

A piston cannot act when it is asked. A repeater told about a change books a turn in the appointment book and a wire recomputes itself on the spot; PistonBaseBlock.checkIfExtend does neither. It appends a four-value record to a set on the level and returns, and the push happens later, in a phase of the level tick named for exactly this. What comes out the other side is stranger still: in a push that destroys nothing, nothing the push writes sends a block update for the moving blocks. The placeholders the server writes carry Block.UPDATE_CLIENTS clear, so nothing incremental is generated for them, and the copy on your screen exists only because your client re-ran PistonBaseBlock.moveBlocks itself against its own world, off a single ClientboundBlockEventPacket. It is not a prediction that gets confirmed. Nothing checks that the two animations agree; if that one packet is lost you watch nothing move, and only the landing write puts the block where everyone else already sees it.

The cast

classwhat it decidesthread
BlockEventDatathe record itself: a position, a block, and two intsa record, no thread
ServerLevelthat a block event is queued rather than run, and the one phase per tick in which the queue drainsServer
Levelthat on any other level — which in practice means ClientLevel — a block event runs immediatelyRender
PistonBaseBlockwhether to extend, which of three events to raise, and — at the drain, having re-checked — what actually movesServer, then the client’s copy
PistonStructureResolverthe set of blocks that move and the set that is destroyed, or a flat refusaleither side, allocated per attempt
MovingPistonBlockthe placeholder state that occupies a position during the motioneither side
PistonMovingBlockEntitythe two ticks of motion, the entities shoved along, and what is left behind at the endboth sides, in the block-entity phase
PistonHeadBlockthe arm once the motion is over, and forwarding neighbour updates back to the baseServer

The queue, and which tick it drains in

Level.blockEvent runs BlockBehaviour.BlockStateBase.triggerEvent on the spot. ServerLevel.blockEvent overrides it to add a BlockEventData to ServerLevel.blockEvents and return. That single override is the whole mechanism, and it is the reason the two sides of a piston behave so differently: the server always defers, the client never does.

The drain is ServerLevel.runBlockEvents, in the blockEvents section of ServerLevel.tick, after tickPending and chunkSource and before entities (the level tick). It is worth being exact about what that timing means, because “a block event is a tick late” is only sometimes true:

  • Queued by a packet handler — the same tick. MinecraftServer.processPacketsAndTick drains the queued packets and then calls MinecraftServer.tickServer in the same lap (the server tick), so a lever a player flipped is handled before the level ticks at all, and the event it raised is drained in that same level tick.
  • Queued by a scheduled tick — the same tick. tickPending runs before blockEvents, so a repeater firing into a piston is also drained immediately (scheduled ticks).
  • Queued by another block event — the same tick. ServerLevel.runBlockEvents drains until the set is empty, so an event raised while the drain is running is taken by the same drain.
  • Queued by an entity or a block entity — the next tick. Those phases run after blockEvents, so anything they raise waits a full lap. A landing PistonMovingBlockEntity is one step short of this group: it raises no event itself, but the neighbour bit of its landing write can reach a neighbouring piston, whose PistonBaseBlock.checkIfExtend then queues one for next tick.
  • In a chunk that is not block-ticking — parked. Such an event goes to ServerLevel.blockEventsToReschedule and is re-added after the loop, so it is retried next tick rather than dropped.

ServerLevel.blockEvents is a linked hash set, so two identical events pending at once collapse into one. And ServerLevel.doBlockEvent re-reads the position and runs the event only if the block there is still the block the event named — the same promise a scheduled tick makes. When BlockBehaviour.BlockStateBase.triggerEvent returns true, and only then, a ClientboundBlockEventPacket goes to every player within 64 blocks.

Who else uses the channel

The piston is the mechanism’s most demanding customer but not its only one. Three blocks raise events directly — PistonBaseBlock, NoteBlock and PotentSulfurBlock, the geyser, whose event carries no data at all and exists only to have each side stamp its own game time on its block entity as the eruption’s start — and seven block entities raise their own, reaching themselves back through BaseEntityBlock.triggerEvent: ChestBlockEntity, EnderChestBlockEntity, ShulkerBoxBlockEntity, BellBlockEntity, DecoratedPotBlockEntity, SpawnerBlockEntity and TheEndGatewayBlockEntity (a chest lid, an ender chest, a shulker box, a bell, a decorated pot, a spawner and an end gateway), all animated on clients that own no copy of their state. The other forty-odd block entities in the game raise none, which is why a block event is a small channel rather than a general one. ComparatorBlock is the odd one out and worth a moment: it overrides BlockBehaviour.triggerEvent to forward to its block entity, but ComparatorBlockEntity overrides nothing and nothing anywhere raises a comparator event, so the override is dead in both directions.

One push, tick by tick

Six flag words go past on this page, and four are worth having in hand: 324 is skip the block entity’s side effects, moved by piston, invisible; 276 swaps the piston bit for known shape; 67 is moved by piston, clients, neighbours; and 3 is the ordinary neighbours and clients. Of the four, only 67 and 3 tell a client anything. The table in the write nobody is told about has the first three row by row, with 82 and 18, which are sent too, the other way to end has the fourth, and the bit names are the catalogue’s (block update flags).

sequenceDiagram
    participant SL as ServerLevel
    participant PBB as PistonBaseBlock
    participant PSR as PistonStructure<br/>Resolver
    participant PMBE as PistonMoving<br/>BlockEntity
    participant CPL as ClientPacketListener
    participant CL as ClientLevel

    Note over SL,CL: tick N, a packet handler, before the level ticks
    SL->>PBB: neighborChanged, so checkIfExtend
    PBB->>PBB: getNeighborSignal finds the wire, EXTENDED is false
    PBB->>PSR: resolve, as a dry run
    PSR-->>PBB: the stone is pushable, air beyond it
    PBB->>SL: blockEvent, TRIGGER EXTEND with the facing packed in
    Note over SL,CL: tick N, block-events phase, still the same tick
    SL->>SL: doBlockEvent re-reads the position
    SL->>PBB: triggerEvent
    PBB->>PBB: getNeighborSignal, a second time
    opt still powered — a pulse shorter than the gap dies here
        PBB->>PSR: resolve, for real this time
        PBB->>SL: setBlock, two MOVING PISTON placeholders at flags 324
        PBB->>SL: setBlockEntity, one PistonMovingBlockEntity per placeholder
        PBB->>SL: setBlock, the extended base at flags 67
        SL-->>CPL: ClientboundBlockEventPacket within 64 blocks, and a sound packet
        CPL->>CL: blockEvent, run at once rather than queued
        CL->>PBB: triggerEvent again, against the client's world
    end
    Note over SL,CL: tick N and tick N plus 1, block-entities phase, both sides
    PMBE->>PMBE: progress 0 to 0.5 to 1, moveCollidedEntities under NOCLIP
    Note over SL,CL: tick N plus 2 on the server, five PistonMovingBlockEntity.deathTicks later on the client
    PMBE->>SL: setBlock, the real stone and a PISTON HEAD, flags 67

One extension, from the redstone update to the landing write. The bracket is the whole of the point: everything inside it, the block-event packet the client re-runs included, happens only if the wire is still powered when the block event is drained, later in the tick that queued it.

How a piston decides, and the line that cannot fire

Quasi-connectivity is two loops, not a rule

PistonBaseBlock.getNeighborSignal is the whole of quasi-connectivity, and it is one short method. Its first loop asks SignalGetter.hasSignal at all six neighbours except the one it faces; then, if none of those answered, a second loop repeats the same question for five of the neighbours of the position directly above the piston, skipping Direction.DOWN because that would only read the piston again. The piston is not the only block that reaches up like this, but the family is tiny: DispenserBlock.neighborChanged asks SignalGetter.hasNeighborSignal at its own position or at the one above it, and DropperBlock has the behaviour too, by extending DispenserBlock without overriding that method — so three classes reach up and only two write it down, and no other block in the game does either. (DoorBlock asks the same pair, but for a door the position above is its own upper half.) That is why quasi-connectivity is a short list of block-by-block quirks rather than a redstone rule. What signal means, and how the wire beside the piston comes to be connected to it at all, is signal and dust.

The call between the loops is dead

Between those two loops sits a third question — SignalGetter.hasSignal at the piston’s own position, looking down — and it can never return true. A position answers with strong power only if the block standing there is a redstone conductor (signal and dust), and Blocks.pistonProperties declares a piston never to be one; PistonBaseBlock overrides no signal method of its own either, so its weak answer is zero as well. Strong power reaches a piston the ordinary way, through its conducting neighbours, in the first loop. The middle call is dead.

Three events, and what the third one declines to do

PistonBaseBlock.checkIfExtend turns the answer into one of three events. Powered and not extended raises PistonBaseBlock.TRIGGER_EXTEND — but only if a dry-run PistonStructureResolver.resolve succeeds first, so a piston with an immovable wall in front of it queues nothing at all. Unpowered and extended raises PistonBaseBlock.TRIGGER_CONTRACT, or PistonBaseBlock.TRIGGER_DROP when the extension it would retract is still in flight: the block two ahead is still a Blocks.MOVING_PISTON facing the same way and extending, and either its progress is under half, or it was ticked this very game tick, or ServerLevel.isHandlingTick says the level is still inside the window that opens at the top of the tick and closes when this drain ends (the level tick).

The two retraction events run the same branch of PistonBaseBlock.triggerEvent, and they differ in exactly one thing: a sticky piston contracting normally may drag the block two ahead back with it, and a drop may not — the pull is guarded on the event being PistonBaseBlock.TRIGGER_CONTRACT. A sticky piston whose extension is caught in flight leaves the block it was carrying where it stands: if the block is still moving at the drain, PistonMovingBlockEntity.finalTick sets it down in place of any pull, whichever the event, and if it has only just landed, the guard keeps a drop from pulling it back. That is the thing players discover by accident.

What moves, and what is simply gone

PistonStructureResolver runs at least twice per push on the server — a dry run each time PistonBaseBlock.checkIfExtend finds the piston powered and not extended, then once for real inside PistonBaseBlock.moveBlocks — and once more in the client’s copy, and produces two lists, PistonStructureResolver.toPush and PistonStructureResolver.toDestroy. PistonStructureResolver.addBlockLine walks backwards first, from the block it starts at and while the block it has reached is sticky, then forward until it runs out of blocks, refusing the whole push, not merely stopping, when the forward walk meets something unpushable or the blocks to move pass twelve in all; walking backwards, an unpushable block only ends the walk. Twelve is the number PistonStructureResolver.MAX_PUSH_DEPTH names, tested in three places. PistonStructureResolver.addBranchingBlocks follows slime and honey sideways, with PistonStructureResolver.canStickToEachOther refusing the one pairing everybody tests first — slime against honey does not stick.

PistonBaseBlock.isPushable is the per-block veto and it is a longer list than folklore suggests: outside the build height or the world border, a block whose destroy speed is −1, a PushReaction of PushReaction.IMMOVEABLE (which is how obsidian and fourteen other blocks, the anvils and the lodestone among them, say it, as data), a PushReaction.POPPED where the caller did not allow destruction, a PushReaction.PUSH being moved the wrong way, an already-extended piston, and — the clause that explains the most — anything else with a block entity that would not pop. A push straight down at the bottom of the world or straight up at the top is refused too, and a piston itself skips the destroy-speed and push-reaction tests entirely. A chest cannot be pushed because it has a block entity; that PistonMovingBlockEntity has nowhere to keep one is the reading of the code that makes sense of the clause, not something the code says.

The write nobody is told about

An extension and a sticky pull write five kinds of position between them, and their flag words are the page’s hook made concrete. Four are PistonBaseBlock.moveBlocks’s; the fifth, on an extension, is written by PistonBaseBlock.triggerEvent after PistonBaseBlock.moveBlocks returns. Which bit does what is blocks and states.

whatwritten byflagsthe bits that matter
each moving block’s destination, and the armPistonBaseBlock.moveBlocks324Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS, Block.UPDATE_MOVE_BY_PISTON, Block.UPDATE_INVISIBLE — and no Block.UPDATE_CLIENTS
an old arm cleared when a sticky piston pullsPistonBaseBlock.moveBlocks276the same three bits again, with Block.UPDATE_KNOWN_SHAPE in place of the piston bit — also not sent
a vacated position, set to airPistonBaseBlock.moveBlocks82Block.UPDATE_MOVE_BY_PISTON, Block.UPDATE_KNOWN_SHAPE, Block.UPDATE_CLIENTS — this one is sent
a destroyed block, set to airPistonBaseBlock.moveBlocks18Block.UPDATE_KNOWN_SHAPE, Block.UPDATE_CLIENTS
the piston base, now extendedPistonBaseBlock.triggerEvent67Block.UPDATE_MOVE_BY_PISTON, Block.UPDATE_CLIENTS, Block.UPDATE_NEIGHBORS

A retraction also writes the base’s own placeholder at 276, from PistonBaseBlock.triggerEvent before any of these, and a piston that does not pull clears its arm with Level.removeBlock, at 3. In this page’s trace nothing is written at 82 at all — every origin down a straight push is somebody else’s destination, so the vacated set empties — and the reader should take the table’s point from the two rows that do fire here: the client is told that the base is extended, and nothing about the two positions now holding placeholders. Each placeholder’s PistonMovingBlockEntity is injected by hand, built by the static MovingPistonBlock.newMovingBlockEntity and installed with Level.setBlockEntity — MovingPistonBlock.newBlockEntity, the ordinary factory, returns null, because a moving piston is never created by the ordinary block-entity path (block entities) — and carries the real block as PistonMovingBlockEntity.movedState. That state is also the one thing about the moving blocks that does travel as data: PistonMovingBlockEntity overrides BlockEntity.getUpdateTag, so a player who loads the chunk mid-push receives the placeholder and its cargo in the chunk packet rather than as an update.

ClientPacketListener.handleBlockEvent hands the packet to the base Level.blockEvent, which runs PistonBaseBlock.triggerEvent immediately against the ClientLevel: the same resolver, the same placeholders, the same injected block entities. What the client’s copy does not do is the server’s-side-only work — no drops for a crushed block, no game event, no BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval — and, most audibly, no sound. PistonBaseBlock.triggerEvent passes a null except entity, so nobody is excluded and there is no local prediction to hear: the piston you hear is the server’s ClientboundSoundPacket, arriving beside the event (what makes a sound). The particles of a crushed block are the mirror image: the level event that spawns them is raised inside PistonBaseBlock.moveBlocks on the client side only, and only for a block outside BlockTags.FIRE.

Two ticks of motion, and two ways to end

PistonMovingBlockEntity.tick runs in the block-entity phase on both sides and does one thing per tick: add 0.5 to PistonMovingBlockEntity.progress, after shoving whatever is in the swept slab (bar the few entities that ignore pistons) — PistonMath is the class that computes it — with PistonMovingBlockEntity.moveCollidedEntities and dragging honey-stuck entities with PistonMovingBlockEntity.moveStuckEntities. The PistonMovingBlockEntity.NOCLIP thread-local is set around each entity’s own move — and holds the push Direction rather than a flag — so that a pushed entity may pass through the very block pushing it. The two ticks are the number PistonMovingBlockEntity.TICKS_TO_EXTEND names.

Note where those two ticks start. The placeholders are written during the blockEvents phase, and a ticker goes straight into the level’s flat list — so the blockEntities phase, later in the very same tick, already walks it. The motion’s first tick is the tick the piston was told about, not the one after, which is why a piston is not “a tick late” so much as three ticks long.

The tick after progress reaches 1 is the landing. The entity is removed, and Block.updateFromNeighbourShapes re-fits the moved state to its new surroundings before it is written at flags 67, with a waterlogged property cleared if it survived the trip. The arm’s position becomes a real PistonHeadBlock, and that block earns its place in the cast by being the piece of the assembly that talks backwards once the motion is over: PistonHeadBlock.neighborChanged forwards the updates it receives to the base behind it whenever it has one, so power arriving at the head reaches the piston that owns it, and PistonHeadBlock.affectNeighborsAfterRemoval destroys that base when the head is broken, which is why an arm cannot be mined off a piston and left behind. The client holds five extra PistonMovingBlockEntity.deathTicks before writing the landing itself; on screen the block has already arrived, its progress having reached 1, and the server’s own write is broadcast anyway.

The other way to end

PistonMovingBlockEntity.finalTick is a different operation, not an early-exit form of that one, and the difference is what makes an interrupted extension clean up after itself. It writes at flags 3 — plain neighbours and clients, without the moved-by-piston bit the ordinary landing carries — and for the entity carrying the arm (the one with PistonMovingBlockEntity.isSourcePiston) it writes air instead of the moved state, so a retraction that catches its own extension in flight leaves nothing behind. That flag is set both on the arm’s placeholder and on the one a contracting piston writes at its own position. It is reached from PistonMovingBlockEntity.preRemoveSideEffects, and directly from PistonBaseBlock.triggerEvent’s contract branch.

Questions players ask

Why did my zero-tick pulse move nothing at all? Because PistonBaseBlock.triggerEvent asks PistonBaseBlock.getNeighborSignal again at the drain, and a piston no longer powered simply returns false — which is what a pulse that ends before that tick’s blockEvents phase leaves. The event is consumed, no blocks move, and no ClientboundBlockEventPacket is sent, so nobody sees anything happen either.

Why does a redstone block in front of a piston not power it? Because the piston never asks there. PistonBaseBlock.getNeighborSignal deliberately skips the direction the piston faces when looking for power, so the block in front never counts as a source — and then goes looking one block higher, where a redstone build would not think to check.

Where to look

The channel first: Level.blockEvent against ServerLevel.blockEvent is the one override the page turns on, ServerLevel.runBlockEvents and ServerLevel.doBlockEvent are the drain, and BlockBehaviour.BlockStateBase.triggerEvent is what a block is finally asked. Then the piston: PistonBaseBlock.checkIfExtend decides, PistonBaseBlock.getNeighborSignal is quasi-connectivity, PistonBaseBlock.triggerEvent runs at the drain and PistonBaseBlock.moveBlocks does the writing. PistonStructureResolver.resolve with PistonStructureResolver.addBlockLine is what moves and PistonBaseBlock.isPushable what does not. The motion is PistonMovingBlockEntity.tick and its two endings, PistonMovingBlockEntity.finalTick being the interesting one. ClientPacketListener.handleBlockEvent is the client’s entire share. BaseEntityBlock.triggerEvent and PistonMovingBlockEntity.moveCollidedEntities are doors this page only points at: the seven block entities that animate themselves, and what a push does to whatever is standing in the way.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Diodes and the observer

Verified against Minecraft 26.3 · Part V · A repeater, a comparator and an observer in one circuit — three blocks that learn about the world three different ways, and one of them is not listening to redstone at all.

Put a repeater, a comparator and an observer side by side and they look like variations on one idea: blocks that take a signal in one side and push one out the other. Two of them genuinely are — RepeaterBlock and ComparatorBlock are both DiodeBlocks and share their output machinery, differing in the number they emit and in what calls it. The observer is not a diode at all — it is a DirectionalBlock, a full cube, with a six-way facing — and the way it finds out that something changed is the page’s hook: ObserverBlock fires from ObserverBlock.updateShape, a shape update, so the one block whose entire job is noticing change is not on the channel that carries change notifications. That is not a curiosity. It is why an observer sees a door opened by hand, an event that writes with Block.UPDATE_NEIGHBORS clear and so fires no neighbour update of its own. And the repeater quietly uses the same trick for its lock.

The cast

classwhat it decidesthread
DiodeBlockeverything the repeater and the comparator share: what counts as input, what counts as a side input, and how output leavesServer
RepeaterBlocka delay in two-tick units, and whether it is lockedServer
ComparatorBlocktwo modes over one pair of inputs, and how far in front of itself it can seeServer
ComparatorBlockEntityone integer — the comparator’s whole reason for having a block entity at allServer
ObserverBlockthat a neighbour’s state changed, on the channel the repeater uses for its lockServer
Levelthat a write carrying the neighbour bit, of a state with an analog output, pokes the comparators around it — and so does BlockEntity.setChangedServer

The observer shares one row with the other two

The table below is two comparisons wearing one grid, and it repays reading in that order. Read the first two columns against each other and you get the repeater against the comparator — four rows of difference, about reach, arithmetic, urgency and storage, and all of them inside machinery the two share. Then read the third column against the other two and there is only one row where it does what both of them do by the same calls: how it outputs. It reads no signal, offers no side input, and is told about the world on a different channel from their input, which is why nothing is the honest answer in two of its cells.

RepeaterBlockComparatorBlockObserverBlock
what it reads from the frontDiodeBlock.getInputSignal — the signal at the block it faces, and if that is under 15, the raw RedstoneWireBlock.POWER of a wire therethe same, then overridden: an analog output if the block in front has one, else one block further through a conductornothing. It reads no signal at all
what it reads from the sidesDiodeBlock.getAlternateSignal, restricted to other diodes (DiodeBlock.sideInputDiodesOnly is true), and used only to lockthe same, unrestricted, and used as the second operandnothing
how it books its turnDiodeBlock.checkTickOnNeighbor unchanged: RepeaterBlock.DELAY doubled, at one of the three priorities that method can chooseoverrides it entirely: always a delay of 2, at TickPriority.HIGH or TickPriority.NORMALObserverBlock.startSignal from a shape update: delay 2 at the default TickPriority.NORMAL, and only if one is not already booked
what it storeseverything, in the block statethe same, plus one int in a ComparatorBlockEntityeverything, in the block state
how it outputsDiodeBlock.updateNeighborsInFrontthe sameObserverBlock.updateNeighborsInFront, an independent copy making the same two calls

A diode never writes into its target

The output half is the least-known part of all three blocks, and the two diodes genuinely share it: one method on DiodeBlock, inherited unchanged. The observer does the same two things by its own copy of the method and offers its signal the same way, which is where its machinery converges with theirs. A diode declares itself a source unconditionally (DiodeBlock.isSignalSource), answers DiodeBlock.ownSignal with DiodeBlock.getOutputSignal when DiodeBlock.POWERED and zero otherwise, and restricts DiodeBlock.getSignal to the one neighbour at its output — so a repeater offers its 15 to precisely one neighbour, and DiodeBlock.getDirectSignal hands out the same value, which is what makes a diode able to strongly power the block in front of it.

A diode’s HorizontalDirectionalBlock.FACING points at the input. DiodeBlock.getStateForPlacement takes the player’s horizontal direction and reverses it, so the output is at Direction.getOpposite of its facing, and that is the position DiodeBlock.updateNeighborsInFront acts on. It does two things there: a direct Level.neighborChanged on the output block, and a Level.updateNeighborsAtExceptFromFacing around that same block, skipping the direction that points back at the diode. It never writes a state into the target. It notifies, and lets the target read back through SignalGetter.getSignal — which is why the signal passes from a repeater to a repeater without either of them knowing what the other is; only how urgently the feeding one books its turn, below, depends on that.

The repeater’s signal leaves by an unexpected door. DiodeBlock.tick writes DiodeBlock.POWERED with Block.UPDATE_CLIENTS alone — flags 2, no neighbour bit (block update flags) — so Level.setBlock’s neighbour fan-out never runs, though its three shape passes still do. What actually propagates the change is DiodeBlock.onPlace, which LevelChunk.setBlockState runs on the server for any write without Block.UPDATE_SKIP_ON_PLACE, and which calls DiodeBlock.updateNeighborsInFront (blocks and states). The comparator’s tick is its own, ComparatorBlock.refreshOutputState, which calls DiodeBlock.updateNeighborsInFront directly as well (below).

What each one can see

DiodeBlock.getInputSignal reads the block it faces and then, if that gave less than 15, takes the maximum with the raw RedstoneWireBlock.POWER of a wire sitting there — the special case that lets a diode read a wire whose connection state does not point at it.

DiodeBlock.getAlternateSignal reads the two horizontals perpendicular to the facing through SignalGetter.getControlInputSignal, and DiodeBlock.sideInputDiodesOnly decides what counts. For a repeater it is true, so only another diode can reach a repeater’s side, and the value is used solely by RepeaterBlock.isLocked. For a comparator it is false, so a redstone block reads as 15, a wire reads as its power, and any signal source reads as its strong output — and the value is the comparator’s second operand.

ComparatorBlock.getInputSignal is where comparators earn their reputation. If the block in front has an analog output, that value replaces the redstone reading outright. Otherwise, if the reading is under 15 and the block in front is a redstone conductor, the comparator looks one block further and takes the best of two things there: the analog output of whatever block is at that position, and the reading of an ItemFrame — but only if exactly one frame in that block’s space faces the way the comparator’s HorizontalDirectionalBlock.FACING points. The direction test comes first and the count second, so a frame pointing some other way is not counted at all; two frames facing the comparator’s way, and the comparator reads neither. Note what it falls back to when both of those come up empty: not zero, but the ordinary front reading it already had. A comparator in front of a conductor with two item frames behind it is not broken — it is reading the conductor.

What it does with the two numbers

Having read a front value and a side value, ComparatorBlock.calculateOutputSignal is two lines. A side reading greater than the front one puts the output at zero, whatever the mode. Otherwise ComparatorBlock.MODE decides, and it holds one of exactly two values: in ComparatorMode.SUBTRACT the output is front minus side, and in ComparatorMode.COMPARE — the default, and the mode a freshly placed comparator is in — it is the front value unchanged. So the comparing mode does no arithmetic at all; what it compares is only whether the side beats the front. The lit pair of torches at its input end is a separate question again: ComparatorBlock.shouldTurnOn decides DiodeBlock.POWERED, which lights them, true when the front beats the side, and on a non-zero tie only in compare mode — which is why a subtract comparator with equal inputs outputs zero and its pair goes out, while a compare comparator with equal inputs outputs the full front value and stays lit. The lone torch at its output end is the mode’s, lit in subtract.

A container’s analog output is AbstractContainerMenu.getRedstoneSignalFromContainer: every slot’s count divided by that stack’s maximum size, summed, divided by the number of slots, and mapped onto 0–15. (The formula takes the smaller of that maximum and the container’s own cap, but the cap is 99 for every container that reaches it.) So a chest of shulker boxes and a chest of cobblestone at the same item count read very differently.

Booking a turn, and why a repeater turns off first

All three answer a change by booking a turn rather than acting on it; the queue itself, its dedup rule and the drain are scheduled ticks, which traces a repeater in full. What belongs here is which priority each one asks for, because that is where the two diodes stop agreeing.

DiodeBlock.checkTickOnNeighbor books only when the diode is not locked, only when the current DiodeBlock.POWERED disagrees with the current input, and only when LevelTickAccess.willTickThisTick says nothing is already about to run there. It picks TickPriority.EXTREMELY_HIGH when DiodeBlock.shouldPrioritize holds — when the block it outputs into is itself a diode whose own input is not on the far side of it, so a diode reading this one or standing sideways to it, but not one aimed back at it — TickPriority.VERY_HIGH when the diode is currently on, and TickPriority.HIGH otherwise. So a diode’s turn-off beats another’s turn-on due on the same tick, and a diode feeding a diode beats both. The only TickPriority.NORMAL booking a repeater makes is DiodeBlock.setPlacedBy, at delay 1, when you place one into an already-powered spot.

ComparatorBlock.checkTickOnNeighbor throws all of that away. It has no lock to consult — DiodeBlock.isLocked is false unconditionally and the comparator does not override it — its delay is a flat 2 whatever the state says, and it chooses between TickPriority.HIGH and TickPriority.NORMAL alone; the two urgent priorities a repeater relies on are not available to it. It also books on a second condition the repeater does not have: not only when the powered flag disagrees with the input, but whenever the computed output value differs from the int currently in the block entity.

DiodeBlock.tick is what pulse extension is made of. Finding itself off, it turns on regardless of whether the input is still there — and then, if the input has already gone, books its own turn-off one delay later at TickPriority.VERY_HIGH. A pulse shorter than the delay is not swallowed; it is stretched to the delay.

The channel the observer listens on

Two channels carry news of a change, and the difference between them is the whole of this page’s hook, so it is worth stating before the figure draws it. The neighbour channel — Level.updateNeighborsAt reaching each neighbour’s BlockBehaviour.neighborChanged — is what a write enters when it carries Block.UPDATE_NEIGHBORS, and it exists only on the server, where those methods have bodies. The shape channel — Level.neighborShapeChanged reaching each neighbour’s BlockBehaviour.updateShape — runs on every write that does not say Block.UPDATE_KNOWN_SHAPE, and it runs on both sides. So the neighbour channel says something near you changed and here is who says so, and the shape channel says your neighbour’s state is now this; do you still fit? — which arrives unless the writer opts out with that bit, where the neighbour channel arrives only if the writer opts in (blocks and states owns both).

flowchart TD
    NC["the neighbour channel: Level.updateNeighborsAt"]
    SC["the shape channel: Level.neighborShapeChanged"]
    RB["DiodeBlock.neighborChanged, on the repeater and the comparator"]
    RL["RepeaterBlock.updateShape writes LOCKED"]
    DONE["no appointment: the lock is already correct"]
    OB["ObserverBlock.updateShape"]
    SS["ObserverBlock.startSignal"]
    BOOK["the appointment book"]
    DT["a diode's tick writes POWERED under UPDATE_CLIENTS"]
    OT["ObserverBlock.tick writes POWERED under UPDATE_CLIENTS"]
    OUT["the block in front, then its five other neighbours"]
    NC -- "the write set UPDATE_NEIGHBORS" --> RB
    SC -- "the write omitted UPDATE_KNOWN_SHAPE" --> RL
    SC --> OB
    RB -- "DiodeBlock.checkTickOnNeighbor" --> BOOK
    RL -- "server only, off the axis" --> DONE
    OB -- "facing direction, while unpowered" --> SS
    SS -- "server only, none booked" --> BOOK
    BOOK --> DT
    BOOK --> OT
    DT -- "DiodeBlock.onPlace inside the write, and a comparator's direct call" --> OUT
    OT -- "ObserverBlock.updateNeighborsInFront, directly" --> OUT

Two channels, one queue, and one end that never reaches it: a diode hears about a change on the neighbour channel and the observer on the shape channel, both book a turn, and only the repeater’s lock is written where it stands. The paths out of the book differ too — the repeater’s pulse leaves through the side effect of its own write, while the observer calls for it and the comparator does both.

An observer watches for ObserverBlock.updateShape arriving from the one direction it faces, while ObserverBlock.POWERED is false, and books a two-tick appointment. Two ticks later ObserverBlock.tick writes the powered state with flags 2, schedules its own turn-off two ticks after that, and pulses through ObserverBlock.updateNeighborsInFront. The shape channel is the right one for the job because it carries your neighbour’s state changed unless the writer opted out: a door opened by hand writes with flags 10 and issues no neighbour update at all, and the observer still sees it (block interaction), and dust the observer is watching carries the same news through the flag-2 writes of signal and dust.

RepeaterBlock.LOCKED works the same way, and it is one of exactly two diode properties computed from a redstone reading at all: DiodeBlock.POWERED is the other, and the difference between them is when. DiodeBlock.POWERED is written at tick time, from the appointment the block booked, and a comparator’s mode click refreshes it on the spot; RepeaterBlock.LOCKED is written inside a shape update or at placement, with no appointment anywhere. RepeaterBlock.updateShape recomputes it whenever a neighbour off the facing axis changes, which is the two sides and, harmlessly, up and down; the value itself comes from the two sides alone. So locking follows a neighbouring repeater’s state without either block scheduling anything.

The tempting conclusion is that both blocks chose the shape channel because it is the half of the update machinery a client also runs. That is not what the code does: both hooks refuse to do their redstone work on the client. RepeaterBlock.updateShape recomputes the lock only when the level is not client-side, and ObserverBlock.startSignal returns immediately on a ClientLevel. Nor would it help the observer if it did — a client keeps no appointment book at all, so a scheduled tick could never fire there. Beyond what the client predicts for its own clicks and placements, everything else it knows about these three blocks the server sends it.

One int, and the fan-out that exists to deliver it

A comparator has a block entity for one reason: ComparatorBlock.calculateOutputSignal can produce a number that the block state has nowhere to keep. DiodeBlock.POWERED is one bit, and a comparator’s output is 0–15. Plenty of redstone blocks have block entities — the sculk sensor keeps its last vibration frequency in one and answers with it while it is active, and most containers answer from their contents — but a DaylightDetectorBlockEntity stores nothing at all, so having one is no evidence of state that a block state could not hold. ComparatorBlockEntity.getOutputSignal is that number, written by ComparatorBlock.refreshOutputState — which also flips the powered bit when it needs to, and then calls DiodeBlock.updateNeighborsInFront whether or not anything changed, whenever the mode is ComparatorMode.COMPARE.

The other half of making comparators work is Level.updateNeighbourForOutputSignal. It walks the four horizontals and notifies any Blocks.COMPARATOR it finds; failing that, where the neighbour is a redstone conductor, it reaches one further and notifies a comparator there instead, mirroring the comparator’s own reach in the opposite direction. Twelve call sites reach it, and the two general ones are the pair that matters here. Level.setBlock calls it on the server for a write that carries Block.UPDATE_NEIGHBORS and whose new state has an analog output — but an item entering a chest is not a write at all. That path is BlockEntity.setChanged, which calls it unconditionally, and it is what makes a comparator notice a hopper filling a chest that nothing else in redstone would have reported.

Where to look

Read the shared half first, because it is where the two diodes stop being two blocks: DiodeBlock.getInputSignal and DiodeBlock.getAlternateSignal are the two readings, DiodeBlock.sideInputDiodesOnly is the one boolean that turns a repeater’s side input into a lock, DiodeBlock.checkTickOnNeighbor books the turn and DiodeBlock.shouldPrioritize decides how urgently, DiodeBlock.tick is where pulse extension lives, and DiodeBlock.updateNeighborsInFront with DiodeBlock.onPlace is the whole of how a repeater’s signal leaves. Then the two divergences: ComparatorBlock.getInputSignal with ComparatorBlock.calculateOutputSignal and ComparatorBlock.refreshOutputState for the arithmetic and the int in the block entity, and ObserverBlock.updateShape with ObserverBlock.startSignal for the channel. RepeaterBlock.updateShape is the lock, and Level.updateNeighbourForOutputSignal is the fan-out built for comparators. AbstractContainerMenu.getRedstoneSignalFromContainer is the door into what a chest is worth.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

VI · Entities

Verified against Minecraft 26.3 · Part VI · Everything in the world that is not a block: what one is, who is allowed to move it, how it is described to the other side, and how it stops.

A zombie glides where your own movement is crisp. A sheared sheep changes on every screen at once. A mob you name-tagged never despawns by distance, a hit that lands during the red flash usually does nothing, and Strength II raises your damage without a single attribute packet leaving the server. Those are five different systems and one question: which of the two programs is allowed to decide this, and how does the other one find out? Part VI is that question asked about everything in the world that is not in the grid — the zombie, the arrow, the boat, the dropped pickaxe, the invisible marker a data pack left as a bookmark. All of them are an Entity, a base class deliberately thin on behaviour: it gives them a box, a numbered array for describing themselves to clients, one collision resolver and one abstract method for being hurt, and not much else besides the riding, fire, fluids and portals they all share. So the answer to the question is never in one place. It is five mechanisms that each decide it separately, and the part’s claim is that every surprise on this list is one of those five answering a question you thought a different one had already settled.

The shape of the part

Part VI is a ladder, and the numbers on it are the watch order below. Each page needs the ones above it and nothing below them, and the second rung — authority — is the one everything else leans on, including Parts VIII, IX and X, which link back to it rather than re-deriving it.

flowchart TD
    A["1 · Entity anatomy"]
    B["2 · Authority"]
    C["3 · Entity lifecycle"]
    D["4 · Synched entity data"]
    E["5 · Attributes"]
    F["6 · Movement and collision"]
    G["7 · Goals and brains"]
    H["8 · Pathfinding"]
    I["9 · Damage and death"]
    OUT["Parts VIII, IX and X"]
    A -- "one type, one factory, one live object" --> B
    B -- "one side of each pair decides where it goes" --> C
    C -- "now it is in a world, findable and ticking" --> D
    D -- "one of six channels, and here is another" --> E
    E -- "the physics knobs are attributes" --> F
    F -- "something has to decide where to walk" --> G
    G -- "a decision is only a position" --> H
    H -- "and one abstract method answers every hit" --> I
    B -- "and so do three later parts" --> OUT

The ladder, numbered to the watch order, where every arrow means what the next page needs from this one, so the labels are why the order is the order. The rung with two arrows out is the one to notice: authority is the only page in Part VI that three later parts name as a prerequisite.

Two of the eight steps down the ladder are pairings rather than sequels. Synched entity data and attributes are two channels doing the same job differently, and the contrast is the lesson, so the second wants the first fresh in mind. Pathfinding is the other half of goals and brains and watchable on its own once that page has said where a wanted position comes from.

The arrow leaving rung two for Part X lands on two pages, and it is Part X’s dependency rather than this part’s: the client level opens by saying it is not an authority either, and prediction and acknowledgement calls itself the client’s second answer to the same question, and neither lands until authority has said what one is. So this part is watched first, and nothing here waits on Part X.

Before you start

The level tick, because two of this part’s surprises are claims about which phase something ran in — the entity phase runs after the phase that broadcasts entity changes, which is why an attribute change waits for the next tick the entity’s update interval allows, while a sheep sheared out of the packet queue, before the tick proper, does not. Tickets and loading, because whether an entity ticks at all is a property of the chunk it is standing in — for everything except a player, which is exempt — and chunk anatomy for the heightmaps that decide where a mob may spawn. Points of interest, because a villager’s whole day is claims on them and goals and brains leans on PoiManager rather than explaining it. Blocks and states for the shapes that entities collide with. Part IV’s scheduled ticks is a page of the world part you can skip before this one: entities keep no appointment book.

One more Part IV page, for one lecture: environment attributes and timelines before goals and brains. A villager’s timetable is a data-pack Timeline looked up at a position, and this part asks that system a question rather than teaching it.

Watch in this order

  1. Entity anatomy — one EntityType from the registry, through a factory, to a live object the level ticks. The registry’s default is a pig, and that default reaches the network and never reaches your save file.
  2. Authority — a zombie, a player and a boat each take one step on both sides. The client runs no physics at all for the mob chasing you, and the server runs your own player’s physics every tick and then throws the answer away.
  3. Entity lifecycle — a zombie appears in the dark, is ticked for a while, and is either forgotten or written to disk. The spawner rolls one height per category per chunk each time it tries, so caves and the open field compete for the same slice.
  4. Synched entity data — a sheep is sheared and every screen in range agrees within the tick. The slot the wool lives in is written nowhere in Sheep: it is 18 because eighteen slots were handed out above it, and the ids stop at 254 because 255 means stop.
  5. Attributes — Strength II lands, and no attribute packet is sent at all. Eight of the forty attributes never reach the client, and attack damage is one of them.
  6. Movement and collision — one tick of a falling zombie. The mover answers what did I walk through afterwards, by replaying the tick’s movement, and it settles fire and water touched in one step in a fixed order, so they always end in the extinguish.
  7. AI: goals and brains — a villager’s day, and the same machinery under a zombie that has none of it. The timetable is not on the villager: it goes to bed because it asked the world what time it is where it is standing.
  8. Pathfinding — the other half of the same lecture. Giving up is machinery: the node being walked towards carries a timeout, and the mob you watch walk into a fence and then wander off is running a scheduled surrender.
  9. Damage and death — the part’s closer. An arrow, eight owners of one number, and one abstract method. A hit that lands inside the red flash usually does nothing at all, and when it is stronger than the last, only its excess lands — silently.

Where the part stops

This is the largest part of the book by lines — 766 files and 112,091 lines in world/entity (less world/entity/player), network/syncher, world/level/pathfinder, world/damagesource and world/effect — and 33% of the part’s lines are named on no page in the book. Most of that is one class per species, and a species is the same nine pages instantiated: Panda is 1,121 lines of Mob with a sitting animation, and the 103 behaviour classes and 61 goal classes are nearly all one of three shapes, drawn once on goals and brains.

Four mechanisms there are more than a species and are explained nowhere — the minecart’s two movement models, the ender dragon’s eleven phases, a raid, and villager gossip — and a second edition should take them. The part also stops one rung short of Avatar: everything player-shaped is Part VIII, drawing an entity is Part XI, and the prediction ledger is Part X. Two packages above have their lecture elsewhere: world/effect, because the split an effect demonstrates is the player’s (status effects), and world/entity/ai/village/poi, lectured with the rest of the world’s claims in points of interest.

Reference this part uses

Five were written for this part. Attributes — all forty, with defaults, ranges and the syncable flag, generated from the registrations. Entity data serializers — all 44, in wire-id order, which is registration order. Entity spawn reasons — all nineteen, with the classes that behave differently for each; six are labels nothing tests. Structure spawn overrides — which six structures replace a biome’s spawn list, and what they put there instead. Damage outside LivingEntity — what each of the twenty-two non-living classes does with a DamageSource, on either side.

Then the shelf the whole book shares: packets, registries, game rules — ten of which this part’s pages name — math and primitives for AABB and VoxelShape, the glossary, threads and diagram lanes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Entity anatomy

Verified against Minecraft 26.3 · Part VI · What an entity is: one EntityType from the registry, through a factory, to a live object the level ticks.

You type /summon pig, and by the next tick there is a pig standing where you are. Between the command and the animal are three objects and one factory call: a ResourceKey in EntityTypeIds, the EntityType that EntityTypes built from it before any world existed, and the Entity that the type’s EntityType.EntityFactory returns. This page is the vocabulary of that chain, and the rest of Part VI is built on it. The surprise is what happens when the kind is one nobody has heard of, because that depends entirely on which of two doors it came through. An entity arriving over the network is named by a number, the registry index inside a ClientboundAddEntityPacket; an entity read back off disk is named by a string, the id field in the region file. Those two doors give opposite answers to the same bad name. Registries.ENTITY_TYPE is one of the few defaulted registries, its default is pig, and DefaultedMappedRegistry overrides nine methods, seven of which hand that default back — five for a miss, among them DefaultedMappedRegistry.byId and DefaultedMappedRegistry.getValue, and DefaultedMappedRegistry.getAny and DefaultedMappedRegistry.getDefaultKey always. So the numeric door cannot fail: ByteBufCodecs.registry decodes through IdMap.byIdOrThrow, which cannot throw here because DefaultedMappedRegistry.byId never returns null, and a pig walks out of the packet. The string door is not defaulted at all, because EntityType.CODEC is Registry.byNameCodec and resolves through a lookup the subclass leaves alone — as does DefaultedMappedRegistry.getOptional, the one override that deliberately answers empty. So the same unknown name in a region file yields nothing: EntityType.create logs Skipping Entity with id … and leaves a hole where the entity was. The default reaches the network and never reaches your save file.

The cast

classwhat it decidesthread
EntityTypeone registered kind: its factory, category, frozen dimensions, feature flags, and the two numbers that decide how it reaches clientsbuilt in a class initialiser, read from both game threads and the worldgen executor after
EntityTypeswhich 161 kinds exist and what every one of them is sized like — the most useful single table in the packageclass initialiser, once
Entityposition, box, network id, synched values, vehicle, removal reason. Deliberately thin on behaviourthe tick thread of whichever level owns it, or the worldgen executor before its chunk is live
EntityDimensionswidth, height, eye height, attachment points, and whether Attributes.SCALE may touch themimmutable record; the type’s is shared by every entity of the type
SynchedEntityDatawhich of the entity’s fields the other side is told about (synched entity data)one container per side; only the server’s writes travel
EntityInLevelCallbackwhether the object is in a world or merely on the heapinstalled by the level’s entity manager
ServerEntitywhen a tracked entity’s state becomes packets (what the client is told)the Server thread
EntityReferencehow one entity remembers another across a chunk unloadwherever it is resolved

Only one packet is this page’s own: ClientboundAddEntityPacket, the birth announcement. Everything after it — ClientboundSetEntityDataPacket, ClientboundEntityPositionSyncPacket, ClientboundEntityEventPacket, ClientboundRemoveEntitiesPacket — belongs to a sibling page.

The type is frozen, the entity is not

An entity is anything in the world that is not a block: mobs, players, items on the ground, arrows, boats, item frames, experience orbs, the invisible markers a data pack uses as bookmarks. The kind is an EntityType, built once and never changed. The individual is an Entity, and nearly all of its state is mutable by design.

classDiagram
    class EntityType {
        one object per registered kind, 161 of them
        EntityType.EntityFactory, usually the constructor
        MobCategory, the spawn cap and despawn distance
        clientTrackingRange in chunks, updateInterval in ticks
        frozen by EntityType.Builder.build
    }
    class EntityDimensions {
        width, height, eye height
        EntityAttachments, and a fixed flag
    }
    class Entity {
        SynchedEntityData, eight accessors and the chain's
        Entity.position at the feet, Entity.bb a stored box
        Entity.dimensions and Entity.eyeHeight, both caches
        Entity.levelCallback, a NULL sentinel until a level takes it
        Entity.removalReason, the whole am-I-alive bit
        Entity.vehicle and Entity.passengers
    }
    EntityType --> EntityDimensions : holds one, built once
    EntityType ..> Entity : EntityType.create calls the factory
    EntityDimensions ..> Entity : copied into the two caches, and recopied on a pose change

What each object holds, and the line the page turns on: everything in the top two boxes is frozen at class-initialiser time and shared by every individual of that kind, and everything in the bottom one belongs to one individual and changes.

Entity implements nine interfaces — Nameable, EntityAccess, ScoreHolder, SyncedDataHolder, DataComponentGetter, ItemOwner, SlotProvider, DebugValueSource and TypedInstance over EntityType — which is a fair measure of how many systems reach into it. Health, AI, the damage pipeline and inventory are all further down the tree.

In 26.3 the type constants are not on EntityType. They live in two parallel files: EntityTypeIds, 161 ResourceKeys with no reference to any entity class, and EntityTypes, the 161 matching objects that EntityType.Builder produced from them. MobCategory — MobCategory.MONSTER, MobCategory.CREATURE, MobCategory.AMBIENT, MobCategory.AXOLOTLS, MobCategory.UNDERGROUND_WATER_CREATURE, MobCategory.WATER_CREATURE, MobCategory.WATER_AMBIENT, MobCategory.MISC — carries the spawn cap (entity lifecycle) and the despawn distance (entity lifecycle). None of this is data-driven: Registries.ENTITY_TYPE is code-registered (the data-driven type pattern is what it is not), and what a data pack can reach is the tags in EntityTypeTags, the loot table at entities/<id>, DataComponents.ENTITY_DATA on a spawn egg, and the per-species variant registries (Registries.WOLF_VARIANT, Registries.CAT_VARIANT, Registries.PAINTING_VARIANT and the rest).

Dimensions, attachments and pose

EntityDimensions is a record — width, height, eye height, an EntityAttachments map and a fixed flag — and EntityDimensions.makeBoundingBox centres the box in X and Z on Entity.position and grows it upward, because that position is the bottom centre, the feet, and not the middle of the entity. EntityDimensions.scale returns the record unchanged when it is fixed, and also when both factors are 1. The flag is not the only way to escape Attributes.SCALE, either: plain Entity.getDimensions never scales at all, so every non-living entity ignores scale for free, and LivingEntity.getDimensions — which is final, the overridable hook being LivingEntity.getDefaultDimensions — short-circuits a sleeping entity to LivingEntity.SLEEPING_DIMENSIONS before any scaling happens.

EntityAttachments answers where a passenger sits, where the entity itself sits on a vehicle, where the name tag floats: EntityAttachment.PASSENGER, EntityAttachment.VEHICLE, EntityAttachment.NAME_TAG and EntityAttachment.WARDEN_CHEST, each with a fallback such as EntityAttachment.Fallback.AT_HEIGHT that EntityAttachments.Builder.build fills in from the width and height.

Pose is eighteen constants with explicit wire ids, not ordinals — the ones that matter are Pose.STANDING, Pose.CROUCHING, Pose.SWIMMING, Pose.FALL_FLYING, Pose.SLEEPING, Pose.SPIN_ATTACK and Pose.DYING, and the rest are animation states of one mob or a few, such as Pose.EMERGING and Pose.DIGGING. Pose.BY_ID is built with ByIdMap.OutOfBoundsStrategy.ZERO, so a pose id outside the range decodes silently to Pose.STANDING rather than failing the connection. Pose is the synched value on the base class that changes the box — a dozen subclasses have their own, from a pufferfish’s puff state to a slime’s size — and the loop is worth stating because everything else on this page hangs off it: Entity.setPose writes the value, SynchedEntityData.set calls Entity.onSyncedDataUpdated inside the setter, before anything is marked dirty, and that sees Entity.DATA_POSE and calls Entity.refreshDimensions, which asks Entity.getDimensions for the new record, overwrites both caches, and calls Entity.reapplyPosition to rebuild the box. The side that set the pose resizes immediately, the other resizes when the value lands. If the box grew, Entity.fudgePositionAfterSizeChange nudges the entity out of whatever it now overlaps — but only on the server, only after the first tick, only with physics on, only when the entity is not a Player, and only when the new box is at most four blocks in both width and height.

That loop is also the answer to why did the hitbox not change when I changed the size. Entity.dimensions, Entity.eyeHeight and Entity.bb are caches, and two things in the base classes refresh them unasked: a pose change, in Entity, and LivingEntity.onAttributeUpdated on Attributes.SCALE. The dozen subclasses with a box-changing value of their own — AgeableMob on the baby bit among them — each call Entity.refreshDimensions for it, and everything else has to call it by hand. While a cache is stale the two eye-height accessors disagree: Entity.getEyeHeight with no argument reads the cache, and the Pose overload recomputes.

The tree, and the class that was inserted into it

Entity has 18 direct subclasses and 192 descendants. LivingEntity and its own 124 make 125 of them — two thirds of the tree in one branch — and the seventeen non-living branches hold the other 67.

The class hierarchy under Entity: each branch shows how many types descend from it Entity (192) LivingEntity (124) Mob (114) PathfinderMob (108) AmbientCreature (1) 3 with no subclasses Avatar (7) Player (4) Mannequin (1) ArmorStand Projectile (26) AbstractHurtingProjectile (8) AbstractWindCharge (2) Fireball (2) 2 with no subclasses ThrowableProjectile (8) ThrowableItemProjectile (7) AbstractArrow (3) 3 with no subclasses 4 with no subclasses VehicleEntity (15) AbstractMinecart (8) AbstractMinecartContainer (2) 5 with no subclasses AbstractBoat (5) AbstractChestBoat (2) 2 with no subclasses BlockAttachedEntity (6) HangingEntity (3) ItemFrame (1) Painting 2 with no subclasses Display (3) 3 with no subclasses 13 with no subclasses
The Entity tree to three levels, generated from the decompile: a name in brackets carries how many types descend from it, and a grey line folds a node's childless children when there are two or more of them. Click to enlarge.

The full drawing, with the block, item and screen trees beside it, is in what extends what. What matters here is the shape: a long spine and a scattering. The spine is LivingEntity, and for all that it carries it has exactly three direct subclasses — Avatar, ArmorStand and Mob — which is worth saying plainly, because it means an armour stand is a living entity with no AI at all: no GoalSelector, no PathNavigation, both of those being Mob’s. The Brain is not Mob’s, though. It is declared on LivingEntity, built in its constructor and written under a Brain tag by every living entity, so an armour stand carries an empty one. PathfinderMob is a small class that scores walk targets and adds no way of moving, which is why Ghast and Phantom navigate without ever being one. AgeableMob adds babies, TamableAnimal an owner reference and a tame bit, Animal and Monster split by disposition, and Monster implements Enemy, a marker interface carrying nothing but XP-reward constants.

The 67 outside LivingEntity’s branch are shallow, and the atlas draws them as four families and a scattering: Projectile with 26 descendants, VehicleEntity with 15, BlockAttachedEntity with 6, Display with 3, and thirteen direct subclasses of Entity with no children of their own, from ItemEntity to LightningBolt. Sharing a base class that thin is what lets them disagree so completely about being hit (damage and death).

Avatar sits between LivingEntity and Player. It is a small class carrying the player-shaped Avatar.POSES dimension map, Avatar.DEFAULT_EYE_HEIGHT of 1.62 and one abstract method, Avatar.getProfile, so anything written against “Player extends LivingEntity” is wrong by one level. Its point is Mannequin, a posable, profile-skinned, player-looking entity in the decoration package that is not a Player and carries none of the inventory, abilities or hunger. What the rung holds and who draws it belong to player anatomy; what matters here is that the tree has that level.

Cutting across the tree are the capability interfaces, where most of the shared behaviour lives: Leashable is the fattest, with Leashable.tickLeash called from Entity.baseTick, and beside it Bucketable, EquipmentUser, NeutralMob, Attackable, Targeting, TraceableEntity, OwnableEntity, Shearable, PlayerRideableJumping, ItemSteerable, and ContainerUser, with exactly two implementors: Player and CopperGolem.

EntityReference deserves a name of its own. It stores either a UUID or the live object, and EntityReference.getEntity resolves lazily and decays back to the UUID the first time it is resolved after the target reports itself removed. It is how “who last hurt me”, “who owns this pet” and “who shot this arrow” survive a chunk unload.

Where the 727 files are

subpackagefileswhat
entity/ai277goals, brains, navigation, attributes, sensors
entity/animal130one subpackage per species or family now
entity/monster84mostly likewise
world/entity itself85Entity, LivingEntity, Mob, Avatar, EntityType, EntityTypes, the capability interfaces
entity/projectile37arrows, fireballs, thrown items
entity/boss24dragon and wither
entity/vehicle24boats and minecarts
entity/npc15villagers and traders
entity/player14Player, Inventory, Abilities
entity/decoration13armour stands, frames, paintings, Mannequin
entity/variant11the machinery that picks a data-driven mob variant
entity/item, entity/raid, entity/ambient, entity/schedule13items, raids, bats, and Activity — which is all entity/schedule still holds

From a registry entry to a live object

EntityTypes runs EntityType.Builder.build for each of the 161 keys in EntityTypeIds and registers the result, and it is that call which freezes the EntityDimensions and its attachment points for the life of the type. Everything below happens per entity, long afterwards.

sequenceDiagram
    participant SumC as SummonCommand
    participant ET as EntityType
    participant Entity as Entity
    participant SL as ServerLevel
    participant PESM as PersistentEntity<br/>SectionManager

    SumC->>SumC: rejects out of bounds, then on peaceful
    SumC->>ET: loadEntityRecursive, a tag with an id string
    ET->>ET: EntityType.by reads the id through EntityType.CODEC
    ET->>ET: EntityType.create checks canSpawn, then the factory
    ET->>Entity: the constructor: the next id, a new UUID, the type's dimensions
    Entity->>Entity: eight base accessors, then defineSynchedData, then setPos
    ET->>Entity: load, then readAdditionalSaveData
    SumC->>Entity: snapTo, the postLoad processor, before any passenger exists
    ET->>Entity: each Passengers child the same way, then startRiding
    SumC->>SL: tryAddFreshEntityWithPassengers, one body at a time
    SL->>PESM: addNewEntity
    PESM->>Entity: setLevelCallback, and only now is it in a world
    Note over SL,Entity: the next server tick
    SL->>Entity: commonTick, then tick

Thirteen steps between a registry entry and a live object, and the one that matters is the second from last: everything above Entity.setLevelCallback happens to an object no level knows about.

The client builds its own copy of the same object, from a packet, whenever a player comes into range — which is the same journey a second time, in five steps instead of thirteen:

sequenceDiagram
    participant Entity as Entity
    participant SE as ServerEntity
    participant CPL as ClientPacketListener

    SE->>Entity: getAddEntityPacket
    SE->>CPL: ClientboundAddEntityPacket, with data, attributes and equipment
    CPL->>CPL: createEntityFromPacket, a second object
    CPL->>CPL: Entity.recreateFrom<br/>Packet
    CPL->>CPL: ClientLevel.addEntity

The client’s Entity is not the server’s: it is a separate object, of the same class for all but a player or a mannequin, built from a packet rather than from a tag, and the two are only ever as alike as the channels in the rest of this part keep them.

Name to type. EntityType.by reads the id field through EntityType.CODEC. An unknown id yields nothing, and the entity is dropped with the log line the opening quoted — this is the save-file half of the hook.

Type to object. EntityType.create checks EntityType.canSpawn: the feature flags, plus a peaceful-difficulty test gated on the type’s own EntityType.isAllowedInPeaceful flag, which is a declared property and not a synonym for hostile. EntitySpawnRequest.ignoreChecks skips both. SummonCommand tests the same peaceful rule itself, a few lines earlier, purely to fail with a better message; the command does not set the skip flag, so both run. One type fails EntityType.create always: EntityTypes.PLAYER was built with EntityType.Builder.createNothing, so its factory returns null, which is why ClientPacketListener.createEntityFromPacket special-cases it and hand-builds a RemotePlayer from the player info it already has; it is EntityType.Builder.noSave and EntityType.Builder.noSummon besides. Otherwise the factory runs. The Entity constructor takes the next id from the level, invents a UUID with Mth.createInsecureUUID, copies the type’s dimensions into its cache, and builds the synched-data container — its own eight accessors, then the abstract Entity.defineSynchedData, which contributes nothing on the base class and exists only for the subclasses to chain up through (synched entity data names the eight and says why their order is the whole numbering scheme). It then calls Entity.setPos at the origin, so a fresh entity already has a full-size box rather than the zero-size Entity.INITIAL_AABB the field initialiser gave it.

Tag to state. Entity.load reads position (clamped to ±3.0000512E7 horizontally), motion, rotation and UUID, then calls the abstract Entity.readAdditionalSaveData, then Entity.reapplyPosition again if Entity.repositionEntityAfterLoad says so — which everything says except BlockAttachedEntity, the corpus’s one override, so paintings, item frames, leash knots and cushions are precisely the entities that skip it and keep the position their own load computed. The save twin is Entity.addAdditionalSaveData, and passengers save inside their vehicle: Entity.save returns false for anything currently riding, and the vehicle writes them into its Passengers list through Entity.saveAsPassenger, using Entity.getEncodeId, which is null for types that never serialise.

Object to level. ServerLevel.tryAddFreshEntityWithPassengers refuses if any UUID in the stack is already loaded, then ServerLevelAccessor.addFreshEntityWithPassengers calls ServerLevel.addFreshEntity once per body. PersistentEntitySectionManager.addNewEntity files it into a section and replaces EntityInLevelCallback.NULL with a real callback. That is the moment it stops being an object on the heap; entity lifecycle takes it from here.

Level to client. ServerEntity.addPairing sends ServerEntity.sendPairingData as one bundle: the packet Entity.getAddEntityPacket returns — id, UUID, type, position, velocity, three rotation bytes and one varint of type-specific data — then the non-default synched values, then attributes and equipment. On the client, ClientPacketListener.handleAddEntity builds the object, calls Entity.recreateFromPacket, and only then adds it to the level.

The spawn-egg path is a different pipeline that meets this one at the end. EntityType.spawn snaps the new entity out of collision with EntityType.getYOffset, aligns head and body rotation, runs Mob.finalizeSpawn, applies a PostSpawnProcessor, adds it through ServerLevelAccessor.addFreshEntityWithPassengers and plays an ambient sound. SummonCommand also calls Mob.finalizeSpawn, but only when the command asks for it, and it never touches the Y offset.

The tick both sides share

ServerLevel.tickNonPassenger opens a profiler section named after the entity type, calls Entity.commonTick — which counts Entity.invulnerableTime down, calls Entity.setOldPosAndRot, steps the interpolation on a client and increments Entity.tickCount — and then calls Entity.tick, all of it through Level.guardEntityTick, which turns any exception into a crash report with the entity’s details attached. ClientLevel.tickEntities does the same, through the same guard, having first skipped anything removed, riding or frozen by the tick-rate manager. Which entities the server’s loop reaches, and in what order, belongs to the level tick. No entity is ever ticked on a worker pool, and Entity is not thread-safe — though entities are constructed on one: ChunkStatus.SPAWN runs NaturalSpawner.spawnMobsForChunkGeneration on the worldgen executor, so a chunk’s first animals are built and finalised off the Server thread before the chunk ever becomes live.

Entity.tick on the base class is one line: call Entity.baseTick. Everything else readers remember happening “in tick” is in Entity.baseTick — the dead-vehicle check, the boarding cooldown, the portal handling, the fluid snapshot, swimming, fire ticking, the lava halving of fall distance, the below-world check, the leash — or in an override. LivingEntity.tick calls up and then runs LivingEntity.aiStep, and Mob.tick calls up and refreshes its goal-control flags through Mob.updateControlFlags every five ticks, on the server only — as the portal handling, the burning and the leash inside Entity.baseTick are too.

What differs between the sides is not the tick but what the tick is allowed to do, and that is exactly the subject of the next page, authority.

The id, and what compares equal

Entity.equals compares the network id and nothing else — not the UUID, not identity — and Entity.hashCode is the id. Ids are handed out by ServerLevel.getNextEntityId from a static AtomicInteger on ServerLevel, so they are process-global, which keeps two server levels’ ids apart, and the level is consulted only to avoid a collision. Equal ids still meet across levels: in singleplayer the client’s copy of an entity and the integrated server’s share one, which is why entities from two levels must never go in one set.

On the client there is no id at all until a packet supplies one. Level.getNextEntityId returns a literal zero and ClientLevel does not override it, while zero is the reserved invalid id and Entity.getId throws Tried to access entity ID before ID assignment on it. Between construction and the Entity.setId that Entity.recreateFromPacket performs, a client-side entity has no id, and therefore no equality and no hash. ServerLevel.getNextEntityId skips zero deliberately.

The two numbers frozen onto the type

The cast promised two numbers that decide how an entity reaches clients. EntityType.clientTrackingRange is in chunks and EntityType.updateInterval in ticks — 5 and 3 unless a builder says otherwise — and between them they decide who is told about the entity and how often its movement is sent, never mind what the packet says (what the client is told owns the asking, and a third parameter, EntityType.trackDeltas, which is true of every type but eleven). Thirty-eight of the 161 types set an interval explicitly, most of them at 10 or 20; eight set Integer.MAX_VALUE (most through EntityType.Builder.noUpdateInterval), which ChunkMap turns into UpdateInterval.NEVER, so their interval branch never fires at all — EntityTypes.ITEM_FRAME and EntityTypes.GLOW_ITEM_FRAME, EntityTypes.PAINTING, EntityTypes.LEASH_KNOT, EntityTypes.END_CRYSTAL, EntityTypes.LIGHTNING_BOLT, EntityTypes.AREA_EFFECT_CLOUD and EntityTypes.CUSHION. At the other end EntityTypes.MARKER has a tracking range of 0 and is never sent to anyone at all.

The client holds a lever over a distance of its own, how far away an entity is still drawn (in singleplayer the same option also scales how far the integrated server tracks), and it is process-global in the same way the ids are. Entity.viewScale is a static field on Entity, and LevelExtractor writes it from the effective render distance and the entity-distance option together, not from the option alone — which is why entity render distance moves when you touch a slider that does not name entities.

Where to look

Start at EntityTypes, which builds all 161 types and is the most useful single table in the package, and read EntityType.Builder.build beside it for what gets frozen. Then EntityType.create and EntityType.loadEntityRecursive for the chain this page traces, and Entity itself — its constructor, Entity.defineSynchedData and Entity.load — for what comes out of it. Entity.refreshDimensions is the way into the dimension caches and EntityDimensions.makeBoundingBox the arithmetic under them. PersistentEntitySectionManager.addNewEntity is where an object becomes part of a world. Two doors the page opens only a crack: EntityReference, for how one entity remembers another, and EntityType.spawn, the spawn-egg path that meets the trace at its end.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Authority: who is allowed to simulate

Verified against Minecraft 26.3 · Part VI · A zombie, a player and a boat each take one step, on the server and on the client, and only one side of each pair decides where it ends up.

A zombie walks towards you across a field. Both programs have a Zombie object; both tick it every twentieth of a second; both call the same Entity.tick. Only one of them works out where it goes. Standing next to the zombie is another player, and their copy is the other way up — and the boat you are steering is a third case again, authoritative on your machine and nowhere else. Five predicates on Entity — two of them final — decide all of it, and they invert the naive picture in both directions: the client runs no physics at all for the mob chasing you, while the server runs your player’s physics every tick and then overwrites the answer with a number your client sent.

This is the single most error-prone idea in the entity part, and this part and three later ones rest on it — movement and collision here, every page in Part VIII about a player, what the client is told in Part IX, and in Part X both the client level, for what the client is allowed to simulate, and prediction and acknowledgement, for what it is allowed to guess while it waits to be told. Nineteen pages in the book’s thirteen parts link back to this one. It is stated in full once, here.

Three cases, read on both sides

The whole answer fits in one table, and the rest of the page is that table explained. The columns are three shapes an entity can have; the rows are what each predicate answers and what follows from it.

a tracked moba playera boat you are steering
Entity.isClientAuthoritativefalsetrue, unconditionallytrue, inherited from you
Entity.isLocalInstanceAuthoritative, servertruefalsefalse
Entity.isLocalInstanceAuthoritative, clientfalsetrue only on your own playertrue, on your machine only
Entity.canSimulateMovement, servertruetrue — its move-simulation typefalse
LivingEntity.travel runs on the serveryesyes, and the result is overwrittenn/a
LivingEntity.travel runs on the clientneveronly for your own playern/a — AbstractBoat.floatBoat and AbstractBoat.controlBoat do it instead
Entity.checkFallDamage inside Entity.moveserver onlyyour own client only — the server reaches it by the packet pathclient only
what the other side does insteadis interpolated, and stands still when the interpolation runs outapplies your movement packetapplies its own copy’s packet

A tracked mob is one the server has told your client about at all, which is every mob you can see. A fourth shape is worth naming here so that it does not look like an omission later: an entity nobody is riding and nothing is steering — an empty boat, a minecart — reaches the base implementations, which answer false to both roots, and is simulated by the server alone. A dropped item, any projectile in flight, an experience orb, a falling block and lit TNT (and a dying ender dragon) answer false to both roots too, but each declares MoveSimulationType.SERVER_AND_CLIENT, so Entity.canSimulateMovement is true for it on both sides.

The row that surprises people is Entity.checkFallDamage. Entity.move gates it on Entity.isLocalInstanceAuthoritative, which for a player is true on your own client and false on the server — so the copy that runs it every tick is the one that cannot hurt you. Three names sit close together here: Entity.checkFallDamage is the gate inside Entity.move; LivingEntity.checkFallDamage overrides it and adds the landing particles only on a ServerLevel, so your client’s copy accumulates the fall distance and calls Block.fallOn, whose damage reaches Entity.hurt, which acts only on a ServerLevel; and Entity.doCheckFallDamage is the entrance the server uses instead, driven by the movement packet, which calls the same method and is Part VIII’s subject.

The cast

classwhat it decidesthread
Entitythe five predicates, and every gate inside Entity.move that reads themthe Server thread and the Render thread
Playeroverrides three of the five and answers its own move-simulation type, and is the reason the picture invertsboth
Mobnarrows Entity.isEffectiveAi with Mob.isNoAiboth; the AI that acts on it runs on the server
LivingEntityLivingEntity.aiStep, which either simulates or coastsboth
ClientPacketListenerwhether an inbound position packet moves the entity or only updates a codecthe Render thread
ServerGamePacketListenerImplre-runs the client’s numbers for the entities the client ownsthe Server thread

Five predicates, and the final one the other four hang off

Entity.isLocalInstanceAuthoritative is the root and it is final — no class overrides it. It asks a different question on each side: on the client, am I locally client-authoritative?; on the server, am I not client-authoritative? Everything else hangs off those two, bar the answers a type or Player gives first: on the server a player asks neither question, and the entities above simulate on both sides whatever the root says.

flowchart TD
    SIM["Entity.canSimulateMovement — final, it reads the type"]
    AI["Entity.isEffectiveAi"]
    SIMT["the type: a player's is not a client or the local player, and an item's, a projectile's, TNT's and a few more are both sides"]
    Q["Entity.isLocalInstanceAuthoritative — final"]
    AIP["Player: not a client, or I am the local player"]
    AIM["Mob: and not Mob.isNoAi"]
    LCA["Entity.isLocalClientAuthoritative"]
    NCA["Entity.isClientAuthoritative, negated"]
    LCAD["base: the controlling passenger, or false"]
    LCAP["Player: am I the local player?"]
    CAD["base: the controlling passenger, or false"]
    CAP["Player: true, so the root is false"]
    SIM -- "or" --> SIMT
    SIM -- "by default" --> Q
    AI -- "by default" --> Q
    AI -- "or" --> AIP
    AI -- "or" --> AIM
    Q -- "on the client" --> LCA
    Q -- "on the server" --> NCA
    LCA --> LCAD
    LCA --> LCAP
    NCA --> CAD
    NCA --> CAP

Every arrow means one thing, is answered by: two of the predicates are answered by the root unless a type or Player’s override answers instead, and the root is answered by the other two, one on each machine. Entity.canSimulateMovement is final like the root, so what a player or an item changes there is its move-simulation type, not the method.

Two things in that picture are easy to miss. The base implementations of both Entity.isLocalClientAuthoritative and Entity.isClientAuthoritative delegate to the controlling passenger — an entity with nobody steering it answers false to both, and an entity with a controlling passenger inherits that passenger’s answer. That single line is the whole vehicle model. And Player answers Entity.getMoveSimulationType with MoveSimulationType.AUTHORITATIVE_SIDE_AND_SERVER, which the final Entity.canSimulateMovement reads, and overrides Entity.isEffectiveAi, both to something different from the root — not a client, or I am the local player, the second half being Player.isLocalPlayer — which is what lets the server simulate a player it is not authoritative for. The member that inverts the picture is Player.isClientAuthoritative, an unconditional true.

Three classes narrow the AI predicate further and are worth naming because they are the exceptions people trip over: Mob.isEffectiveAi adds Mob.isNoAi, which is where the NoAI tag bites (what the guard around it does and does not wrap is goals and brains), and both ArmorStand.isEffectiveAi and Mannequin.isEffectiveAi add a physics or immovability flag of their own.

The mob: the client is not correcting, it is replaying

A client-side zombie fails Entity.isLocalInstanceAuthoritative because nothing is riding it, so Entity.canSimulateMovement is false and LivingEntity.aiStep never reaches LivingEntity.travel. What it does instead is two things: Entity.commonTick steps its InterpolationHandler just before the zombie’s own tick, and LivingEntity.aiStep opens with the other half: when nothing is interpolating, scale the stored delta by 0.98. Nothing then applies that delta, because the only thing that would is Entity.move, which nothing in the mob’s own tick reaches on this side, and this is precisely the seam where the client’s other answer begins: what it may guess rather than simulate belongs to prediction and acknowledgement. (A piston or a shulker box can still drive a client-side mob into Entity.move, with a vector of their own rather than its stored delta — movement and collision has the five MoverType constants that say who is pushing.) Inside the mob’s own tick, though, there is no collision, no gravity, no friction and no attempt at prediction. It is not simulating and being corrected — it is replaying what ClientboundMoveEntityPacket and ClientboundEntityPositionSyncPacket tell it, and standing perfectly still when the interpolation runs out.

The player: simulated twice, believed once

A ServerPlayer passes Entity.canSimulateMovement and Entity.isEffectiveAi — both true on the server, by Player’s move-simulation type and its override — so the server’s copy runs the whole of LivingEntity.travel. It also fails Entity.isLocalInstanceAuthoritative, so the one gate inside Entity.move that reads it alone, the fall damage, does not fire. The predicates are why a copy that is not authoritative simulates at all; what the server then does with the answer is the two-phase tick — the physics run in the connection phase, after every level has ticked, inside a bracket that puts the player straight back where it found them, and the authoritative position moves in the movement packet instead. The server’s own simulation is a sanity check whose position nobody uses.

Authority is also about which of two numbers is real

A player’s position on the server moved by the packet path rather than by the server’s own arithmetic, so how far did this entity move this tick has two possible answers for a player, and for anything a player is steering, and one for everything else — and a block has to know which it is being given. SweetBerryBushBlock.entityInside asks Entity.isClientAuthoritative to decide how to find out: Entity.getKnownMovement, the distance the client reported, for a player or anything a player steers, whose movement the server did not compute, and old-position-minus-current for everything else. The predicate is not only a switch for physics.

The boat: authoritative on exactly one machine

Sit in a boat’s first seat and the base delegation makes it yours. On your client Entity.isLocalClientAuthoritative walks to the controlling passenger, finds you, and returns true, so your machine simulates the boat for real. On the server the same delegation makes Entity.isClientAuthoritative true, so the server’s copy is not authoritative and does not simulate — it zeroes its own delta outright. Every other client’s copy does the same, and is moved by its LinearInterpolationHandler.

sequenceDiagram
    box transparent your machine
    participant LP as LocalPlayer
    participant AB as AbstractBoat
    participant CPL as ClientPacketListener
    end
    participant Wire as the network
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SAB as AbstractBoat
    end

    AB->>AB: floatBoat, then controlBoat, then move for real
    LP->>Wire: ServerboundMoveVehiclePacket.fromEntity, once per client tick
    Wire->>SGPL: handleMoveVehicle
    SGPL->>SAB: move, MoverType.PLAYER over the reported distance
    alt the move is accepted
    SGPL->>SAB: absSnapTo the reported position
    SGPL->>SAB: setOnGroundWithMovement, then doCheckFallDamage
    SGPL-->>Wire: nothing
    else it is rejected
    SGPL->>SAB: absSnapTo the old position
    SGPL->>Wire: ClientboundMoveVehiclePacket
    Wire->>CPL: handleMoveVehicle
    CPL->>AB: absSnapTo the server's position
    CPL->>Wire: ServerboundMoveVehiclePacket, echoed back to confirm
    end

Two copies of one boat, one lane each: yours is simulated, and the server’s is moved over the distance yours reports and then kept or snapped back (a report too far off is refused before it is moved at all). The alt is one moment with two outcomes, and only the accepted one lands the fall damage on the server’s copy.

The inbound half of that is the sharpest demonstration of what the predicate is for. ClientboundMoveVehiclePacket is not a routine update — the server sends it only when it has rejected your movement, and ClientPacketListener.handleMoveVehicle applies it only for a vehicle the client is authoritative for, then immediately echoes a ServerboundMoveVehiclePacket back to confirm. Meanwhile the ordinary per-entity position packets take the opposite branch: ClientPacketListener.handleEntityPositionSync and ClientPacketListener.handleMoveEntity both check Entity.isLocalInstanceAuthoritative and, when it holds, do not move the entity — ClientPacketListener.handleMoveEntity decodes the delta into the entity’s position codec and stops there, and ClientPacketListener.handleEntityPositionSync records the absolute position in the codec on either branch. The server’s opinion about where your boat is gets recorded and ignored.

Where the gates actually sit

Authority is not one flag consulted once. Entity.move and LivingEntity.aiStep read a predicate at eight places between them, and one of the eight reads two, so there are nine readings in all:

wherewhich memberreads
the vertical collision flags, and Entity.setOnGroundWithMovementEntity.movelocally authoritative or moved vertically — the horizontal flags are always updated
fall damageEntity.checkFallDamagelocally authoritative
the bounceEntity.restituteMovementAfterCollisionsEntity.canSimulateMovement
the step sound and GameEvent.STEPEntity.applyMovementEmissionAndPlaySoundnot a client or locally authoritative
the 0.98 decay of the stored deltaLivingEntity.aiStepEntity.canSimulateMovement is false
the AI stepMob.serverAiStepEntity.isEffectiveAi and not client-side
the travel forkLivingEntity.travelEntity.canSimulateMovement and Entity.isEffectiveAi — the one place that reads a pair
the block effects that follow travelEntity.applyEffectsFromBlocksthe same not-a-client-or-authoritative test as the step sound

The row worth slowing down for is the fifth. The 0.98 decay is what runs because Entity.canSimulateMovement is false — the not-authoritative branch, not a fallback inside the authoritative one — which is the whole of what a client-side mob does with its stored delta.

The travel fork has a fork of its own in front of it, the ridden branch, which movement and collision walks. What belongs here is that LivingEntity.travelRidden is a ninth site: an Entity.canSimulateMovement test that zeroes the delta outright when it fails.

Questions players ask

Why does a mob rubber-band and my own player does not? On an ordinary tick neither one is being corrected, and for opposite reasons. Your client does simulate your own player, and the boat or horse you are steering — but it never simulates a tracked mob, so there is nothing about the mob for the server to disagree with. What you see on a mob is the gap between position packets, walked by InterpolationHandler.

Why does a boat feel responsive and a horse feel heavy? You steer both, and both are authoritative on your machine — but a horse is a LivingEntity going through LivingEntity.travelRidden, which asks the mob for its own speed and drag, while a boat runs its own physics directly. The authority answer is the same; the layer above it is not.

Does NoAI stop a mob moving? On its own, completely — a NoAI mob does not even fall. Mob.isEffectiveAi is the gate on Mob.serverAiStep, which is the obvious half, and it is also the second half of the gate on LivingEntity.travel, so nothing in the mob’s own tick reaches Entity.move. What still moves it comes from outside: a piston, a shulker or a shulker box, a vehicle carrying it, and a player steering it, whose branch asks only Entity.canSimulateMovement.

Where to look

Read the five predicates first, from the root out: Entity.isLocalInstanceAuthoritative, then the two it asks — Entity.isLocalClientAuthoritative and Entity.isClientAuthoritative — then Entity.canSimulateMovement and Entity.isEffectiveAi, which default to it. Player.isClientAuthoritative and Mob.isEffectiveAi, with its Mob.isNoAi, are the two overrides that matter. Then read the two methods that spend them, Entity.move and LivingEntity.aiStep, with the table above open. For the packet half, ServerGamePacketListenerImpl.handleMoveVehicle on the server and ClientPacketListener.handleEntityPositionSync on the client are the two branches the boat’s section turns on, and InterpolationHandler is what moves a mob when nothing simulates it. SweetBerryBushBlock.entityInside is a door worth opening for one line: it is the only place in the game that reads a predicate to choose which measurement of a movement to believe.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Entity lifecycle

Verified against Minecraft 26.3 · Part VI · A zombie spawns in a dark chunk at night, is ticked for a while, and then either despawns or is written to disk when the chunk unloads.

Night falls, you are standing in a field, and somewhere behind you a zombie appears. What the server actually did is smaller and stranger than it looks. Once a tick, for each chunk near a player and each mob category still under its cap, NaturalSpawner.getRandomPosWithin rolls a random x, a random z and one y — a single uniform draw between the world bottom and the surface height of that column (chunk anatomy). Three attempts follow — each one a group, because a successful attempt keeps spawning siblings around the first mob until a size limit stops it — and each one makes a handful of tries that jitter only x and z. So the whole of one category’s chance in one chunk this tick lives on one horizontal slice: every eligible category gets its own slice, caves and the open field compete for the same rolls, and a world with more vertical space between bedrock and grass spreads those rolls thinner over the surface. Everything else on this page — the caps, the light test, the despawn radius, the write to disk — hangs off that one roll, or off the moment several rejections later where the mob is finally allowed to exist.

The cast

classwhat it decidesthread
NaturalSpawnerthe cascade of tests between a chunk and a mob, run from one file of static methodsthe Server thread, and a worker for generation’s spawn step
NaturalSpawner.SpawnStatethe per-tick census, the global cap per MobCategory, and the biome crowding budget through PotentialCalculatorthe Server thread, rebuilt each tick
LocalMobCapCalculatorwhether any player near this chunk is still under the per-player limitthe Server thread, rebuilt each tick
SpawnPlacementsthe placement type, heightmap and predicate for each EntityType — code, not dataa static table, read on the Server thread, and on a worker by generation’s spawn step
PersistentEntitySectionManagerwhich entities exist, which are findable, which tick, which chunks are queued to unloadthe Server thread, with one concurrent load inbox
Visibilitythe three-state projection of FullChunkStatus that everything above readsan enum, read on both sides
EntityTickListthe set the tick walks, and the double buffer that makes mutating it mid-walk safethe Server thread, and the Render thread for ClientLevel
EntityStoragethe entities/ region files, separate from the block onesreads and writes the file on the IO pool, turns tags into entities and back on the Server thread

A spawn attempt is a filter, not a conversation

Almost every step of the spawner is a rejection, and the rejections are not all the same size: one drops a category for the whole tick, one skips a chunk for every category, one ends this category on this chunk, one ends a group attempt, and most of them cost nothing but a single jittered try. Drawing it as a conversation hides all of that. Here it is as the filter it is, in two halves — what has to pass before the first try, and what happens at each try after it — with the tests themselves in the table under them.

flowchart TD
    CAT{"the category"}
    CAT -->|"fails"| XCAT["out for the whole tick"]
    CAT -->|"passes"| CHK{"the chunk"}
    CHK -->|"fails"| XCHK["out for every category"]
    CHK -->|"passes"| LOC{"the local cap"}
    LOC -->|"fails"| XPAIR["this category on this chunk is over"]
    LOC -->|"passes"| ROLL["the roll: one x, one z, one y"]
    ROLL --> POS{"the position"}
    POS -->|"fails"| XPAIR
    POS -->|"passes"| JIT["three group attempts at that position"]

Three sizes of giving up before the first jittered try: a category can leave the whole tick, a chunk can leave every category, and the two of them can end together, at the local cap or at the rolled position. The rejecting edges are the subject of this page; the passing ones are only what is left.

Follow the right-hand edge to the bottom and the roll is the thing to stop at: one x, one z and one y, and everything below it happens at that one height.

flowchart TD
    JIT["one jittered try"] --> TRY{"the group's species, then the type filter"}
    TRY -->|"no species to pick"| GRP
    TRY -->|"fails"| MORE{"tries left in this group?"}
    TRY ==>|"passes"| MAKE{"EntityType.create"}
    MAKE -->|"null"| OVER["this category on this chunk is over"]
    MAKE -->|"a Mob at last"| OBJ{"the object filter"}
    OBJ -->|"fails"| MORE
    OBJ -->|"passes"| FIN["Mob.finalizeSpawn, then ServerLevelAccessor.addFreshEntityWithPassengers"]
    FIN -->|"cluster full"| OVER
    FIN -->|"a lone tropical fish"| GRP
    FIN -->|"room for a sibling"| MORE
    MORE -->|"yes"| JIT
    MORE -->|"no"| GRP{"all three attempts spent?"}
    GRP -->|"no"| JIT
    GRP -->|"yes"| OVER

After the roll the ways out are cheap: almost every failure jitters x and z and tries again, three arrows end only the group attempt (a new one puts x and z back at the roll), and three reach the box at the foot. The thick edge is the boundary the rest of this section is about.

Four things in that loop are worth reading twice. Almost every rejection merely jitters again — it costs one try, not one of the three attempts. The three that end a group are the empty species list, Mob.isMaxGroupSizeReached (which only a tropical fish that does not school ever answers yes to) and the tries running out. And the two that end the whole category on this chunk are a null from EntityType.create and a full cluster, which is why one bad construction and one crowded spawn both look, from outside, like the chunk going quiet. Only the last box is a spawn: Mob.finalizeSpawn settles what the mob is, and ServerLevelAccessor.addFreshEntityWithPassengers is the door the game’s other spawners use too, or LevelWriter.addFreshEntity behind it for a single body.

What each test drops, in the order it runs

the testit fails whenwhat is given up
NaturalSpawner.getFilteredSpawningCategoriesit is a monster category and the monster game rules are off; it is a persistent category and the game time is not a multiple of 400; or NaturalSpawner.SpawnState.canSpawnForCategoryGlobal is already at the capthe category, for the whole tick
ChunkMap.collectSpawningChunksthe holder has no ticking chunk behind it, or ChunkMap.playerIsCloseEnoughForSpawning finds no non-spectator player within 128 blocks measured horizontally to the chunk centrethe chunk, for every category
ServerLevel.canSpawnEntitiesInChunkthe chunk is not entity-ticking, or lies outside the world borderthe chunk, for every category
NaturalSpawner.SpawnState.canSpawnForCategoryLocalLocalMobCapCalculator.canSpawn finds every nearby player at their own capthis category on this chunk
NaturalSpawner.getRandomPosWithinthe single y roll landed at the very bottom of the worldthis category on this chunk
the block at the rolled positionit is a redstone conductor, and this is asked before any species is pickedthis category on this chunk
EntityGetter.getNearestPlayerthere is no non-spectator player anywhere in the levelthis try
NaturalSpawner.isRightDistanceToPlayerAndSpawnPointwithin 24 blocks of the nearest player; within 24 of the respawn point and that point is in this dimension; or the jitter left this chunk for one that cannot spawnthis try
NaturalSpawner.getRandomSpawnMobAtthe list is empty — or the category is MobCategory.WATER_AMBIENT in a reduced-water-ambient biome, 98 times in 100this group attempt
NaturalSpawner.isValidSpawnPostitionForTypethe category is MobCategory.MISC; it is too far out for a type that cannot spawn far from a player; the type is unsummonable or no longer in the list at this exact block; SpawnPlacements.isSpawnPositionOk fails on the placement type; SpawnPlacements.checkSpawnRules fails, which is where Peaceful and the light test live; or the type’s spawn box collides with the worldthis try
NaturalSpawner.SpawnState.canSpawnthe biome’s crowding budget for this exact type is spentthis try
EntityType.createfeature-flagged off (Peaceful was caught two rows up), or what it built is not a Mobthis category on this chunk
NaturalSpawner.isValidPositionForMobMob.checkSpawnRules or Mob.checkSpawnObstruction fails on the real object, or Mob.removeWhenFarAway says it would despawn instantly anywaythis try
Mob.getMaxSpawnClusterSizethe cluster is fullthis category on this chunk
Mob.isMaxGroupSizeReachedthe mob is a tropical fish that does not school, the one type that ever answers yesthis group attempt

The boundary that matters is the thick edge. Everything above it is decided against the EntityType — the placement type, the light rule, the collision box — because constructing a mob to ask it costs more than answering from the type. Nothing above that line has an object to call a method on. Monster.checkMonsterSpawnRules is the light rule for a zombie, and it hands the light half to Monster.isDarkEnoughToSpawn, three tests in a row: sky light against a random draw from zero to 31, then the dimension’s DimensionType.monsterSpawnBlockLightLimit if that limit is below 15, then the local brightness against a sample of DimensionType.monsterSpawnLightTest. The last of those is where storms come in: during thunder the brightness is computed with a fixed sky-darkening of 10 instead of the level’s current one, which is what lets monsters spawn outdoors in the daytime. The light rule is per-dimension data, not a constant, and EntitySpawnReason.ignoresLightRequirements exempts exactly one reason, EntitySpawnReason.TRIAL_SPAWNER.

The last gate before construction is the one nobody meets, and it is worth knowing why. NaturalSpawner.SpawnState.canSpawn asks the position’s EnvironmentAttributes.NATURAL_MOB_SPAWNS — the spawn settings every biome file sets (biomes) — for a MobSpawnSettings.MobSpawnCost for this exact type: a charge the mob adds to a field of nearby charges and an energy budget the sum may not exceed, so a species can be made to thin itself out over distance rather than over a cap. PotentialCalculator is that field, and the rule is per biome and per type rather than global. In vanilla data two biomes use it at all — soul sand valley, for ghasts, skeletons, endermen and striders, and the warped forest, for endermen alone. Everywhere else the biome has no cost for the type, MobSpawnSettings.getMobSpawnCost returns null, and the gate passes without arithmetic.

Construction is itself a filter, and the harshest-tempered one: EntityType.create can return null (entity anatomy has the two gates it checks and the one request flag that skips them). What the spawner does with a null is its own: NaturalSpawner.spawnCategoryForPosition returns outright, not trying the next position. On Peaceful a type not allowed there never gets this far: SpawnPlacements.checkSpawnRules refuses it first, one try at a time.

The species it was about to build came from a list, and that list has a hard-coded exception in front of it. NaturalSpawner.mobsAt asks ChunkGenerator.getMobsAt, which walks the structures the position’s chunk references and, for the first one that declares a StructureSpawnOverride for this category and whose box — a piece’s or the whole start’s, depending on the override’s StructureSpawnOverride.BoundingBoxType — contains the position, replaces the biome’s list entirely with the structure’s. That is how an ocean monument spawns guardians and empties itself of axolotls, and it is a data-pack field (spawn_overrides) on the structure. Fifty-two shipped structures carry the field and six fill it in — and of the twenty-three overrides those six declare, eighteen name nothing at all, which replaces the list with an empty one and so bans the category inside the box (structure spawn overrides). Ahead of all of it sits NaturalSpawner.isInNetherFortressBounds, which is not data-driven at all: if the category is MobCategory.MONSTER and the block below is Blocks.NETHER_BRICKS and the position is anywhere inside a fortress’s bounds, the list is the constant NetherFortressStructure.FORTRESS_ENEMIES. The fortress declares the same list as an override too — the code path just gets there first, over a wider box.

The two caps, and where 289 comes from

A category must pass both caps before a chunk tries it, and they are counted differently. The global cap in NaturalSpawner.SpawnState.canSpawnForCategoryGlobal is MobCategory.getMaxInstancesPerChunk — 70 for MobCategory.MONSTER, 10 for MobCategory.CREATURE — times the number of spawnable chunks, divided by NaturalSpawner.MAGIC_NUMBER. That divisor is 17², and the 17 is not arbitrary: DistanceManager tracks spawn chunks out to eight chunks from each player over a neighbourhood that includes diagonals, so one player contributes a Chebyshev square of 17×17 chunks. The constant normalises the cap back into seventy monsters per player’s worth of area, which is why it grows with player count and shrinks when players stand together.

The local cap is LocalMobCapCalculator.canSpawn, and it is a veto rather than a budget: it walks the players near this chunk and answers yes the moment it finds one under the raw per-chunk number for the category. With no player near the chunk the walk would find nobody and answer no, but the natural spawner never gets there: near here is the same 128-block test to the chunk centre two gates above, run over the same players in the same tick, so a chunk that reached this gate always has someone near it. (SharedConstants.DEBUG_IGNORE_LOCAL_MOB_CAP is the development switch that turns that half off.) The census both caps count from skips any mob that is Mob.isPersistenceRequired or Mob.requiresCustomPersistence — among them a mob name-tagged, holding loot it picked up, leashed or riding something — so a named zombie costs nothing against either cap. That is the same predicate pair that keeps Mob.checkDespawn from discarding it by distance, which is why name it and it stays and name it and it stops counting are one fact and not two.

The spawn distances, and one that is not the number it looks like

NaturalSpawner names three spawn distances — NaturalSpawner.MIN_SPAWN_DISTANCE 24, NaturalSpawner.SPAWN_DISTANCE_CHUNK 8 and NaturalSpawner.SPAWN_DISTANCE_BLOCK 128 — and the two tests against a player compare squared distances, against 576 and 16384. A fourth, NaturalSpawner.INSCRIBED_SQUARE_SPAWN_DISTANCE_CHUNK, is neither 8 nor 24: it is the floor of 8 divided by the square root of two, so 5, and DistanceManager.hasPlayersNearby uses it as the fast yes of a three-way answer — inside 5 chunks certainly near, beyond 8 certainly not, and in between fall through to the real per-player distance test.

What finalizeSpawn settles for the whole pack

Mob.finalizeSpawn adds a triangular random bonus to Attributes.FOLLOW_RANGE under Mob.RANDOM_SPAWN_BONUS_ID and rolls a 5 % chance of left-handedness. Zombie.finalizeSpawn then rolls loot-pickup and door-breaking against local difficulty, equipment and its enchantments, and — the part players notice — returns a Zombie.ZombieGroupData that the loop feeds back into the next mob of the same group. Baby-or-adult is decided once, by the first zombie, and inherited by the rest: a spawn group is all-baby or all-adult, never mixed. A baby gets a 5 % roll at an existing unridden Chicken within five blocks out on each side and three up or down, and only if that roll fails a second 5 % roll to create one. Two different limits can end a pack. Mob.isMaxGroupSizeReached breaks the current group and lets the next of the three attempts start, though only a tropical fish that does not school ever answers yes, so a zombie’s group ends when its tries run out; Mob.getMaxSpawnClusterSize returns outright and kills all three. Mob’s base value is four, and seven classes override it: AbstractHorse to 6, Wolf and AbstractFish to 8 — with AbstractSchoolingFish overriding again to return its own school size — and Ghast, HappyGhast and Pillager down to 1.

The variant that same method picks

Seven species pick a variant in their own override of Mob.finalizeSpawn — Chicken, Cow, Pig, Cat, Frog, Wolf and ZombieNautilus — and all seven do it through the same call, VariantUtils.selectVariantToSpawn, handed a SpawnContext built from the level and the block position. Each variant in the registry carries a SpawnPrioritySelectors: a list of conditions, each with an integer priority. PriorityProvider.select unpacks every variant’s selectors into one list, sorts it by priority descending, and walks it keeping entries whose condition passes and dropping every entry below the highest priority that has matched — so the highest matching priority wins outright, and PriorityProvider.pick then chooses uniformly at random among whatever is tied at it. A variant whose selector has no condition at all (PriorityProvider.alwaysTrue, which SpawnPrioritySelectors.fallback wraps) always matches, which is how a default loses to a biome-specific variant without either knowing the other exists. SpawnConditions registers three condition types into BuiltInRegistries.SPAWN_CONDITION_TYPE — BiomeCheck, StructureCheck and MoonBrightnessCheck — and the shipped data uses the priority to mean rarity: an all-black cat is priority 1 inside a swamp hut and priority 0 anywhere the moon is at least 0.9 bright, so inside a hut it always wins, and under a full moon it only joins the other ten variants in an even draw. The registry of codecs behind one interface is the data-driven type pattern once more, and this is the entry that pattern’s table sends here for.

The other ways in

Natural spawning is one caller of LevelWriter.addFreshEntity among many. BaseSpawner drives the SpawnerBlockEntity and TrialSpawner the trial chambers (block entities owns the block half of both); SpawnEggItem and SummonCommand are the deliberate ones; AgeableMob.getBreedOffspring makes babies; a raid spawns its waves through Raid and its Raiders; and five CustomSpawner implementations — PhantomSpawner, PatrolSpawner, CatSpawner, WanderingTraderSpawner and VillageSiege — are ticked as a list by ServerLevel.tickCustomSpawners after the chunks, a list only the overworld is constructed with (the level tick). Each stamps one of the nineteen EntitySpawnReason constants, though not a distinct one — phantoms and cats both count as natural, sieges and wandering traders both as event — and that reason never leaves the server: nothing about why something spawned crosses the wire. Nor does all of it change anything: six of the nineteen are labels no class tests. Of the thirteen that are tested, eleven are compared directly, nearly always in a species’ own override of Mob.finalizeSpawn, and the two spawner reasons only through EntitySpawnReason.isSpawner and EntitySpawnReason.ignoresLightRequirements, in the spawn rules (entity spawn reasons lists every site).

Entry: what addFreshEntity actually does

LevelWriter.addFreshEntity is a default method that returns false. Level does not override it and neither does ClientLevel. Exactly two classes do. ServerLevel.addFreshEntity is the one this page is about. WorldGenRegion.addFreshEntity is the other, and it does something entirely different: it writes the entity straight into the ChunkAccess’s own list and never touches PersistentEntitySectionManager at all. That is the path of EntitySpawnReason.CHUNK_GENERATION, and of EntitySpawnReason.STRUCTURE for the mobs a structure piece places — worldgen mobs are parked in the proto-chunk as NBT and only enter the manager later, when the chunk is promoted and PersistentEntitySectionManager.addWorldGenChunkEntities is handed them (the generation pipeline). On the client the only way in is ClientLevel.addEntity, called from the packet handler, and it begins by removing whatever already holds that network id. The server door takes the passengers with it: ServerLevelAccessor.addFreshEntityWithPassengers walks Entity.getSelfAndPassengers, vehicle first, and puts each of them through the four steps below — all four on one tick, and the order of them is the thing to read.

sequenceDiagram
    participant SL as ServerLevel
    participant PESM as PersistentEntity<br/>SectionManager
    participant CM as ChunkMap
    participant ETL as EntityTickList
    participant Wire as the network

    Note over SL,Wire: all of it on the tick it is created
    SL->>SL: addFreshEntity, once per body, the vehicle first
    SL->>PESM: addNewEntity
    PESM->>PESM: claim the UUID, file it in its EntitySection, install the Callback
    PESM->>SL: LevelCallback.onCreated
    PESM->>SL: LevelCallback.onTrackingStart
    SL->>CM: addEntity, through ServerChunkCache
    CM->>Wire: ClientboundAddEntityPacket, with data, attributes, equipment and leash
    PESM->>SL: LevelCallback.onTickingStart
    SL->>ETL: add

The manager files the entity before anything else hears of it, and the client is told in the middle: tracking, then the packet, then ticking. Leaving and being written are the other half, and they belong to the state machine below.

PersistentEntitySectionManager.addNewEntity is the manager’s one public step in that figure, and LevelCallback.onCreated the first thing it raises. ServerLevel.EntityCallbacks is the class those callbacks land in, and the figure draws every call through it: PersistentEntitySectionManager.startTracking and PersistentEntitySectionManager.startTicking are private, and apart from the first of them filing the entity in the manager’s EntityLookup, all they do is raise LevelCallback.onTrackingStart and LevelCallback.onTickingStart — two of LevelCallback’s seven — which is how ServerChunkCache.addEntity and EntityTickList.add come to be called by a class that knows about neither. ServerLevel.EntityCallbacks is also where a surprising amount of the level hangs: the scoreboard entry, the players list and the sleeping-player recount, waypoint tracking, the navigating-mob set the block-change notifier walks, the EnderDragonPart id registrations, and the dynamic DynamicGameEventListener registration (game events).

Findable, ticking, or neither

Findable means findable in the entity grid, which is a second grid: entities live in EntitySections of 16³ blocks keyed by SectionPos, held by an EntitySectionStorage that is nothing to do with the block sections of the same size (chunk anatomy). A section holds a ClassInstanceMultiMap, so asking a section for every arrow in it costs one class lookup rather than a walk, and — the part that matters here — a section carries its own Visibility, which is why status is a property of a section and not of an entity. Beside it EntityLookup keeps the flat id-and-UUID index; which of the two a query uses is Part XIII’s subject.

Visibility is the whole idea in three constants — Visibility.HIDDEN, Visibility.TRACKED, Visibility.TICKING — and Visibility.fromFullChunkStatus is the projection: FullChunkStatus.FULL makes a chunk’s entities findable, FullChunkStatus.ENTITY_TICKING makes them tick, anything less hides them (tickets and loading).

stateDiagram-v2
    state "Visibility.HIDDEN" as H
    state "Visibility.TRACKED" as T
    state "Visibility.TICKING" as K
    H --> T : reaches FULL, tracking starts
    T --> K : reaches ENTITY_TICKING, ticking starts
    H --> K : straight up, tracking starts then ticking
    K --> T : below ENTITY_TICKING, ticking stops
    T --> H : below FULL, tracking stops
    K --> H : straight down, ticking stops then tracking
    H --> [*] : the manager's unload pass, EntityStorage.storeEntities

These are a section’s states, not an entity’s: an entity other than a player takes its section’s, and changes when that section does or when it walks into another. A jump either way crosses the middle state inside one call, and the exit comes after the client was told, not with it.

What the jumps hide is an order. PersistentEntitySectionManager.updateChunkStatus runs its four tests in a fixed order — stop ticking, stop tracking, start tracking, start ticking — so on the way up an entity becomes trackable before it becomes tickable, and on the way down it stops ticking before it stops being tracked. Adding and removing an entity keep the same order. The one path that does not is an entity walking across a section boundary into a differently-statused section, which runs through PersistentEntitySectionManager.Callback instead: tracking first in both directions and then ticking, and LevelCallback.onSectionChange at the end when the new section is findable. And the always-ticking exemption, Entity.isAlwaysTicking, which lifts an entity clear of every one of those filters, is claimed by exactly one class in 26.3: Player.

The tick it gets, and the despawn check it gets anyway

The entity block of ServerLevel.tick is skipped in its entirety once the level has gone 300 ticks without a ticket that keeps a dimension awake or an entity arriving from another dimension. Otherwise the tick walks EntityTickList and, for each entry that is neither removed nor frozen by TickRateManager.isEntityFrozen, calls Entity.checkDespawn — whose base implementation is empty, overridden only by Mob, EnderDragon, WitherBoss and ShulkerBullet, so for an item or an arrow it is a no-op call. Only after that does the range test run: a ServerPlayer is exempt outright, everything else needs DistanceManager.inEntityTickingRange for its own chunk, and an entity already riding a live vehicle returns here to be ticked by ServerLevel.tickPassenger instead.

So despawn is checked for every live, unfrozen member of the tick list while ticking is not. The two sets should agree, and mostly do — the tick list is the ticking set — but they read different sources: membership follows the chunk holder’s promoted FullChunkStatus, while the per-tick gate reads the simulation tracker directly, and the two need not have converged in the same tick.

EntityTickList is what makes that walk safe. It holds two id-keyed maps and a nullable reference to whichever one is being iterated. The first add or remove during a walk copies the live map into the spare and swaps them, so the in-flight iterator finishes over the original, untouched map and the mutation lands in the new one; every later one in the same walk writes to the new map directly. A second concurrent walk is refused outright, by checking that reference rather than a boolean.

Ending one: Mob.checkDespawn

Left in a loaded chunk, the zombie ends through Mob.checkDespawn, whose first branch consults no player at all: on Peaceful, anything whose type is not EntityType.isAllowedInPeaceful is discarded on the spot, ahead of even the persistence check. Past that, persistence does not end the method: it only blocks the two discards below, and a named zombie’s LivingEntity.noActionTime still resets whenever a player comes near.

The rest is measured against the nearest non-spectator player — and if there is no player in the level at all, both remaining branches do nothing, so a mob alone in a world never despawns by distance. Beyond MobCategory.getDespawnDistance — 128 blocks for every category except MobCategory.WATER_AMBIENT, which is 64 — it is discarded instantly. Beyond MobCategory.getNoDespawnDistance, a flat 32 for every category, it is discarded on a 1-in-800 roll, but only once LivingEntity.noActionTime has passed 600; inside that 32 the same method resets that counter to zero, so standing near a mob keeps it alive. Both distance branches also require Mob.removeWhenFarAway, the per-species veto — and it is broader than people expect: Animal returns false, which only eight classes below it override (a hoglin, a chicken carrying a jockey and an untamed cat among them), and Villager returns false for every villager, so a wild cow on a hilltop never despawns by distance at all. That is why 128 blocks and it is gone is a species-dependent rule and not a universal one. What both branches call is Entity.discard, which destroys and does not save.

Ending two: the chunk goes away

Walk far enough instead and the chunk falls out of entity-ticking: the zombie stops ticking but stays findable. Fall to Visibility.HIDDEN and two things happen, one after the other. At the status change, PersistentEntitySectionManager.updateChunkStatus stops ticking and stops tracking the section’s entities, and stopping tracking is what reaches ChunkMap and sends ClientboundRemoveEntitiesPacket — the client is told then, not at the write. The chunk key goes into the manager’s unload set, and the manager’s next PersistentEntitySectionManager.tick — later in the same server tick, or in the next — runs PersistentEntitySectionManager.processUnloads over it.

That later step is not a formality, and it can refuse. A chunk whose entity data is still being read back off disk is deferred to a future tick. A chunk that has entities to save but has never been read is loaded first, so the two sets can be merged — the unload triggers a load. Only then does EntityStorage.storeEntities write the Entities list and a Position into the entities/ region files (chunk storage), which are separate from the block region/ files and which remember the chunks that came back empty so they are never re-read. Each saved entity and its passengers then take Entity.RemovalReason.UNLOADED_TO_CHUNK and drop their level callback.

Entity.shouldBeSaved has three clauses, and past two overrides that refuse outright — Player, which is saved in its own file, and EnderDragonPart — they decide the whole contents of that file, bar the types built EntityType.Builder.noSave, which pass them and write nothing. Passengers are written inside their vehicle, never beside it, so anything currently riding is refused; a vehicle carrying exactly one player among its passengers is refused too, because it travels in that player’s own data instead; and — the clause that is easy to miss, because it is the first one in the method — an entity already carrying a non-saving removal reason is skipped, which is what keeps a discarded mob still sitting in a section out of the file.

Five reasons, one label

reasondestroyssaveswhat leaves it behind
Entity.RemovalReason.KILLEDyesnodeath, in every sense the game means it
Entity.RemovalReason.DISCARDEDyesnoEntity.discard, every despawn, a ConversionType.SINGLE conversion (Mob.convertTo adds the new mob, then discards the old — ConversionType.SPLIT_ON_DEATH keeps it), the client replacing a network id
Entity.RemovalReason.UNLOADED_TO_CHUNKnoyesthe unload above — the only reason that saves
Entity.RemovalReason.UNLOADED_WITH_PLAYERnonoa player leaving, and the vehicle and ender pearls that travel in its own save data
Entity.RemovalReason.CHANGED_DIMENSIONnonoa change of dimension, where the entity is rebuilt on the far side — a player only on its way out of the End

Destroys means LevelCallback.onDestroyed fires — the scoreboard entry and the waypoint go. An unloading zombie keeps all of it, because Entity.RemovalReason.UNLOADED_TO_CHUNK does not destroy. The five are not a state machine, and Entity.setRemoved is where that shows: it writes the reason only if none is set, so the entity’s stored reason is the first one and a second call cannot change it — but the rest of the method runs every time, and it runs on two different reasons at once. The dismount branch reads the stored reason (it dismounts from a vehicle only when that reason destroys); passengers are dropped unconditionally; and EntityInLevelCallback.onRemove is then fired with the reason this call was given, which on a second call is not the one the entity is carrying. The one link a removal does not break is an EntityReference somebody else is holding (entity anatomy), which decays back to a UUID rather than to nothing.

Where to look

NaturalSpawner is one file of static methods and the cascade above is its table of contents; read NaturalSpawner.spawnCategoryForPosition first, then NaturalSpawner.SpawnState and LocalMobCapCalculator for the two caps and SpawnPlacements for the per-type rules the cascade consults. Mob.finalizeSpawn is where a group becomes a group and Mob.checkDespawn where one ends. For the other half of the page, start at PersistentEntitySectionManager.updateChunkStatus — the four tests in their fixed order — with Visibility and EntityTickList beside it, and finish on Entity.setRemoved and Entity.RemovalReason. Two doors the page leaves closed: EntityLookup, the flat index a command selector walks when it has no area to search, and TransientEntitySectionManager, the client’s much simpler counterpart to everything above.

Before this page: authority, on which side is allowed to decide any of it. After it: synched entity data — what the ClientboundAddEntityPacket bundle above is carrying, and how it stays current.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Synched entity data

Verified against Minecraft 26.3 · Part VI · A player shears a sheep: one byte flips on the server, and the wool disappears on every screen in tracking range.

You right-click a sheep with shears. Somewhere on the server a single byte changes — bit four of one entry in a nineteen-slot array the sheep carries — and before that tick ends, every player watching has been told, by a packet that spends four bytes after the entity id. That array is SynchedEntityData, the channel the server uses to describe an entity to the clients that see it: the health bar over another player, a sneaking crouch, an armour stand’s pose, an item frame’s item, this sheep’s wool. It is a numbered array, and the numbers are the surprising part. The slot the wool lives in is not written anywhere in Sheep: it is 18 because eighteen slots were handed out above Sheep in its superclass chain, and one new field on Entity would renumber every entity in the game. Ids are ordinals handed out down the class tree by a single shared ClassTreeIdRegistry, which walks only the superclass chain: Entity takes 0 to 7, LivingEntity 8 to 14, Mob 15, AgeableMob 16 and 17, and Sheep, last in the chain, gets 18. Nothing names these numbers, nothing writes them down, and they stop at 254 — because on the wire, 255 means end of packet.

The cast

classwhat it decidesthread
SynchedEntityDataone entity’s numbered array, and whether anything in it has changed since the last flushone container per side, a live one confined to the Server thread or the Render thread
SynchedEntityData.DataItemone slot: its value, the default it was built with, and its own dirty flagwith its container
EntityDataAccessorthe key — an int id and a serializer, equal to another accessor on the id aloneimmutable, shared by every instance of the class
ClassTreeIdRegistrywhich id a SynchedEntityData.defineId call gets, from the last id already taken by an ancestor classwhichever thread first initialises the class
EntityDataSerializersthe 44 registered serializers and the wire id of each, in registration ordera static block, once
ServerEntitywhether this entity sends anything this tick, and whatthe Server thread
ClientboundSetEntityDataPacketthe wire form: an entity id, then id/serializer/value triples, then 255encoded on the Netty pipeline, built on the Server thread
ClientPacketListenerapplying an incoming batch to the client’s own containerthe Render thread

Nineteen slots, and where the numbers come from

SynchedEntityData.defineId is called from the static initialiser of an entity class and asks ClassTreeIdRegistry.define for a number. ClassTreeIdRegistry keeps one map from class to last id issued, and ClassTreeIdRegistry.getLastIdFor walks up the superclass chain until it finds an entry — so a subclass continues its parent’s numbering rather than starting over. Java’s guarantee that a superclass initialises before its subclass is the entire ordering mechanism. ClassTreeIdRegistry.getCount is the same walk plus one, and it is what sizes the array.

A Sheep — Entity → LivingEntity → Mob → PathfinderMob → AgeableMob → Animal → Sheep, of which PathfinderMob and Animal define nothing — therefore has exactly nineteen slots:

idfieldserializerdefault
0Entity.DATA_SHARED_FLAGS_IDEntityDataSerializers.BYTE0
1Entity.DATA_AIR_SUPPLY_IDEntityDataSerializers.INTEntity.getMaxAirSupply
2Entity.DATA_CUSTOM_NAMEEntityDataSerializers.OPTIONAL_COMPONENTempty
3Entity.DATA_CUSTOM_NAME_VISIBLEEntityDataSerializers.BOOLEANfalse
4Entity.DATA_SILENTEntityDataSerializers.BOOLEANfalse
5Entity.DATA_NO_GRAVITYEntityDataSerializers.BOOLEANfalse
6Entity.DATA_POSEEntityDataSerializers.POSEPose.STANDING
7Entity.DATA_TICKS_FROZENEntityDataSerializers.INT0
8LivingEntity.DATA_LIVING_ENTITY_FLAGSEntityDataSerializers.BYTE0
9LivingEntity.DATA_HEALTH_IDEntityDataSerializers.FLOAT1.0
10LivingEntity.DATA_EFFECT_PARTICLESEntityDataSerializers.PARTICLESempty
11LivingEntity.DATA_EFFECT_AMBIENCE_IDEntityDataSerializers.BOOLEANfalse
12LivingEntity.DATA_ARROW_COUNT_IDEntityDataSerializers.INT0
13LivingEntity.DATA_STINGER_COUNT_IDEntityDataSerializers.INT0
14LivingEntity.SLEEPING_POS_IDEntityDataSerializers.OPTIONAL_BLOCK_POSempty
15Mob.DATA_MOB_FLAGS_IDEntityDataSerializers.BYTE0
16AgeableMob.DATA_BABY_IDEntityDataSerializers.BOOLEANfalse
17AgeableMob.AGE_LOCKEDEntityDataSerializers.BOOLEANfalse
18Sheep.DATA_WOOL_IDEntityDataSerializers.BYTE0

There is one of those arrays per side, not one per entity: the server’s Sheep has its own container and your client’s Sheep has another, built independently from the same class chain, and the packet below is the only thing that ever reconciles them.

Declaration order is not definition order

The declaration order and the definition order are two different lists. Ids come from where SynchedEntityData.defineId sits in the class body; the values come from the Entity constructor, which defines its own eight and then calls the abstract Entity.defineSynchedData that every subclass with data of its own overrides and chains up through. LivingEntity defines its seven in a different order from the one that numbered them, and it does not matter, because SynchedEntityData.Builder.define writes each item at EntityDataAccessor.id. SynchedEntityData.Builder.build then refuses to hand over a container with any slot still null, naming the id it is missing — which is why SynchedEntityData.get can read a slot with no null check at all.

Four slots that are bitfields

Four of the nineteen are bitfields, and they are the dense part of the channel. Slot 0 is Entity.FLAG_ONFIRE 0, Entity.FLAG_SHIFT_KEY_DOWN 1, Entity.FLAG_SPRINTING 3, Entity.FLAG_SWIMMING 4, Entity.FLAG_INVISIBLE 5, Entity.FLAG_GLOWING 6 and Entity.FLAG_FALL_FLYING 7 — bit 2 is unnamed and unused — behind Entity.getSharedFlag and Entity.setSharedFlag, whose parameter carries a purpose-built Entity.Flags type-use annotation. Slot 8 carries using an item, off hand and spin attack (LivingEntity.LIVING_ENTITY_FLAG_IS_USING, LivingEntity.LIVING_ENTITY_FLAG_OFF_HAND, LivingEntity.LIVING_ENTITY_FLAG_SPIN_ATTACK), slot 15 no-AI, left-handed and aggressive behind Mob.setNoAi, Mob.setLeftHanded and Mob.setAggressive, and slot 18 packs a DyeColor id into the low nibble and sheared into bit four — Sheep.getColor, Sheep.setColor, Sheep.isSheared, Sheep.setSheared, and the same storage read out as DataComponents.SHEEP_COLOR by Sheep.get and written back through Sheep.applyImplicitComponent.

Nothing may be inserted above you

The numbering belongs to the class, not to the concept. Avatar — the class between LivingEntity and Player (entity anatomy) — owns Avatar.DATA_PLAYER_MAIN_HAND and Avatar.DATA_PLAYER_MODE_CUSTOMISATION, so the skin-part toggles belong to every avatar, while Player.DATA_PLAYER_ABSORPTION_ID, Player.DATA_SCORE_ID and the two shoulder parrots (Player.DATA_SHOULDER_PARROT_LEFT, Player.DATA_SHOULDER_PARROT_RIGHT, optional ints rather than NBT) sit one level further down.

That is the whole answer to can two mods both add a field to LivingEntity: only by accident. Inserting, removing or reordering a SynchedEntityData.defineId on a base class shifts every id below it in every subclass, so two mods claiming the same number either collide in SynchedEntityData.Builder.define, which rejects a duplicate, or land a value in the wrong slot, which throws the serializer check on the client when the two serializers differ and passes silently when they match. And one class treats these numbers as what they are: Display.RENDER_STATE_IDS is an int set of the eight accessor ids that force a render-state rebuild, tested against each incoming accessor, with Display.TextDisplay keeping a second set of its own. Nothing in the codebase demonstrates more plainly that a slot number is an ordinal and not a name.

The serializer is the other half of the key

An EntityDataAccessor is an id and an EntityDataSerializer, and the serializer is what turns the value into bytes. The interface is deliberately thin: a StreamCodec returned by EntityDataSerializer.codec — returned, not extended — paired with an EntityDataSerializer.copy that copies a value as it is packed, so a later change to the container’s live value cannot reach a packet already built or the tracker’s snapshot. EntityDataSerializer.ForValueType is the immutable case, where copying is identity, and EntityDataSerializer.forValueType builds one from a codec alone.

EntityDataSerializers registers 44 of them into a CrudeIncrementalIntIdentityHashBiMap, from a single static block, so registration order is the wire id — EntityDataSerializers.BYTE is 0, EntityDataSerializers.POSE is 20, EntityDataSerializers.DYE_COLOR is 43 and last. Several of them carry Holders of data-pack registries — the per-species variants and the painting variants — and those, the item stacks, the text components and the particles are why the buffer on both sides is a RegistryFriendlyByteBuf rather than a plain one (codecs). The full list with wire ids and value types is the serializer table. EntityDataSerializers.registerSerializer is public and is the only thing that fills the bimap: vanilla calls it 44 times from that one block, and nothing else in the tree ever calls it again. It is a mod extension point shipped with no caller outside its own file.

Shears to wool gone, inside one tick

sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Sheep as Sheep
    participant SED as SynchedEntityData

    MPGM->>SGPL: ServerboundInteractPacket, from MultiPlayerGameMode.interact
    MPGM->>MPGM: then the same interaction locally, unless spectator
    rect rgba(0, 0, 0, 0.04)
    Note over SGPL,SED: the server tick, before MinecraftServer.tickServer
    SGPL->>SGPL: Entity.setShiftKeyDown, then the range and border checks
    SGPL->>Sheep: Mob.interact, down from Player.interactOn
    Sheep->>Sheep: Mob.checkAndHandle<br/>ImportantInteractions
    Sheep->>Sheep: Sheep.mobInteract
    Sheep->>Sheep: Sheep.shear: the sound, the loot table, then Sheep.setSheared
    Sheep->>SED: SynchedEntityData.set, slot 18 with bit four
    SED->>Sheep: Entity.onSyncedDataUpdated, before anything is dirty
    SED->>SED: SynchedEntityData.<br/>DataItem.setDirty
    end

The server’s container, and the one moment worth slowing down for: the callback to Sheep runs first and the item is marked dirty after it, not the other way round. The whole band is one tick, and it is over before MinecraftServer.tickServer has begun.

Later in that same tick the container is drained, and what the client writes it into is a second container — one per side, not one per entity:

sequenceDiagram
    box transparent the server
    participant SED as SynchedEntityData
    participant SE as ServerEntity
    end
    box transparent the client
    participant CPL as ClientPacketListener
    participant CSED as SynchedEntityData
    end

    rect rgba(0, 0, 0, 0.04)
    Note over SED,SE: the same tick, the ServerLevel.chunkSource phase
    SE->>SED: SynchedEntityData.packDirty, once the gate opens
    SED-->>SE: one SynchedEntityData.DataValue: id 18, serializer 0, one byte
    SE->>CPL: ClientboundSetEntityDataPacket, flushed at the end of the tick
    end
    CPL->>CSED: SynchedEntityData.assignValues, per item then the batch
    Note over CPL,CSED: next frame: SheepRenderer.extractRenderState reads Sheep.isSheared

Two containers, one per side, and the packet is the only thing that ever reconciles them. The server’s half runs later in the same tick as the shear, in the chunk-source phase; the client’s container was built with the client’s own Sheep long before, and changes only when the packet lands.

The click. MultiPlayerGameMode.interact sends a ServerboundInteractPacket — a flat record of entity id, hand, an entity-relative hit location and the secondary-action flag, attacks having left for ServerboundAttackPacket — and then, on the next line, runs the interaction locally as a prediction, unless the local game mode is spectator. The packet goes first.

The server checks the geometry, not the outcome. ServerGamePacketListenerImpl.handleInteract confirms the thread with PacketUtils.ensureRunningOnSameThread and that the client has loaded, resolves the entity with ServerLevel.getEntityOrPart, tests the world border and Player.isWithinEntityInteractionRange, and checks the held item against the level’s feature flags. Before any of the geometry, though, it writes the packet’s secondary-action flag straight into Entity.setShiftKeyDown — which is itself a synched-data write on the player, so every interaction packet is also a potential update on slot 0.

Dispatch down the hierarchy. Player.interactOn calls Entity.interact, which dispatches virtually to the most derived override, Mob.interact. That runs three things in a fixed order: Mob.checkAndHandleImportantInteractions (name tags, spawn eggs), then the superclass hook Entity.interact with its leashing branch, and only if that passes, Sheep.mobInteract — the base hook runs between the two mob hooks, not before them. The shears test is item identity against Items.SHEARS, not a tag and not a component, plus Sheep.readyForShearing from the shared Shearable interface, whose other implementors are MushroomCow, SnowGolem, Bogged, CopperGolem and SulfurCube. A sheep that is not ready returns InteractionResult.CONSUME rather than falling through, which is why shears on an already-sheared sheep do nothing visible at all.

The effect, and one byte. Sheep.shear plays SoundEvents.SHEEP_SHEAR, drops wool through LivingEntity.dropFromShearingLootTable with BuiltInLootTables.SHEAR_SHEEP — the tool is passed in, so the loot table can see it — and calls Sheep.setSheared, which ors bit four into slot 18. SynchedEntityData.set compares the new value against the current one, stores it, calls Entity.onSyncedDataUpdated for that accessor, and then marks the item and the container dirty. Sheep does not override the hook, and the base implementation reacts to exactly one accessor, Entity.DATA_POSE, by calling Entity.refreshDimensions — which is also the first answer to whether any of this channel touches the physics the client simulates. A few slots do. Entity.DATA_POSE travels on its own EntityDataSerializers.POSE, and the hook resizes the hitbox on whichever side just received it (entity anatomy has what the resize costs); a dozen subclasses resize the same way on a value of their own, this sheep’s AgeableMob.DATA_BABY_ID among them; Entity.DATA_NO_GRAVITY switches gravity off; and a handful more steer what the client simulates, a pig’s boost, a sleeper’s bed, an item in use slowing your own input. The rest of the channel is cosmetic to the client, or read back by gameplay code that already knew. Back in Sheep.mobInteract: Entity.gameEvent with GameEvent.SHEAR for the sculk listeners (game events), ItemStack.hurtAndBreak on the shears, and InteractionResult.SUCCESS_SERVER.

The send, in the same tick. MinecraftServer.processPacketsAndTick drains the queue with PacketProcessor.processQueuedPackets and only then calls MinecraftServer.tickServer, so the shear happened before the tick proper began. ServerLevel.tick reaches its chunkSource phase after block and fluid ticks and before block events and entity ticking, and that phase runs ChunkMap.tick, whose loop over ChunkMap.TrackedEntity is the only caller of ServerEntity.sendChanges in the tree. The dirty flag opens the gate, ServerEntity.sendDirtyEntityData calls SynchedEntityData.packDirty, and one ClientboundSetEntityDataPacket goes to every tracking player, and to the entity itself when it is a player. After the entity id, the wire carries an unsigned byte 18, a var-int 0 for EntityDataSerializers.BYTE, one payload byte and then the terminator 255 — and that terminator is the whole reason ids stop at 254: the 255 ClientboundSetEntityDataPacket.EOF_MARKER names, and the 254 SynchedEntityData.MAX_ID_VALUE names.

The packet is not on the wire yet. MinecraftServer.tickChildren calls ServerCommonPacketListenerImpl.suspendFlushing on every player before it ticks any level and ServerCommonPacketListenerImpl.resumeFlushing at the end of the tick, so a whole tick’s packets leave together.

The apply, and the frame. ClientPacketListener.handleSetEntityData looks the entity up in ClientLevel and silently drops the entire packet if the id is unknown, then calls SynchedEntityData.assignValues, which checks that the incoming serializer is the one the accessor was defined with — a mismatch throws, loudly, on the client — stores each value, fires Entity.onSyncedDataUpdated per item and then the batch overload once. That batch overload, SyncedDataHolder.onSyncedDataUpdated, is the only place a client could see a whole update atomically, and nothing in the game overrides Entity’s empty body of it. Nothing tells the renderer. It finds out next frame, by reading the sheep again from scratch: SheepRenderer.extractRenderState copies Sheep.isSheared and Sheep.getColor into a fresh SheepRenderState, after which SheepWoolLayer draws nothing. The wool vanishing is a layer skipped, not a model swap — and only one layer, because SheepWoolUndercoatLayer tests colour, baby and invisibility but never the sheared flag, so a sheared coloured sheep still draws its undercoat.

The gate that holds a packet back

Two tests stand between a dirty byte and the wire, and only the first can hold it back:

flowchart TD
    IN{"ChunkMap.tick: a section changed, Entity.needsSync, or in entity-ticking range?"}
    SC["ServerEntity.sendChanges, opening with Entity.updateDataBeforeSync"]
    GATE{"the tracker's UpdateInterval, Entity.needsSync, or SynchedEntityData.isDirty?"}
    SEND["position, rotation and motion"]
    DATA["ServerEntity.sendDirtyEntityData"]
    HOLD["nothing goes out, and the dirty flags survive the tick"]
    QUIET["no position this tick, and no data was waiting"]

    IN -- "no" --> HOLD
    IN -- "yes" --> SC
    SC -- "an ItemFrame, every tenth tick: the map bypass" --> DATA
    SC --> GATE
    GATE -- "yes" --> SEND
    SEND --> DATA
    GATE -- "no" --> QUIET

Two tests, and the one thing that gets past the second without answering it. A dirty container is one of the second test’s own terms, so only the first can hold a byte back, and what it holds is kept for a later tick, not lost.

The first is not this page’s: ChunkMap.tick decides whether ServerEntity.sendChanges is called at all, on three conditions what the client is told sets out. An entity that fails them keeps its dirty data until one of the three becomes true. Entity.needsSync is the entity’s own way of forcing the issue: a flag that a push, a load and twenty-one other classes set to say ask about me this tick whatever the schedule says, read by both tests. The second test lets a dirty byte through, since SynchedEntityData.isDirty is one of its terms. Inside ServerEntity.sendChanges, that gate covers the position block and the usual ServerEntity.sendDirtyEntityData call, which is why shearing a sheep can also send that sheep’s position and rotation this tick: synched data is, incidentally, a latency channel for movement.

The interval comes from EntityType.updateInterval, fixed on the type at registration and turned into an UpdateInterval when ChunkMap starts tracking the entity — 2 for a player, 3 by default, and Integer.MAX_VALUE for eight types including EntityTypes.ITEM_FRAME, which becomes UpdateInterval.NEVER, so their interval branch never fires at all (entity anatomy has the eight and the tracking range beside it).

ServerEntity.sendChanges also has an ItemFrame special case that runs every tenth tick before the gate: it sends a framed map’s own update packets, which a player not carrying that map gets no other way, and then calls ServerEntity.sendDirtyEntityData, the one call to the synched-data flush outside the gate. (Two sends also sit outside the gate — the passengers packet and the Entity.syncVelocity motion packet — but neither touches the data channel.) ServerEntity.handleMinecartPosRot calls it too, but from inside the gate, not around it. Entity.syncPosition is the other lever. A living entity’s SteppedInterpolationTracker (every living type’s but a shulker’s) takes the flag first and records the position as a step; for the rest it realigns the tracker’s own counter so the very next evaluation lands on a multiple of the interval, which does nothing for a type whose interval is UpdateInterval.NEVER.

Entity.updateDataBeforeSync opens ServerEntity.sendChanges, ahead of the gate, and it is the hook LivingEntity overrides to reconcile its effects: LivingEntity.updateInvisibilityStatus and the glowing status write slot 0, and the swirl list goes into slot 10. A mob effect that expired this tick can therefore dirty the container and open its own gate, in the same call that goes on to read the flag.

One flush carries at most one value per slot, because SynchedEntityData.packDirty clears each flag as it packs — but the comparison in SynchedEntityData.set is against the current value, not the last one sent, so setting A then B then A within a tick dirties the item twice and then sends A, a value the client already had. The only thing that never dirties is setting a slot to what it already holds, and even that can be overridden: SynchedEntityData.set has a three-argument force-dirty form that skips the comparison. Display uses it to restart an interpolation whose delay was re-set to the same number; CopperGolem’s two uses of it are redundant, because the value it writes is always the previous weather state and so always different.

Five more channels, all keyed by the same entity id

Synched data is one of six clientbound descriptions of an entity, and knowing which one a fact travels on answers most why does the client not know that questions. There is no serverbound counterpart to any of them. Nothing stops SynchedEntityData.set on the client — LocalPlayer prediction does it constantly — but the write goes nowhere: ServerEntity.sendDirtyEntityData is the only caller of SynchedEntityData.packDirty in the whole tree, so the client’s dirty flag is read by nobody and the next server value overwrites the slot (authority). One serverbound packet moves two slots by proxy all the same: ServerboundClientInformationPacket reaches ServerPlayer.updateOptions, which sets the skin-customisation byte and the main hand. And the client’s container is not useless to it — ClientPacketListener.handleRespawn copies the old player’s non-default values straight into the new one when the respawn keeps entity data, the single client-to-client use of the channel.

channelpacketsnote
synched dataClientboundSetEntityDataPacketto trackers and self, when the entity is a player — ServerEntity.Synchronizer.sendToTrackingPlayersAndSelf
attributesClientboundUpdateAttributesPacketflushed by the same ServerEntity.sendDirtyEntityData (attributes)
equipmentClientboundSetEquipmentPacketincremental updates bypass ServerEntity entirely — LivingEntity.handleEquipmentChanges sends them, to trackers only, not the wearer
mob effectsClientboundUpdateMobEffectPacket, ClientboundRemoveMobEffectPacketthe authoritative list, unlike the swirl in LivingEntity.DATA_EFFECT_PARTICLES
position and motionClientboundMoveEntityPacket, ClientboundEntityPositionSyncPacket, ClientboundRotateHeadPacket, ClientboundSetEntityMotionPacket, ClientboundMoveMinecartPacketthe block the synched-data gate shares; ClientboundTeleportEntityPacket is the exception, sent from Entity itself
one-shot eventsClientboundEntityEventPacketa single byte from ServerLevel.broadcastEntityEvent, dispatched by Entity.handleEntityEvent — EntityEvent declares 64 of them

Pairing a viewer who has never seen it

Pairing a new viewer runs the same machinery once. ServerEntity.addPairing calls ServerEntity.sendPairingData, which bundles ClientboundAddEntityPacket with a ClientboundSetEntityDataPacket, attributes, equipment, passengers and a leash link into one ClientboundBundlePacket. The data packet is built not from a fresh pack but from ServerEntity.trackedDataValues, a snapshot taken in the ServerEntity constructor from SynchedEntityData.getNonDefaultValues and refreshed only when SynchedEntityData.packDirty later returns something. Two consequences: an entity still entirely at its defaults sends no data packet on pairing at all, and a change still waiting behind a closed gate is not in that cache, so a new viewer is paired with the older snapshot and gets the change with everyone else on the next flush.

Defaults never travel on pairing, in other words, because SynchedEntityData.getNonDefaultValues skips any item still equal to its SynchedEntityData.DataItem.initialValue — which is why a freshly loaded entity is described by so little. Both sides construct their own defaults independently from the same class chain, and if they ever disagreed no packet would correct it.

Where to look

ClassTreeIdRegistry is one short class and it is where the numbering scheme lives; read it before anything else, then SynchedEntityData itself and its SynchedEntityData.Builder for how the array is filled and checked. EntityDataAccessor and EntityDataSerializers are the key and its other half. On the sending side the whole gate is ServerEntity.sendChanges, with ServerEntity.sendDirtyEntityData as its exit and ServerEntity.sendPairingData, reached from ServerEntity.addPairing, as the pairing path; on the receiving side, ClientPacketListener.handleSetEntityData. Sheep.mobInteract is the scenario’s own method, three lines of which are this whole page. One door worth opening: Display, the class that reads accessor ids as values.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Attributes

Verified against Minecraft 26.3 · Part VI · Strength II is applied to a player: one modifier lands on one attribute, no attribute packet goes on the wire, and the swing three seconds later reads the new number.

Strength II lands on you and thirty seconds later it wears off. In between, one AttributeModifier — an amount of +6, an operation of AttributeModifier.Operation.ADD_VALUE, an id of effect.strength — sits on one attribute of one entity, and every swing you make asks for that attribute and gets a bigger number back. The same mechanism is behind armour points, a horse’s jump strength, the extra reach of a creative-mode player and the distance at which a mob notices you: ask a question, get a number, cheaply, with a defined order of operations. What is surprising is what does not happen. Strength II sends no attribute packet at all. The effect itself goes out, but eight of the forty registered attributes are not client-syncable and Attributes.ATTACK_DAMAGE is one of them, so for the whole thirty seconds your own client’s copy of your attack damage sits at the base value it was born with — 1.0 — and nothing ever tells it otherwise.

A different system with the same words. world/attribute is environment attributes — per-position world properties like the sky’s colour (environment attributes and timelines) — with its own registries and its own class also named AttributeModifier. Nothing on this page refers to it.

The cast

classwhat it decidesthread
Attributeone named number’s default, its description id, its Attribute.Sentiment (tooltip colour only) and the one boolean that decides whether the client is ever toldbuilt in the Attributes class initialiser, read from every thread after
RangedAttributethe minimum and the maximum, and RangedAttribute.sanitizeValue — the clamp. The only subclass, and every registered attribute is oneas above
AttributeSupplierwhat attributes an EntityType has at all, and their base values. Frozenbuilt at class-init, inside DefaultAttributes
AttributeMapwhich of two dirty sets a change lands in, and therefore whether a packet is sentthe Server thread for mutations, the Render thread for the mirror
AttributeInstancethe number: a base value, three modifier indices, a dirty flag and a cacheas above
AttributeModifieran Identifier, an amount and an operation. A record, and inside an instance the identifier alone is its identityimmutable, shared
LivingEntitywhen the update set drains, and what reacts to a changeboth sides, in LivingEntity.tick
ServerEntitywhen the sync set drains and what goes on the wirethe Server thread, in the level tick’s chunkSource phase

Four objects, two dirty sets, and one list that is neither

flowchart TD
    ATTR["Attribute, always a RangedAttribute: a range, and a syncable flag"]
    SUP["AttributeSupplier: one frozen prototype map per EntityType"]
    MAP["AttributeMap: one per LivingEntity, only what was asked for"]
    INST["AttributeInstance: a base value, a cached value, a dirty flag"]
    UPD["AttributeMap.attributesToUpdate: every dirtied attribute"]
    SYNC["AttributeMap.attributesToSync: only the syncable ones"]
    MOD["AttributeMap.onAttributeModified"]
    PAIR["AttributeMap.getSyncableAttributes: not a set, a filter"]
    REACT["LivingEntity.refreshDirtyAttributes, in the entity's own tick"]
    SEND["ServerEntity.sendDirtyEntityData, the ServerLevel.chunkSource phase"]
    WIRE["ClientboundUpdateAttributesPacket"]

    ATTR -- "registered once" --> SUP
    SUP -- "one per EntityType" --> MAP
    MAP -- "AttributeMap.getInstance" --> INST
    INST -- "AttributeInstance.setDirty" --> MOD
    MOD -- "always" --> UPD
    MOD -- "and if syncable" --> SYNC
    MAP -- "when tracking starts" --> PAIR
    UPD --> REACT
    SYNC --> SEND
    SEND --> WIRE
    PAIR --> WIRE

The four objects and the three collections — and every collection is the AttributeMap’s, which is the thing to look at: the instance only calls AttributeInstance.setDirty, and the map decides which sets that lands in.

Four objects carry the whole system — the Attribute, the frozen AttributeSupplier per type, the AttributeMap per entity, and the AttributeInstance per number asked for. The instance keeps three lists of its own, and they belong to a later section; the three in the figure all belong to the map.

Two of them are the dirty sets, and they are not a partition: AttributeMap.onAttributeModified always adds to the update set and additionally to the sync set when the attribute is syncable, so a syncable attribute is in both and a non-syncable one is in the update set alone. The third is not a dirty set at all: AttributeMap.getSyncableAttributes filters the whole live map every time it is asked, and it exists for one caller — ServerEntity.sendPairingData, which has to describe an entity to a player who has never seen it and so cannot work from what has changed since last time.

Forty numbers, every one of them clamped

The forty constants of Attributes register themselves in their own static field initialisers, into BuiltInRegistries.ATTRIBUTE under Registries.ATTRIBUTE. Attributes.bootstrap does nothing but return Attributes.MAX_HEALTH, and exists only as the class-loading trigger BuiltInRegistries needs — which is also why a data pack can reference an attribute but never add one.

Every one of the forty is a RangedAttribute, and RangedAttribute is the only subclass of Attribute, so every attribute in the game has a minimum and a maximum and every computed value passes through one clamp. Several of those bounds are the reason a mechanic behaves the way it does: Attributes.MAX_HEALTH has a minimum of 1, so no entity’s maximum health can reach zero, and Attributes.KNOCKBACK_RESISTANCE has a minimum of −2, so amplified knockback is a legal value rather than a bug. The full list — id, constant, default, minimum, maximum, syncable, sentiment — is the attribute table.

The syncable flag, and the eight that never travel

What has to be said here is the syncable flag, because it explains most of what a client and a server disagree about. Eight of the forty never reach the client: Attributes.ATTACK_DAMAGE, Attributes.ATTACK_KNOCKBACK, Attributes.KNOCKBACK_RESISTANCE, Attributes.FOLLOW_RANGE, Attributes.TEMPT_RANGE, Attributes.SPAWN_REINFORCEMENTS_CHANCE — whose registry id is spawn_reinforcements, disagreeing with its own constant name — and the pair Attributes.WAYPOINT_TRANSMIT_RANGE and Attributes.WAYPOINT_RECEIVE_RANGE. An attribute is syncable only because its registration line called Attribute.setSyncable, and that setter is public and has no freeze behind it, on an object that lives in a registry. Nothing calls it after bootstrap. Nothing stops it either.

The client is not left with a blank where an unsyncable attribute should be, which is the part that makes the silence hard to notice: both sides run the same Attributes class initialiser and build the same DefaultAttributes prototypes, so your client’s copy of your attack damage holds the prototype’s 1.0 from the moment the entity exists. It is not missing. It is a number that was right once and is never corrected — and the client reads it: your own client runs Player.attack for your swing, which asks for your attack damage (authority).

The prototype, frozen at class-init

DefaultAttributes holds one AttributeSupplier per EntityType, each built by chaining builders: LivingEntity.createLivingAttributes is the twenty-six-entry base, Mob.createMobAttributes adds Attributes.FOLLOW_RANGE at 16, Monster.createMonsterAttributes adds Attributes.ATTACK_DAMAGE and nothing else, and each species’ own builder finishes the job. AttributeSupplier.Builder.build keeps the last entry for a repeated attribute, so a species builder does not have to know what its ancestors declared: of the five attributes Zombie.createAttributes names, four are already in the chain and are simply overwritten — attack damage from the monster builder, follow range from the mob builder, movement speed and armour from the twenty-six-entry living base — and only Attributes.SPAWN_REINFORCEMENTS_CHANCE is new. Anything outside MobCategory.MISC with no supplier at all is logged by DefaultAttributes.validate.

A prototype is frozen in the sense that AttributeSupplier.Builder.build arms a callback that throws on any later write: reading one is fine, dirtying one throws. Reading an attribute the type does not have is also fatal — the fallback in AttributeSupplier raises rather than returning a default, which is why LivingEntity.getAttribute is nullable and LivingEntity.getAttributeValue is not.

Player.createAttributes is on Player, not on Avatar. That intermediate class (entity anatomy) owns the player-shaped hitbox but not the attribute set, so Mannequin — the other Avatar — is registered with LivingEntity.createLivingAttributes and gets the plain living set, including the registry’s default movement speed of 0.7 rather than a player’s 0.1. The default is not a dead value: the wandering trader is registered with the bare Mob builder, which sets no speed either, and walks on it.

The map, and which set a change lands in

AttributeMap starts empty. AttributeMap.getValue and AttributeMap.getBaseValue answer from the prototype when the entity has no instance of its own, and change nothing. AttributeMap.getInstance is different: it creates the instance on demand through AttributeSupplier.createInstance, which copies the frozen template with AttributeInstance.replaceFrom — which ends in AttributeInstance.setDirty. So asking for an instance is a mutation: the first call to LivingEntity.getAttribute for a syncable attribute enqueues it for broadcast before any modifier exists, and a share of the attribute packets a busy server sends are caused by something merely asking.

The two sets drain in two different phases

The two sets drain in different phases of the same tick, and that is where the visible lag comes from. ServerEntity.sendDirtyEntityData is reached from ChunkMap.tick, which runs inside ServerLevel.tick’s chunkSource phase — before the entities phase (the level tick). An attribute dirtied during an entity’s own tick (equipment, an effect, powder snow, anything in ServerPlayer.updatePlayerAttributes) has therefore already missed this tick’s send, and a dirty attribute set is not one of the three things that open ServerEntity.sendChanges’s gate (synched entity data owns that gate), so it waits for the next tick whose count is a multiple of the entity’s EntityType.updateInterval (up to two ticks for a player, whose interval is 2, and up to three for the default 3), unless something else opens the gate first. Only a mutation made before the chunk-source phase — a player’s command, an interaction handled out of the packet queue at the top of the server tick — can reach the wire in the tick that produced it, and then only if the gate opens. It is the same phase ordering that puts a block entity’s writes a tick late (block entities).

The update set drains in the entity’s own tick — the entities phase for a mob, the connection phase for a player — in LivingEntity.refreshDirtyAttributes, which calls LivingEntity.onAttributeUpdated once per dirtied attribute and then clears the set. That hook has exactly four branches: clamp health down to a reduced maximum health, clamp absorption, call Entity.refreshDimensions on a scale change, and register or unregister the transmitted waypoint with the ServerWaypointManager. Subclasses add two more — ServerPlayer.onAttributeUpdated takes the receive half of the waypoint pair, and Mob.onAttributeUpdated recomputes the pathfinder’s node budget through PathNavigation.updatePathfinderMaxVisitedNodes on a change to Attributes.FOLLOW_RANGE or Attributes.TEMPT_RANGE (pathfinding owns that budget).

LivingEntity.refreshDirtyAttributes is called from LivingEntity.tick with no side check, so the client runs LivingEntity.onAttributeUpdated too, clamping health and resizing an entity whose scale changed — which is why the waypoint branch inside it is the one that has to test for a ServerLevel explicitly.

A frozen mob is the worked example

Everything above has a cost attached, and the cheapest way to see it is a mob standing in powder snow. LivingEntity.aiStep calls LivingEntity.removeFrost and LivingEntity.tryAddFrost back to back, server-side, with no test for whether anything changed. Each has a gate — the remove only dirties when the modifier is there, the add needs a non-air block underfoot and a non-zero frozen counter — but when both hold, the pair destroys and re-creates a modifier on Attributes.MOVEMENT_SPEED, dirtying a syncable attribute twenty times a second and re-sending that entity’s whole movement-speed modifier list for as long as it stays frozen. Compare ServerPlayer.updatePlayerAttributes, which runs just as often but uses AttributeInstance.addOrUpdateTransientModifier with a constant modifier object, and so dirties nothing after the first tick. The whole difference is whether each write dirties anything at all.

What the map writes to disk

Two more things the map decides, and between them they answer why did my /attribute change survive death? AttributeMap.pack writes every instantiated instance, base value included, so a base value set by command persists with no modifier attached to carry it — AttributeInstance.pack writes only the permanent modifiers of each. And on respawn, ServerPlayer.restoreFrom always calls AttributeMap.assignBaseValues but calls AttributeMap.assignPermanentModifiers only on a full restore: returning from the End, not an ordinary death. Base values always come across, a command-added modifier only sometimes.

The instance: three indices and one cached number

An AttributeInstance keeps its modifiers three times over: bucketed by AttributeModifier.Operation for the arithmetic, indexed by Identifier for lookup and duplicate detection, and a second id-index of the permanent ones for saving. A modifier’s identity is its Identifier alone — there is no UUID and no name — so two systems that pick the same identifier for the same attribute collide, and AttributeInstance.addTransientModifier and AttributeInstance.addPermanentModifier throw rather than silently overwrite. AttributeInstance.addOrUpdateTransientModifier and AttributeInstance.addOrReplacePermanentModifier are the safe forms. Vanilla mostly removes by id before it adds, or uses the safe forms; three mob classes and AttributeCommand guard with AttributeInstance.hasModifier instead.

Transient versus permanent is about keeping: both kinds sit in the same indices, both affect the value identically, both go on the wire, and only the permanent ones are packed, or carried across a return from the End. Mob-effect modifiers are added permanently, and that is the only reason an effect’s modifier survives a reload (status effects). On the client, meanwhile, every modifier is transient, because ClientPacketListener.handleUpdateAttributes sets the base value, wipes the whole modifier list and re-adds the incoming ones with AttributeInstance.addTransientModifier. A client attribute map is never persisted.

AttributeInstance.getValue recomputes through AttributeInstance.calculateValue only when the dirty flag is set, and the flag starts true, so the first read always computes — three passes and one clamp:

flowchart TD
    B["the base value: the prototype's, or whatever has set it since"]
    P1["pass 1: every ADD_VALUE modifier, added"]
    P2["pass 2: every ADD_MULTIPLIED_BASE modifier, off the same base"]
    P3["pass 3: every ADD_MULTIPLIED_TOTAL modifier, off the running total"]
    C["RangedAttribute.sanitizeValue, once"]
    O["AttributeInstance.cachedValue, to the next AttributeInstance.setDirty"]
    B --> P1 --> P2 --> P3 --> C --> O

The three passes, in the order they always run. What separates pass 2 from pass 3 is the two words on their boxes: the same base against the running total.

That difference is the whole of the arithmetic. Every AttributeModifier.Operation.ADD_MULTIPLIED_BASE modifier reads the base as pass 1 left it, so two of them do not compound — +50% and +50% is +100%. Every AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL modifier reads the previous one’s output, so two of those do — +50% and +50% is +125%. The clamp runs once at the end: RangedAttribute.sanitizeValue collapses a not-a-number result to the minimum and otherwise holds the value between the minimum and the maximum. Operation order is therefore global, not insertion order, and intermediate values are never clamped. Within a bucket, iteration order is a hash map’s — safe only because each bucket’s arithmetic is commutative.

…except in the other implementation, which is insertion-ordered

ItemAttributeModifiers.compute is a second, disagreeing implementation of the same idea. It walks an item’s ItemAttributeModifiers.Entry list in declaration order, applying each entry’s operation to the running total as it goes, with no three-pass grouping at all. It is not a duplicate of AttributeInstance.calculateValue and it does not agree with it. Its one caller in the whole game is Mob.getApproximateAttributeWith — the “would this weapon or armour be better than what I have?” estimate a mob makes when deciding whether to pick an item up.

Where the modifiers come from

Equipment is the busiest source. LivingEntity.detectEquipmentUpdates, in the server-only half of LivingEntity.tick, only dispatches; LivingEntity.collectEquipmentChanges does the work, adding each incoming stack’s modifiers as transient (removing by id first) through ItemStack.forEachModifier, which merges DataComponents.ATTRIBUTE_MODIFIERS with the enchantment modifiers from EnchantmentHelper.forEachModifier. Exactly eight vanilla enchantments carry EnchantmentEffectComponents.ATTRIBUTES — fire and blast protection, respiration, aqua affinity, depth strider, swift sneak, sweeping edge, efficiency — and EnchantmentAttributeEffect has a second, location-based path through EnchantmentAttributeEffect.onChangedBlock that exactly one uses: soul speed, registered under EnchantmentEffectComponents.LOCATION_CHANGED instead.

The rest, in one breath: MobEffect declarations, permanently and scaled by amplifier; item components, whose ItemAttributeModifiers.Display decides whether and how a tooltip line appears — its default form adds the reader’s own base value back in for Item.BASE_ATTACK_DAMAGE_ID and Item.BASE_ATTACK_SPEED_ID, so a weapon shows a total rather than a bonus; SetAttributesFunction in loot tables; Zombie.handleAttributes at spawn; LivingEntity.setSprinting; ServerPlayer.updatePlayerAttributes; and AttributeCommand, whose modifier add is permanent, and so saved.

One packet, one direction: ClientboundUpdateAttributesPacket, server to every tracking player and to the entity itself when it is a player — which is why your own client has a live attribute map at all. At most 128 attributes fit in one (checked on encode as well as decode), each snapshot carrying the attribute holder, the base value and the complete, uncapped modifier list. There is no serverbound attribute packet.

Strength II lands, and no attribute leaves the server

Put the four objects, the two sets and the two phases together and the whole system runs once, end to end, for a number the wire never hears about:

sequenceDiagram
    participant EffC as EffectCommands
    participant LE as LivingEntity
    participant ME as MobEffect
    participant AttrM as AttributeMap
    participant AttrI as AttributeInstance
    participant SE as ServerEntity

    EffC->>LE: addEffect, Strength at amplifier 1
    LE->>LE: onEffectAdded, guarded server-side
    LE->>ME: addAttributeModifiers, the map and the amplifier
    ME->>AttrM: getInstance, Attributes.ATTACK_DAMAGE
    AttrM->>AttrI: replaceFrom, inside AttributeSupplier.createInstance, if new
    AttrI->>AttrM: onAttributeModified, into the update set, if new
    ME->>AttrI: removeModifier, then addPermanentModifier at plus 6
    AttrI->>AttrM: onAttributeModified, into the update set
    Note over LE,SE: not syncable, so the sync set stays empty
    rect rgba(0, 0, 0, 0.04)
    Note over LE,SE: the next server tick: the ServerLevel.chunkSource phase, then the player's own tick
    SE->>SE: sendDirtyEntityData finds an empty attribute set
    LE->>LE: refreshDirtyAttributes drains the update set
    end
    Note over LE,AttrI: three seconds later, inside Player.attack
    LE->>AttrM: getValue, Attributes.ATTACK_DAMAGE
    AttrM->>AttrI: getValue, dirty, so calculateValue
    AttrI-->>AttrM: 1.0 base, plus the sword's, plus 6.0
    AttrM-->>LE: the same number

The lane to watch is ServerEntity: it is reached, finds the attribute sync set empty, and sends no attribute packet. The effect itself does go out, as a mob-effect packet when it is added and as the swirl in the entity data, but the attack damage never does.

MobEffects.STRENGTH is declared with one attribute modifier: +3 on Attributes.ATTACK_DAMAGE, AttributeModifier.Operation.ADD_VALUE, under the id effect.strength, and the amount is multiplied by amplifier + 1 when the modifier is built — so Strength II is +6. (MobEffects.WEAKNESS is the same construction at −4.) LivingEntity.addEffect puts the instance into the active list and calls LivingEntity.onEffectAdded, which is guarded server-side and hands the entity’s AttributeMap to MobEffect.addAttributeModifiers. If the effect was already running, the path is LivingEntity.onEffectUpdated instead, which removes and re-adds and, unlike the add path, refreshes the dirty attributes on the spot.

MobEffect.addAttributeModifiers calls AttributeMap.getInstance, and that is where the instance for Attributes.ATTACK_DAMAGE is born if nothing has asked for it yet (a player holding a sword already has one, made when the sword was equipped), copying the frozen prototype’s base value: 1.0 for a player, 3.0 for a zombie. Creating it dirties it, before any modifier exists. Then the effect removes its own id, which dirties nothing when the id is not there, and adds the +6 with AttributeInstance.addPermanentModifier — the remove is not optional, since the plain add throws on a duplicate id — which dirties it. Each time the callback adds to the update set and consults the syncable flag before touching the sync set, so the sync set stays empty and no attribute packet is sent at all. Next tick, ServerEntity.sendDirtyEntityData finds no attribute to send (it does send the swirl the effect put in the entity data), and LivingEntity.onAttributeUpdated matches none of its four branches.

Three seconds later Player.attack asks LivingEntity.getAttributeValue for the attack damage. The instance is dirty, so AttributeInstance.calculateValue runs: the addition bucket holds the sword’s base_attack_damage modifier from DataComponents.ATTRIBUTE_MODIFIERS and the effect’s +6, the two multiplication buckets are empty, the clamp passes, the result is cached. The rest of the swing — cooldown scale, enchantment bonuses, the crit — belongs to the sword swing. Expiry runs it backwards: LivingEntity.onEffectsRemoved calls MobEffect.removeAttributeModifiers, which removes by id from all three indices and dirties one last time.

Swap Strength for Speed and the missing limb appears

MobEffects.SPEED targets Attributes.MOVEMENT_SPEED with AttributeModifier.Operation.ADD_MULTIPLIED_TOTAL, and movement speed is syncable. Everything above is identical until the callback, which now fills the sync set too. ServerEntity.sendDirtyEntityData drains it in the next chunkSource phase and emits a ClientboundUpdateAttributesPacket carrying the base value and every modifier on that attribute — sprinting, powder snow, the effect, all of them — and ClientPacketListener.handleUpdateAttributes rebuilds the client’s instance from scratch. That difference, one boolean on the Attribute, is the whole design.

Where to look

Attributes is forty field initialisers and the syncable flag is visible in each of them, so start there; DefaultAttributes and AttributeSupplier.Builder.build are the prototypes and the freeze. Then read the three methods this page turns on, in this order: AttributeMap.getInstance, because asking is a mutation; AttributeMap.onAttributeModified, the four lines that decide which set a change lands in; and AttributeInstance.calculateValue, the three passes and the clamp. LivingEntity.refreshDirtyAttributes and ServerEntity.sendDirtyEntityData are where the two sets drain, in different phases. Two doors: ItemAttributeModifiers.compute, the second and disagreeing implementation of the arithmetic, and AttributeCommand, whose modifier add is the plainest way a player can make a permanent modifier by hand.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Movement and collision

Verified against Minecraft 26.3 · Part VI · One tick of a falling zombie: 0.08 of gravity, one swept box against a stone floor, and the four booleans everything downstream reads.

A zombie has been falling for a few ticks and this is the one where it reaches the stone. Its tick builds one delta vector, hands it to Entity.move, gets back the part of it the world allowed, and sets four booleans from the difference — Entity.horizontalCollision, Entity.verticalCollision, Entity.verticalCollisionBelow and Entity.minorHorizontalCollision. Then it has to answer a harder question: what did I just walk through? It does not answer that by sampling the destination. Every segment of the tick’s movement was recorded into a deque, Entity.movementThisTick, and Entity.applyEffectsFromBlocks replays those segments afterwards — in the same axis order the collision used, visiting every block the swept box actually crossed, AABB.collidedAlongVector rather than a static overlap. And the ignition, freezing and extinguishing that replay finds are not applied where they are found: they are queued into an InsideBlockEffectApplier.StepBasedCollector and flushed in InsideBlockEffectType declaration order, so fire and water touched in the same step of the replay always end in the extinguish, whatever order the blocks came in.

The cast

classwhat it decidesthread
Entitythe geometry: clipping, stepping up, bouncing, whether you are on the ground and what you are standing onthe Server thread, the Render thread for whatever is authoritative there, and both for an item, an orb, a falling block or lit TNT, or anything a piston or a shulker pushes
LivingEntitythe physics above it: gravity, drag, friction, swimming, gliding, climbingsame
CollisionGetterwhich blocks are candidates, through BlockCollisions, and which one is holding you upsame
Shapes / VoxelShapethe clipping arithmetic, one axis at a timesame
Entity.Movementone recorded segment — from, to, and, for a segment a move recorded, the pre-collision vector that fixes the replay’s axis ordersame
InsideBlockEffectApplier.StepBasedCollectorwhen an ignition, a freeze or an extinguish happens, and in what ordersame
EntityFluidInteractionthe snapshot of water and lava height, whether the eyes are in and the current, that the tick readssame
ServerEntitywhether this tick’s new position costs a short delta or an absolute syncthe Server thread, in the broadcast phase

Who is allowed to run this at all

The trace below is the authoritative copy’s tick, and which copy that is inverts between a mob and a player: nothing in a client-side mob’s own tick moves it through Entity.move (a dying ender dragon aside), while your own player is simulated on your machine for real. The predicate that decides is Entity.isLocalInstanceAuthoritative rather than a bare “am I the client”, and it is stated in full once, at authority; this page notes each gate where the trace hits it.

The tick

One tick of a falling zombie, from the entity loop to the packet that carries the result. The mob is one object and one lane; the class on each message is which half of it runs, because the page’s two roles — LivingEntity’s physics and Entity’s geometry — alternate all the way down:

sequenceDiagram
    participant SL as ServerLevel
    participant LE as LivingEntity
    participant SE as ServerEntity

    SL->>LE: tick, from ServerLevel's entity loop
    LE->>LE: Entity.baseTick, and the fluid snapshot
    LE->>LE: LivingEntity.aiStep: coast if nothing simulates, then serverAiStep
    LE->>LE: LivingEntity.travel, which picks travelInAir
    LE->>LE: Entity.moveRelative, then Entity.move
    Note over LE: the resolve, and the step-up loop, are the next section
    LE->>LE: Entity.setPos, then the four collision booleans
    LE->>LE: Entity.setOnGround<br/>WithMovement
    LE->>LE: Entity.checkFallDamage
    LE->>LE: the bounce, the step sound, the block speed factor
    LE->>LE: back in travelInAir: 0.08 of gravity, then the drags
    LE->>LE: Entity.applyEffects<br/>FromBlocks
    LE->>LE: LivingEntity.push<br/>Entities, then doPush
    rect rgba(0, 0, 0, 0.04)
    Note over SL,SE: the next tick, the ServerLevel.chunkSource phase
    SL->>SE: sendChanges: a short Pos delta, or an absolute sync
    end

The line to look for is the fourth from the bottom. Gravity is subtracted after the move, not before it, so the delta a tick moves the mob by was built by the tick before — and the shaded band at the foot is the next tick again, which is when the wire hears about any of it.

Building the delta

Entity.baseTick clears the cached block state, records whether the eyes were in water, and calls Entity.updateFluidInteraction — one sweep that fills the water and lava trackers of EntityFluidInteraction with their heights and accumulated current. Everything downstream — Entity.isInWater, Entity.isInLava, Entity.getFluidHeight, Entity.isEyeInFluid — reads that snapshot and never the live world. Fire ticks after it, lava halves Entity.fallDistance rather than clearing it, and Entity.checkBelowWorld discards anything 64 below the world floor — except a LivingEntity, which overrides the hook and takes four points of fell out of the world damage a tick instead.

LivingEntity.aiStep is the order of every mob’s tick and worth memorising: the coast when nothing interpolates or simulates, head turn, equipment, a deadzone that zeroes any delta component under 0.003 (a squared-horizontal test instead, for players), LivingEntity.applyInput, Mob.serverAiStep — the goal selector and the movement control, which set LivingEntity.xxa and LivingEntity.zza (AI, pathfinding) — the jump branch, gliding, the travel branch, Entity.applyEffectsFromBlocks, animation, freezing, LivingEntity.pushEntities. Our zombie’s jump branch is skipped before Entity.onGround is ever consulted, because the branch is gated on LivingEntity.jumping and a falling zombie is not asking to jump.

The travel branch is a fork, not a call. If the controlling passenger is a Player and the mob is alive it is LivingEntity.travelRidden — the path every horse, pig and happy ghast takes, and the reason a ridden mob’s input comes from LivingEntity.getRiddenInput rather than its own AI. Otherwise, and only if both Entity.canSimulateMovement and Entity.isEffectiveAi hold, it is LivingEntity.travel, which picks one of three: LivingEntity.travelInFluid (splitting again into LivingEntity.travelInWater and LivingEntity.travelInLava), LivingEntity.travelFallFlying — the elytra model, lift from the square of the pitch cosine — or LivingEntity.travelInAir. LivingEntity.shouldTravelInFluid picks the first, and note what it reads: the cached in-water and in-lava flags, with the live FluidState at the block position used only for LivingEntity.canStandOnFluid, which is how a strider walks on lava.

The knobs on the entity and the knobs on the block

LivingEntity.travelInAir probes the block below through Entity.getBlockPosBelowThatAffectsMyMovement — 0.500001 down — for its friction: airborne, 1.0, and on stone Block.getFriction’s 0.6 through LivingEntity.computeModifiedFriction. For an airborne entity LivingEntity.getFrictionInfluencedSpeed then returns LivingEntity.getFlyingSpeed: 0.02 for a mob nobody is riding, which is why you have almost no air control — a literal, not Attributes.FLYING_SPEED, which is not even in the base living attribute set. Entity.moveRelative rotates the input by the yaw and adds it to Entity.deltaMovement through Entity.setDeltaMovement, which silently discards the whole vector if it is not finite, so NaN never enters the physics state.

Every knob on the entity’s side is a syncable attribute (attributes): Attributes.GRAVITY (0.08), Attributes.STEP_HEIGHT (0.6), Attributes.MOVEMENT_SPEED (0.7), Attributes.JUMP_STRENGTH (0.42), Attributes.SAFE_FALL_DISTANCE (3.0), Attributes.FALL_DAMAGE_MULTIPLIER, Attributes.MOVEMENT_EFFICIENCY, Attributes.WATER_MOVEMENT_EFFICIENCY, Attributes.AIR_DRAG_MODIFIER, Attributes.FRICTION_MODIFIER, Attributes.BOUNCINESS. The world’s half is five values each block copies out of its BlockBehaviour.Properties (blocks and states) and returns from a getter:

propertydefaultwho changes it
Block.getFriction0.60.98 on ice, packed ice and Blocks.FROSTED_ICE, 0.989 on blue ice, 0.8 on Blocks.SLIME_BLOCK
Block.getSpeedFactor1.00.4 on soul sand and honey
Block.getJumpFactor1.00.5 on honey
Block.getBounceRestitution0.01.0 on Blocks.SLIME_BLOCK, 0.75 on beds and Blocks.SHELF_MUSHROOM
Block.getFallDistanceReduction0.00.5 on beds and Blocks.SHELF_MUSHROOM

Who else can move you

MoverType names who is moving you, in five constants. MoverType.PISTON is the one with collision rules of its own — Entity.limitPistonMovement collapses the vector to a single axis, Entity.applyPistonMovementRestriction clamps it to ±0.51 per game tick, and that path alone is exempt from the multiply by Entity.stuckSpeedMultiplier — it still clears the field (pistons). MoverType.SHULKER_BOX makes a Shulker teleport rather than move, and MoverType.SELF and MoverType.PLAYER are read together by Player.maybeBackOffFromEdge, which is what stops a crouching player walking off a ledge.

Resolving one axis at a time

Entity.move opens with two things that are easy to miss. Entity.stuckSpeedMultiplier is applied to the delta and cleared in the same breath, zeroing Entity.deltaMovement with it — that pair is the whole cobweb, berry-bush and powder-snow model — and Entity.noPhysics is an escape hatch above even it: an entity with it set skips collision entirely and has all four booleans cleared.

flowchart TD
    COLLIDE["Entity.collide"]
    GATHER["gather the colliders over the swept box"]
    RESOLVE["Entity.collideWithShapes"]
    AXIS["Direction.axisStepOrder: Y, then the larger horizontal, then the smaller"]
    TEST{"step height above zero, colliding horizontally, on the ground?"}
    FLAT["return the flat result"]
    HEIGHTS["Entity.collectCandidateStepUpHeights, ascending, less the one already tried"]
    NEXT{"a candidate height left?"}
    RETRY["retry the whole resolve at that height"]
    MORE{"more horizontal distance than the flat attempt?"}
    WIN["return that one, with the tick's fall added back"]

    COLLIDE --> GATHER --> RESOLVE --> AXIS --> TEST
    TEST -- "no" --> FLAT
    TEST -- "yes" --> HEIGHTS --> NEXT
    NEXT -- "no" --> FLAT
    NEXT -- "yes" --> RETRY --> MORE
    MORE -- "no" --> NEXT
    MORE -- "yes" --> WIN

Three diamonds: the first decides whether a step-up is attempted at all, the second walks the candidate heights in ascending order, and the third is asked once per height. The loop leaves with the flat result when the heights run out, and with the stepped one at the first height that gains ground.

Two things in the gathering stage surprise people. The first is that collision is against shapes, not blocks: a candidate contributes whatever BlockBehaviour.BlockStateBase.getCollisionShape says, which for a fence is 1.5 blocks tall — to walk into and to stand on. The 1.0 you see outlined is BlockBehaviour.BlockStateBase.getShape, the selection box, and CrossCollisionBlock builds the two from different heights. The mover only ever asks for the first. The second is that most entities are not colliders at all: EntityGetter.getEntityCollisions wraps with Shapes.create the box of every entity that answers Entity.canBeCollidedWith, and the base class answers false — so the mob standing next to you contributes nothing, while boats, living shulkers and happy ghasts do. (Pushing is a different predicate, Entity.isPushable, and belongs to the crowding pass below.) BlockCollisions walks the box with a Cursor3D, reads chunks through CollisionGetter.getChunkForCollisions — a full chunk if it is already there, null otherwise, and a missing chunk is simply stepped past, so an entity at the edge of loaded space falls through empty space rather than blocking the tick. A full cube short-circuits to a box intersection; anything else goes through Shapes.joinIsNotEmpty.

Entity.collideBoundingBox is the flat attempt itself — the public static that gathers the colliders and hands them to Entity.collideWithShapes, with no step-up in it at all — and Entity.collide calls it once before deciding whether stepping up is worth trying. The step-up loop in the figure is the part worth slowing down for. It does not guess a height and it does not pick the best one. It harvests the Y coordinates of the candidate shapes that lie above the entity’s feet and within Entity.maxUpStep, skipping the height the flat attempt already tried, sorts them ascending, and retries the whole resolve at each until one yields any more horizontal distance than the flat attempt — and returns that one with this tick’s fall added back in, because the box it stepped from was the one already lowered by this tick’s vertical movement. It is the lowest step that helps, which is also why an entity can step onto a shape’s internal ledge and not only its top face. Entity.maxUpStep is zero on the base class and LivingEntity.maxUpStep reads Attributes.STEP_HEIGHT, raised to at least 1.0 when a Player is riding — which is how a ridden horse climbs a full block.

What the move reports back

Before committing, one clip: if Entity.fallDistance is non-zero and the allowed movement is at least a block long, Entity.move casts a ray up to eight blocks along it for BlockTags.FALL_DAMAGE_RESETTING or water and resets the fall distance on any hit. Then an Entity.Movement record — from, to, and the pre-collision delta — goes onto Entity.movementThisTick, and Entity.setPos moves the point and the bounding box together.

The four booleans are then computed by comparing what was asked with what was allowed. Entity.horizontalCollision is true when either horizontal component was clipped; Entity.verticalCollision when the Y component was, and Entity.verticalCollisionBelow when that clip was downward. The fourth, Entity.minorHorizontalCollision, is the odd one: it asks Entity.isHorizontalCollisionMinor, which is false on the base class and overridden by exactly one class in the game, LocalPlayer, which is also its only reader — a scrape too small to be a wall is the difference between stopping a sprint and not. The comparison is Mth.equal on the two horizontals but exact inequality on Y, and the whole vertical block only runs if the entity moved vertically at all or is authoritative. Entity.onGround is not a fifth boolean of the same kind: it is set from Entity.verticalCollisionBelow, so it too is a comparison and not a raycast. The probe for what holds you up is CollisionGetter.findSupportingBlock, reached through Entity.setOnGroundWithMovement and Entity.checkSupportingBlock, and it answers which block is holding you (for sounds and the speed factor), not whether — probing a paper-thin slab under the box, retrying with the box shifted back along the movement if that finds nothing, and setting Entity.onGroundNoBlocks when it still does.

Landing, and the fall distance that resets from many places

Entity.checkFallDamage runs next, only when this instance is authoritative. It adds the downward movement to Entity.fallDistance and, on landing, calls Block.fallOn, posts GameEvent.HIT_GROUND and resets the distance. Block.fallOn is what calls LivingEntity.causeFallDamage, with the distance scaled down by the block’s Block.getFallDistanceReduction; LivingEntity.calculateFallPower subtracts Attributes.SAFE_FALL_DISTANCE and LivingEntity.calculateFallDamage multiplies by Attributes.FALL_DAMAGE_MULTIPLIER and checks EntityTypeTags.FALL_DAMAGE_IMMUNE (damage). Our two-block fall is 2 − 3 < 0, so the power is zero and nothing at all happens: the landing particles are gated on a positive power and the fall sound on positive damage, so only the game event and the reset fire. That reset is reached from more places than you would guess — landing, entering water in Entity.updateFluidInteraction, climbing in LivingEntity.handleOnClimbable, every LivingEntity.rideTick, under MobEffects.SLOW_FALLING or MobEffects.LEVITATION at the top of the travel branch, Entity.makeStuckInBlock, a bubble column, a honey block’s slide, a mace’s smash, and the tag-or-water clip above. Lava halves it instead.

Then Entity.restituteMovementAfterCollisions, gated on Entity.canSimulateMovement: a real restitution model, not slime-block code. It reflects the horizontal components, and for a downward hit combines Attributes.BOUNCINESS with Block.getBounceRestitution, damped to 80% for non-living entities, gated on the impact being at least one tick of gravity — which is why nothing jitters at rest on a slime block — and opted out of by BlockTags.SUPPRESSES_BOUNCE or by crouching. A bounce posts GameEvent.BOUNCE (game events) and sets Entity.syncPosition. Entity.applyMovementEmissionAndPlaySound follows, gated on not client-side or authoritative: it accumulates Entity.moveDist and fires Entity.playStepSound plus GameEvent.STEP when it passes Entity.nextStep. Last, the horizontal components are multiplied by Entity.getBlockSpeedFactor — soul sand’s 0.4, lerped towards 1 by Attributes.MOVEMENT_EFFICIENCY.

And then gravity

Control returns to LivingEntity.travelInAir, after the move, and only now is gravity subtracted: Entity.getEffectiveGravity, 0.08, or capped at 0.01 while falling with MobEffects.SLOW_FALLING. MobEffects.LEVITATION replaces that step entirely rather than modifying it, and a client-side entity standing over an unloaded chunk gets a hard-coded −0.1. The horizontals are then multiplied by block friction times a 0.91 scaled by Attributes.AIR_DRAG_MODIFIER, the vertical by a 0.98 scaled by the same — Attributes.FRICTION_MODIFIER touches only the block-friction term, and block friction is 1.0 unless Entity.onGround. The whole drag step is skipped when LivingEntity.shouldDiscardFriction is set. Climbing straddles the move: LivingEntity.handleOnClimbable clamps the fall speed on a climbable block before it, and a separate clamp in LivingEntity.handleRelativeFrictionAndCalculateMovement after it sets the vertical component to 0.2 when a climbing or powder-snow entity is either colliding horizontally or jumping — which is the whole of “you go up a ladder by pressing into it”.

So the delta Entity.move consumes carries the previous tick’s gravity. That is one of two conventions in the codebase, and the other is Entity.applyGravity, which most of its callers run before the move. An ItemEntity does it the other way, and the contrast is the clearest way to see both. ItemEntity.tick applies gravity (a default of 0.04) before Entity.move and drag after it, reverses any downward velocity on landing at half strength — items bounce, they do not merely damp — and skips the move entirely when it is resting still on the ground and the tick count says it is not this item’s turn, calling Entity.applyEffectsFromBlocksForLastMovements on the previous tick’s segments instead. Its Entity.getMovementEmission is Entity.MovementEmission.NONE, so it makes no step sounds. Neither convention is wrong.

The fluid snapshot is not taken only once a tick, and usefully so: LivingEntity.checkFallDamage re-runs Entity.updateFluidInteraction from inside Entity.move whenever the entity is not already in water (and ItemEntity.tick, PrimedTnt.tick and AbstractMinecart.tick re-run it too), which is exactly why falling into water cancels the fall damage in the same tick that entered it.

What did I pass through

Entity.applyEffectsFromBlocks runs on the same gate as the step sound — not client-side, or authoritative. It drains Entity.movementThisTick into Entity.finalMovementsThisTick first — substituting a single old-position-to- position segment when the deque is empty, and appending a final segment when the entity ended somewhere the last recorded one did not — and only then runs the replay, which opens by calling Block.stepOn for the block underfoot, gated on Entity.onGround.

Each recorded segment is replayed in the same axis order the collision used — Direction.axisStepOrder again, over the segment’s stored pre-collision vector, while a substituted, appended or merged one is swept straight — and Entity.checkInsideBlocks walks each leg with BlockGetter.forEachBlockIntersectedBetween, testing each block with AABB.collidedAlongVector (through Entity.collidedWithShapeMovingFrom) rather than a static overlap at the destination, and calling BlockBehaviour.BlockStateBase.entityInside, Entity.onInsideBlock and FluidState.entityInside on what it finds. Entity.visitedBlocks is the deduplicator: a block is visited at most once across the whole replay, however many segments cross it. Two budgets bound the work — sixteen sweep steps per segment, which is not sixteen blocks, because every block the box covers at one end of the sweep shares a single step index; and Entity.movementThisTick merges its two oldest entries once it reaches a hundred, buying bounded memory with a little precision. A segment that exhausts its steps gets one last zero-length visit at the destination, which covers every block the box ends up inside.

Ignition, freezing and extinguishing are not applied inline, though most of what a block does is (a cobweb’s slowdown, a cactus’s or a campfire’s damage). Each of those is queued into the InsideBlockEffectApplier.StepBasedCollector, which flushes a step’s worth at a time in InsideBlockEffectType declaration order — InsideBlockEffectType.FREEZE, InsideBlockEffectType.CLEAR_FREEZE, InsideBlockEffectType.FIRE_IGNITE, InsideBlockEffectType.LAVA_IGNITE, InsideBlockEffectType.EXTINGUISH — so fire and water touched in the same step always end in the extinguish. The reordering is strictly per step: InsideBlockEffectApplier.StepBasedCollector.advanceStep flushes as the replay advances and InsideBlockEffectApplier.StepBasedCollector.applyAndClear runs the accumulated list at the end, so across steps the order stays chronological and fire in a later step than water still burns you.

The crowding pass closes the tick

LivingEntity.pushEntities closes the tick. It collects pushable neighbours through Level.getPushableEntities — a different predicate from the collision one, and on the client ClientLevel.getPushableEntities returns at most the local player, never the crowd. On a server it applies GameRules.MAX_ENTITY_CRAMMING (default 24, checked on a one-in-four roll each tick, the damage 6) and then calls LivingEntity.doPush → Entity.push, a horizontal-only impulse scaled by 0.05 and ignored below a hundredth of a block.

And the tick after: what this one costs on the wire

Nothing has crossed the network yet. ServerEntity.sendChanges runs in the chunk-source phase of ServerLevel.tick, which comes before the entity loop (the level tick) — so this tick’s movement is broadcast at the start of the next one. It becomes a short delta, ClientboundMoveEntityPacket.Pos, only when it can: not too big for a short, no more than 400 gated evaluations since the last absolute sync — ServerEntity.teleportDelay is incremented inside the gate, so for an entity tracked every tick on the default interval that is about 1,200 ticks, fewer when its data or a push opens the gate, and more when the tracker is not called at all — not riding, the entity does not demand precision, and Entity.onGround still matches what the last absolute sync recorded. The last condition is the one that fails most often, and it is a real cost: a landing or a step off a ledge that a gated call sees forces a full ClientboundEntityPositionSyncPacket. The bounce’s Entity.syncPosition flag re-phases the tracker’s own counter to the next interval boundary for an entity with no stepped interpolation, so its send happens at the next evaluation rather than up to an interval late; for a living entity like this zombie (every one but a shulker), its SteppedInterpolationTracker takes the flag first and records the bounce as a step instead. ClientboundSetEntityMotionPacket carries the delta separately (what the client is told owns the choice between the two shapes). On the receiving side, ClientPacketListener.handleEntityPositionSync moves only an entity that is not locally authoritative (authority), and snaps rather than interpolates past 64 blocks of correction — otherwise it feeds the entity’s InterpolationHandler, for a living entity other than a shulker a stepped one whose step count is its update interval, three by default, which Entity.commonTick steps just before the entity’s tick; the coast branch LivingEntity.aiStep opens with is what runs when nothing is interpolating.

Where to look

Two methods hold almost all of this and are worth reading end to end in this order: LivingEntity.aiStep, which is every mob’s tick in one list, and Entity.move, which is the geometry. Under the second, Entity.collide and Entity.collideWithShapes are the axis-at-a-time resolve, with the step-up loop in Entity.collide and Entity.collectCandidateStepUpHeights harvesting its heights; BlockCollisions is what supplies them with candidates, and CollisionGetter.findSupportingBlock is the probe for what holds you up. For the replay, read Entity.applyEffectsFromBlocks and then Entity.checkInsideBlocks, with InsideBlockEffectType open beside them for the order the collector flushes in. LivingEntity.travelInAir is where gravity and drag are spent, after the move rather than before it. Two doors: ItemEntity.tick, the other convention run end to end in one method, and InterpolationHandler, which is what moves an entity when nothing on that side simulates it.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

AI: goals and brains

Verified against Minecraft 26.3 · Part VI · A villager’s day — wake, claim a job site, work until the schedule turns, walk home to bed — and the same tick under a zombie that has none of it.

It is dawn in a village. One villager climbs out of bed, walks to its composter and works there until the afternoon. Ten blocks away a zombie catches fire, sees the villager, and comes for it. Both are Mobs, both are driven by the same Mob.serverAiStep on the Server thread, and neither is running a script: each is being re-asked, every tick or every other tick, what it would like to be doing now. They are asked in two completely different ways, and the villager’s is the surprising one. Its day looks like a timetable held on the villager, and it is not one. A Brain holds an EnvironmentAttribute of Activity — a pointer into the world rather than a table on the mob — and Brain.updateActivityFromSchedule asks the EnvironmentAttributeSystem what that attribute’s value is at this position, at this time. The villager goes to bed because it asked the world what hour it is where it is standing. The answer comes out of a data-pack Timeline, Timelines.VILLAGER_SCHEDULE: a 24000-tick loop whose adult track names the tick each activity starts — idle at 10, work at 2000, meet at 9000, idle again at 11000, rest at 12000 — beside a baby track that swaps play in. Because the lookup takes a position, the day can in principle differ by location. That system is environment attributes and timelines; this page only asks it a question.

The cast

classwhat it decidesthread
GoalSelectorwhich goals run, by holding a four-entry table of Goal.Flag to the goal that owns itserver, from Mob.serverAiStep
Goalwhether it wants to run, whether it may be interrupted, and which flags it needsas above
WrappedGoalthe arbitration — WrappedGoal.canBeReplacedBy — plus the priority and the running bitas above
Brainwhich activity is active, what the memories hold, and which behaviours may start at allserver, from Mob.customServerAiStep
MemoryModuleTypethe vocabulary a brain thinks in: 115 constants, of which the 52 with a codec are the mob’s entire saved minddeclared, never ticked
Sensorwhen to look at the world, on its own scan rate, and which memories to writeserver, from Brain.tick
ActivityDataone activity’s prioritised behaviour list, its memory requirements, and the memories erased when it stopsbuilt per body by Brain.ActivitySupplier
Sensingwhether this mob can see that entity, memoised for exactly one tick — and both systems go through itserver, cleared at the top of Mob.serverAiStep

Seven things they do differently

the goal selectorthe brain
what holds the stateGoalSelector.availableGoals, an insertion-ordered set of WrappedGoal, beside a lock table and a set of disabled flagsa memory map, a sensor map, and Brain.availableBehaviorsByPriority — priority to activity to behaviour set
what fills itMob.registerGoals, once, from the constructor, and only when the level is a ServerLevelBrain.Provider.makeBrain, from an activity list built per body — and, for a villager, built again when its body changes
what decidesGoal.canUse, re-asked on every other tickBehavior.hasRequiredMemories then Behavior.checkExtraStartConditions, asked once a tick
what arbitratesthe flag table. Lower priority number wins a contested flag, and a non-interruptable incumbent wins outrightthe active activity. A behaviour whose activity is not active is not started at all
what persists across a savenothing. Not the running set, not the flags52 of the 115 memories, through Brain.Packed
what the world can push inMob.updateControlFlags every five ticks, and the leash, both on one selector onlythe schedule attribute, POI claims, hostiles seen by sensors, Attributes.FOLLOW_RANGE
which mobs use itevery Mob ticks one, and almost every mob without a brain fills it. 58 goal classes and 10 targeting ones20 classes override LivingEntity.makeBrain — but only Villager sets a schedule

Where both of them sit in one mob tick

The profiler section names, because they are what a profile shows:

LivingEntity.tick
  LivingEntity.aiStep
    "ai"               the guard: server side, and Mob.isEffectiveAi
      "newAi"          Mob.serverAiStep
        "sensing"        Sensing.tick — the line-of-sight memo is cleared
        "targetSelector" ┐ GoalSelector.tick on the full pass,
        "goalSelector"   ┘ tickRunningGoals(false) on the off tick
        "navigation"     PathNavigation.tick — see pathfinding
        "mob tick"       Mob.customServerAiStep → "villagerBrain" → Brain.tick
        "controls"       "move" / "look" / "jump"
    "jump"             LivingEntity's own jump handling — outside the guard
    "travel"           LivingEntity.travel — where the body actually moves
  "headTurn"           Mob.tickHeadTurn — no side check, so both sides

Note the scope of that guard. LivingEntity.aiStep runs on the client too; what it wraps in server side and effective AI is the one call to Mob.serverAiStep, not the jump and travel sections beneath it. Mob.isEffectiveAi is the more interesting half of the condition, and what it is — one of five predicates that decide which copy of an entity may do anything — belongs to authority. What belongs here is the guard’s scope. On the client neither selector nor brain is ticked at all — only the jump, travel and head-turn sections beneath the gate, and the debug renderers.

Each of the seven rows of that table is a section below, in the table’s own order.

What holds the state

GoalSelector holds three things and none of them is a plan: a set of WrappedGoal, a map from Goal.Flag to the goal currently holding it, and a set of flags that have been switched off. There is no state machine and no sequence. The only persistent state a goal system has is which goals are running and which flags are held, and a Mob keeps two independent copies of it, Mob.goalSelector and Mob.targetSelector.

A Brain holds a great deal more, and the piece to keep hold of is Brain.availableBehaviorsByPriority: a sorted map from priority, to activity, to a set of BehaviorControl. Priority is the outer key, so the whole brain is walked in priority order regardless of which activity a behaviour belongs to. Beside it sit the memory map, the sensor map, the per-activity requirements, the per-activity erase lists, the core activities, the active set and a default of Activity.IDLE.

Two things about ownership surprise people. Brain is declared on LivingEntity, not on Mob — every living entity has one, the player included, and the base implementation hands back an empty one that reports itself Brain.isBrainDead. And the activity list is not static: a Brain.Provider is what builds a brain, and it asks its Brain.ActivitySupplier for that list per body, which is how a villager’s profession selects its work package.

What fills it

Goals go in once, for almost every mob. The Mob constructor calls Mob.registerGoals only when the level it is being built into is a ServerLevel, so a client-side mob’s selectors stay all but empty for its whole life (a few mobs add a goal from the constructor or from loaded data, which nothing on the client ever ticks). After that the set is fixed, but for the few mobs that add or remove a goal on a state change.

Memories are filled continuously, by sensors and by behaviours alike. A Sensor looks at the world and writes what it saw: the default scan rate is 20 ticks (Sensor.DEFAULT_SCAN_RATE), GolemSensor uses 200 and SecondaryPoiSensor 40, and Sensor.randomlyDelayStart offsets each one at construction so a village does not scan in lockstep. What registers a memory slot is declaring it — every memory a sensor lists in Sensor.requires and every memory a behaviour names in its entry condition is registered when the brain is built. Reading one that was never registered throws Unregistered memory fetched: a behaviour has been installed on a mob that has no idea what it is talking about.

The two systems share one piece of machinery here, and it is easy to file under the wrong heading. Sensing is the per-mob line-of-sight memo, cleared once per tick at the top of Mob.serverAiStep, and it is not the goal system’s alone: TargetingConditions.test routes every line-of-sight check through Mob.getSensing, and the shared conditions the brain’s sensors use have that check on by default.

Those shared conditions deserve a second look. Sensor holds six static TargetingConditions objects, used by every brain mob in the world, and re-ranges all six from this body’s Attributes.FOLLOW_RANGE immediately before every scan. That is correct only because AI is strictly single-threaded — there is not a future, an executor or a thread anywhere in the AI packages — and it is about as clear a demonstration of the fact as the codebase offers.

What decides

A goal is asked Goal.canUse every other tick, staggered across mobs by tickCount + id, with an exception for a mob’s first tick, where the full pass runs whatever the parity. On the off tick GoalSelector.tickRunningGoals is called with false, so only goals that answer Goal.requiresUpdateEveryTick are ticked at all — and, more importantly, no goal is stopped, because the Goal.canContinueToUse sweep lives in the full pass. A goal that lost its reason to run on an off tick keeps running until the next even one.

The full pass, GoalSelector.tick, is three phases, and they are the next level down in a profile. goalCleanup stops every running goal that either holds a now-disabled flag or fails Goal.canContinueToUse, then drops every lock whose holder is no longer running. goalUpdate walks the set again and starts anything that is not running, holds no disabled flag, can take all its flags, and answers Goal.canUse. goalTick — reached through GoalSelector.tickRunningGoals with true — ticks the survivors.

A behaviour is asked once per tick, and the first question is not about the behaviour at all. Brain.tick runs four fixed phases in order: Brain.forgetOutdatedMemories, where every MemorySlot counts down and an expired one clears itself; Brain.tickSensors, where each Sensor counts its own scan rate down before it looks at anything; and then the two that walk the behaviours, Brain.startEachNonRunningBehavior over Brain.availableBehaviorsByPriority lowest number first, and Brain.tickEachRunningBehavior over everything now RUNNING. Both of those are complete sweeps, so what happens to one behaviour is two questions asked in two different phases of the same tick:

flowchart TD
    A["one behaviour, in one Brain.tick"]
    E{"is its activity active?"}
    F["never started"]
    G["Behavior.tryStart — its memories, then its extra conditions"]
    X["stays STOPPED until a later tick"]
    H["RUNNING, to a rolled end timestamp"]
    S["phase three ends, phase four begins"]
    J{"timed out, or Behavior.canStillUse false?"}
    K["Behavior.doStop, in the Brain.tick that started it"]
    L["Behavior.tick"]
    A -- "STOPPED" --> E
    A -- "already RUNNING" --> S
    E -- "no" --> F
    E -- "yes, and it is STOPPED" --> G
    G -- "either test fails" --> X
    G -- "both pass" --> H
    H --> S
    S --> J
    J -- "yes" --> K
    J -- "no" --> L

One behaviour through one Brain.tick. The two diamonds are the whole of this page: the first is asked in phase three and the second in phase four, and a one-shot, or a behaviour that keeps the default Behavior.canStillUse, ends at the second in the very tick it started.

The branch marked never started is what this page turns on. An activity is a filter, not a mode, and it filters starting: a behaviour already running when its activity leaves is still ticked, which is why SleepInBed checks the rest activity itself. The brain’s active set is always the core activities plus exactly one other, so Activity.CORE behaviours run at every hour of the day and switching activity only swaps the second half. (Core activities is plural in the API and singular in practice: nothing in 26.3 calls Brain.setCoreActivities with anything but Activity.CORE alone.)

The canStillUse branch is sharper than “a behaviour runs for one tick”. Behavior.canStillUse defaults to false, and phases 3 and 4 are both inside the same Brain.tick — so for a Behavior subclass that does not override it, Behavior.tick is not called once. Everything it does, it does in Behavior.start (a one-shot does its work in its trigger). The duration rolled at start between the behaviour’s minimum and maximum (Behavior.DEFAULT_DURATION is 60) matters only for the ones that do override it, which is why the same behaviour class configured with different bounds behaves differently in two packages.

What arbitrates

On the goal side, the flag table is the arbiter, not the priority list. GoalSelector.availableGoals is insertion-ordered and never sorted; priority only settles a contested flag. Two goals with no flag in common run together whatever their numbers, and two that share one never do. WrappedGoal.canBeReplacedBy is the whole rule: the incumbent must answer Goal.isInterruptable, and the challenger’s number must be strictly lower.

There is a small piece of craft in how that is arranged. GoalSelector never puts a placeholder in its lock table; it reads the table with a default — a sentinel WrappedGoal of maximum priority that reports itself not running — so this flag is free and this flag is held by someone worse than me are the same WrappedGoal.canBeReplacedBy call on an entry that may not exist.

On the brain side the arbiter is the active set, and the fallback is silent. Brain.setActiveActivityIfPossible checks the target activity’s memory requirements and, if they do not hold, calls Brain.useDefaultActivity instead. A jobless villager at tick 2000 is not “off schedule”: the switch to Activity.WORK fails its MemoryModuleType.JOB_SITE requirement and the villager is idle by construction. Switching is also not free — the brain first erases, for every activity leaving the set, the memories that activity’s ActivityData names as memoriesToEraseWhenStopped. It is one of the few places the brain mutates state rather than reading it.

What persists across a save

A goal system saves nothing. Which goals were running, which flags were held, how far through an attack a mob was — all of it is rebuilt from scratch when the chunk reloads and the constructor calls Mob.registerGoals again.

A brain saves Brain.Packed, and Brain.pack walks the memories keeping only those whose MemoryModuleType can serialise: 52 of 115, the remaining 63 transient by construction. Time-to-live travels with them, so a memory can expire across a reload as easily as within a tick — MemorySlot counts down in phase 1 of every Brain.tick and clears itself at zero.

The reason Brain.Packed exists as a first-class shape is that a brain is built more than once in a mob’s life. Villager.refreshBrain stops every running behaviour, packs the current memories, and runs the provider again from the packed state with a fresh activity list. Changing profession does that; so does growing up, which is how a baby swaps EnvironmentAttributes.BABY_VILLAGER_ACTIVITY for EnvironmentAttributes.VILLAGER_ACTIVITY.

What the world can push in

Into a goal selector, two things, and both of them work by switching a flag off rather than by asking for anything. The first is Mob.updateControlFlags, called from Mob.tick on the server every five ticks. It sets Goal.Flag.MOVE and Goal.Flag.LOOK from one question — is a Mob steering me — and Goal.Flag.JUMP from that and am I in an AbstractBoat. So a mob a mob is riding loses all three, and a mob sitting in a boat by itself loses only the jump.

The second is the lead in your hand. Leashable.tickLeash runs from Entity.baseTick (entity anatomy puts it among the capability interfaces), and when the mob is pulled past the distance a lead can hold, Mob.leashTooFarBehaviour breaks the lead and disables Goal.Flag.MOVE outright — so a mob that has just snapped its lead is not choosing to stand still, it has had the flag its movement goals need taken away — until Mob.updateControlFlags puts it back within five ticks. Both levers touch Mob.goalSelector only; Mob.targetSelector is never disabled. GoalSelector.tick then stops any running goal holding a disabled flag and refuses to start another.

Into a brain, rather more: the schedule attribute, whose value comes from the world; hostiles, players, items, beds and golems, all written by sensors that query the level; and POI claims, which go through the shared PoiManager (points of interest). And one attribute change reaches into the machinery itself: a new Attributes.FOLLOW_RANGE re-ranges the shared targeting conditions above and, through Mob.onAttributeUpdated, resizes how far the pathfinder may search (pathfinding).

Neither of them crosses the network

There is no AI packet. What a client sees are consequences — head rotations, motion, position deltas, pose changes, the occasional entity-event byte — plus a debug channel that costs nothing until someone subscribes: Mob.registerDebugValues registers DebugSubscriptions.ENTITY_PATHS and DebugSubscriptions.GOAL_SELECTORS for every mob, and DebugSubscriptions.BRAINS only for one that is not brain-dead.

And neither library is anything but more of the same

There are 103 classes at the top of world/entity/ai/behavior and 61 at the top of world/entity/ai/goal, and nearly all of them are one of the shapes above — a behaviour with its required memories and its start conditions, whether a Behavior subclass or a one-shot built with BehaviorBuilder, or a Goal with its flags and its Goal.canUse — beside a handful of the machinery that runs them and the value types they use. Reading a third is reading the first twice. The sensors are the same — 23 Sensor subclasses, each with a scan rate and a list of memories it writes, from TemptingSensor to PiglinSpecificSensor — and so are the eighteen *Ai classes, which hold the lists and, for the busier mobs, the helpers the lists call.

Nor is either of them data-driven. The villager’s day is data (Timelines.VILLAGER_SCHEDULE in Registries.TIMELINE) — but VillagerProfession is not — a BuiltInRegistries entry bootstrapped from code, with no directory under the built-in data pack, and neither is the PoiType it names (points of interest). And behaviours and goals are code too, plain Java lists in VillagerGoalPackages and the *Ai classes, not registered and not addressable from a data pack.

Which mobs use which

Every Mob has both fields, and almost every mob uses exactly one. Twenty classes override LivingEntity.makeBrain — Villager, Piglin, Warden, Hoglin, Frog, Allay, Axolotl, Goat and twelve more — and each keeps its behaviour lists in a class named for the mob: PiglinAi, WardenAi, FrogAi. There are eighteen such classes for twenty mobs, and the two exceptions are worth naming. Zoglin keeps its lists inline, in the mob itself. And Villager’s live in VillagerGoalPackages — genuinely the 26.3 name, and not a typo: it is the last survivor of the old convention, on a mob that has no goals at all. Nor do most of the other nineteen: a brain mob typically registers none.

One brain mob has a schedule: Brain.setSchedule has exactly two call sites and both are in Villager, picking the adult attribute or the baby one. The other nineteen never consult a clock: they call Brain.setActiveActivityToFirstValid, which walks a priority list and takes the first activity whose memory requirements hold. That is how PiglinAi picks fight over idle and FrogAi picks tongue over swim, with no time of day involved anywhere.

The brain’s trace: a villager’s day

Put the two phases together over one villager and the priorities become a clock. Four behaviours, in the order phase three reaches them:

sequenceDiagram
    participant Brain as Brain
    participant MTS as MoveToTargetSink
    participant AP as AcquirePoi
    participant UAFS as UpdateActivity<br/>FromSchedule
    participant EAS as Environment<br/>AttributeSystem
    participant SIB as SleepInBed

    rect rgba(0, 0, 0, 0.04)
    Note over Brain,SIB: one Brain.tick, phase three, ascending priority
    Brain->>MTS: priority 1, core
    Note over MTS: a wanted position leaves here for the pathfinder
    Brain->>AP: priority 6, core, which runs only while JOB_SITE is empty
    Brain->>UAFS: priority 99, last in the package
    UAFS->>Brain: updateActivityFromSchedule
    Brain->>EAS: getValue, VILLAGER_ACTIVITY at this position
    EAS-->>Brain: WORK, from tick 2000
    Brain->>Brain: setActiveActivity<br/>IfPossible
    end
    Note over Brain,SIB: with a job site held, the next Brain.tick is the first to run the work package
    rect rgba(0, 0, 0, 0.04)
    Note over Brain,SIB: after tick 12000, REST, which has no requirement
    Brain->>SIB: rest package, priority 3, once the walk home reaches the bed
    Brain->>SIB: tickOrStop, and again every tick until dawn
    end

A villager’s day as four behaviours in priority order; the two shaded bands are two ticks about ten thousand apart. The one to watch is the schedule behaviour at 99: it runs last, so the activity it asks for is the next tick’s — and Brain.setActiveActivityIfPossible takes it only if that activity’s requirements are met, and falls back to the default without saying so.

Read the priorities in that diagram as the ordering claims they are. The schedule behaviour is UpdateActivityFromSchedule, and it sits at 99, the last slot in every package that has one, so the activity a villager switches to is never the one the rest of this tick runs: the switch lands and the next tick acts on it. And it is only consulted when a behaviour asks — Brain.updateActivityFromSchedule refuses unless more than twenty ticks have passed since the last one. Five of the ten packages carry no such behaviour: core, panic and hide have nothing at 99 at all, and pre-raid and raid have ResetRaidStatus there instead. Core is never the activity a villager is stuck in, because it is always active alongside one other; the omission is how the other four pin the villager, and each carries its own way out rather than leaving it to the clock: VillagerCalmDown sits at priority 0 in the panic package and calls Brain.updateActivityFromSchedule itself once the fear memories clear, SetHiddenState does the same for hide, and ResetRaidStatus for the two raid packages. Nothing is asking the clock on a schedule; each escape hatch asks on its own terms, and the brain’s twenty-tick throttle meters them all.

The rest of the day hangs off that. Claiming a job site is AcquirePoi from the core package, and the claim itself — the scan, the best five, the one pathfind that decides which of them is reachable — belongs to points of interest, told there against a bed. A bell across a ravine is invisible to a villager. What is this page’s is what the memory then means: AcquirePoi writes MemoryModuleType.POTENTIAL_JOB_SITE, not the job site itself; AssignProfessionFromJobSite waits until the villager is within two blocks of that position, then erases the memory, writes MemoryModuleType.JOB_SITE and sets the profession if it has none — which is why walking to the workstation is a required step and not decoration, and why a villager that seems to be ignoring a perfectly good workstation is usually in one of three states rather than broken: it claimed the block and has not arrived yet, so the memory still says potential; the pathfind inside AcquirePoi failed and the site was skipped as unreachable; or it did not arrive within 1,200 ticks and GoToPotentialJobSite let the claim go. Three more behaviours can take a claim away afterwards — PoiCompetitorScan hands a contested one to the more experienced villager, YieldJobSite has a villager with no profession hand its claim to a neighbour whose profession works there, and ValidateNearbyPoi erases it when the block is gone.

Work is a weighted RunOne — one of the GateBehavior composites, which are behaviours whose children are behaviours — over six: WorkAtPoi (or WorkAtComposter), StrollAroundPoi, StrollToPoi, StrollToPoiList, HarvestFarmland and UseBonemeal; WorkAtPoi wants 300 ticks since the last check and 1.73 blocks or less to the workstation. Walking anywhere is MoveToTargetSink, entered on walk target present, path absent: it turns a MemoryModuleType.WALK_TARGET into a path, hands it to the navigation, and records a failure as MemoryModuleType.CANT_REACH_WALK_TARGET_SINCE. Below that hand-off is pathfinding and then movement and collision. Bed is SleepInBed, whose entry conditions belong to the night shift. What matters here is that it is this page’s counter-example: it overrides Behavior.canStillUse, so, like MoveToTargetSink and unlike a one-shot, it is still running on the tick after the one that started it, and it overrides Behavior.timedOut to answer no, so it never times out. It ends when Brain.isActive for Activity.REST goes false at dawn.

The goal selector’s trace: the zombie

A Zombie has a Brain, because every LivingEntity does, but it is the base one — no memories, no sensors, no behaviours, Brain.isBrainDead true. Almost everything it will ever do comes out of Mob.registerGoals, run once in the constructor: seven goals in Mob.goalSelector — a turtle-egg attack goal, a SpearUseGoal, a ZombieAttackGoal, a MoveThroughVillageGoal, a WaterAvoidingRandomStrollGoal, a LookAtPlayerGoal and a RandomLookAroundGoal — and five in Mob.targetSelector, one HurtByTargetGoal and four NearestAttackableTargetGoals. Almost, because a zombie is one of the few mobs that adds a goal later: Zombie.setCanBreakDoors, rolled against local difficulty at spawn, inserts a thirteenth — a BreakDoorGoal at priority 1, above everything the constructor registered — and takes it out again when the flag is cleared.

Every other tick each of the twelve is re-asked, and the flag table settles it. The target goals hold Goal.Flag.TARGET and write Mob.setTarget. The attack goals hold Goal.Flag.MOVE and Goal.Flag.LOOK and drive the navigation, with SpearUseGoal at priority 2 sitting above ZombieAttackGoal at 3 — though it wants a spear in the main hand, so for most zombies the attack goal is the one holding them. LookAtPlayerGoal wants Goal.Flag.LOOK alone and RandomLookAroundGoal wants Goal.Flag.MOVE as well, and both lose to whoever already has them. No activity, no schedule, nothing persisted but a door-breaking flag, nothing the world can push in but the control flags and a neighbour’s alert: the zombie’s entire mind is a handful of running bits and one target field. Even that field is not read directly — Mob.getTarget filters through Mob.asValidTarget on every call, so a target that turned creative or spectator is gone the moment it is asked for, and nine of the brain mobs source theirs from Mob.getTargetFromBrain instead.

For a 1.21-era reader. There is no Schedule class in 26.3 and no schedule registry, so the file a villager’s timetable used to live in is gone. What replaced it is a Timeline in Registries.TIMELINE, read through an EnvironmentAttribute rather than off the mob: Timelines.VILLAGER_SCHEDULE is the data, Brain.setSchedule takes an attribute rather than a schedule, and the lookup now takes a position. Activity, MemoryModuleType, Sensor and the behaviour classes keep their shapes, and world/entity/schedule is a package that still exists and holds one class, Activity.

Where to look

The two systems have one entry point each and they are short: GoalSelector.tick, whose three phases are the whole arbiter, and Brain.tick, whose four are. Read WrappedGoal.canBeReplacedBy beside the first — it is the entire priority rule in one method — and Behavior.tryStart and Behavior.canStillUse beside the second, with OneShot, because between them they explain which behaviours see a second tick. Mob.registerGoals on any mob shows a goal list; VillagerGoalPackages shows what a brain’s lists look like instead, and Villager.refreshBrain is how one is rebuilt mid-life. Brain.updateActivityFromSchedule is the method the opening is about. Two doors: BehaviorBuilder, the DSL the one-shot behaviours, over half of them, are declared with, and Sensing, the one-tick memo both systems share.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Pathfinding

Verified against Minecraft 26.3 · Part VI · A mob walks into a fence, stands there, and then wanders off — a hundred ticks after a behaviour told it where to go.

A behaviour has produced a position and stopped caring. Everything between that position and a mob leaning into a direction is this page: one A* search over a snapshot of already-loaded chunks, a Path of nodes, and a control that has to be told again every single tick. The part of it people recognise is the failure. Giving up is machinery, not an absence of it — every node the mob is walking towards carries a timeout computed from its distance and the mob’s current speed, three times over that budget abandons the path outright, and a separate check every hundred ticks asks whether the mob has covered a quarter of the ground its speed says it should have. The mob you watch walk into a wall and then wander off is running a scheduled surrender.

Above the waterline is goals and brains, which decides where; this page is how, and it is the same machinery whichever decision system asked.

The cast

classwhat it decidesthread
PathNavigationwhen to search, what budget to search with, when to give upthe Server thread
PathNavigationRegionwhich blocks the search is allowed to see — a snapshot, never a loadthe Server thread
NodeEvaluatorwhat a block is to this mob, as a PathTypethe Server thread
PathTypeCachethe 4,096-entry memo that makes that affordablethe Server thread, owned by ServerLevel
PathFinderthe A* itself, bounded by a node budgetthe Server thread
Paththe node list, and whether it reaches the targetthe Server thread, with a stream codec for the debug channel only
MoveControlwhere a wanted position becomes a yaw and a speed, and forgets it again the same tickthe Server thread

Nothing here is asynchronous. There is not a future, an executor or a thread anywhere in the pathfinder, and every entry point takes a ServerLevel or a Mob on one. Nothing crosses the network either, except when a debug client has subscribed.

The pipeline

Two ways in, one gate, and three ways the navigation ends it by itself (a caller can stop it too). What happens between the gate and the ending is the search, which has a figure of its own:

flowchart TD
    WANT["a goal or a behaviour: PathNavigation.moveTo, or PathNavigation.createPath"]
    WORLD["the world: PathNavigation.recomputePath, after a block update"]
    GATE{"any of the early exits?"}
    NULL["null, or the existing path unchanged"]
    RUN["the search, then the follow, one node at a time"]
    GIVEUP{"arrived, stuck for 100 ticks, or the node timed out?"}
    DONE["PathNavigation.isDone, and the caller finds out next evaluation"]

    WANT --> GATE
    WORLD --> GATE
    GATE -- "yes" --> NULL
    GATE -- "no" --> RUN
    RUN --> GIVEUP
    GIVEUP -- "yes" --> DONE
    GIVEUP -- "no" --> RUN

The page’s ways in and the navigation’s own ways out. The second entrance is the one to notice: the world pushes a recompute in after a block changes, and it is the only arrow on this page that does not start with a mob wanting something.

Asking: the ways a search does not happen

PathNavigation.createPath refuses before it does anything expensive. It returns null on an empty target set, on a mob below Level.getMinY, and on PathNavigation.canUpdatePath — which for GroundPathNavigation means on the ground, in liquid, or riding something, so an airborne mob simply cannot ask. The fourth exit is the interesting one: if there is already a path that is not done and the target of the navigation’s last search is among the requested targets, the existing path is returned unchanged. Re-asking PathNavigation.createPath for a destination you are already walking to costs nothing and changes nothing. And GroundPathNavigation, the navigation most mobs have, puts a fifth in front of the four for a request of one position or one entity: a target whose chunk is not loaded returns null at once.

Most callers never name it: PathNavigation.moveTo is what a goal or a behaviour reaches for, and its position and entity overloads call PathNavigation.createPath themselves and hand the result to the overload that takes a Path. Find a route and start walking it are one call from outside, with the early exits still in front of them, and that last overload restarts the stuck check even when the path is the one already being walked.

PathNavigation.recomputePath is the other entrance, and it is rate-limited rather than refused. More often than every twenty game ticks, or with PathNavigation.canUpdatePath false, it sets a flag instead and the next PathNavigation.tick tries again. That deferral is what keeps a mob standing in a doorway from re-searching twenty times a second.

The budget, which is also the map

One number governs both how hard the search may work and how much world it may look at, and it is worth naming before anything is built out of it. Call it the maximum path length: the larger of the mob’s follow range and its required path length, in blocks. PathNavigation.setRequiredPathLength sets the second half — 16 by default, raised in seven classes, all of them in the mob’s constructor or its navigation: 48 for Villager, Allay, Bee, CopperGolem and a baby HappyGhast’s navigation, 40 for Llama, 32 for Fox.

The node budget is that length times sixteen. PathNavigation builds its PathFinder with Attributes.FOLLOW_RANGE’s base value times sixteen, and PathNavigation.updatePathfinderMaxVisitedNodes later recomputes it against the modified follow range (attributes owns the difference between the two). The recompute has two triggers: PathNavigation.setRequiredPathLength itself, and Mob.onAttributeUpdated, which calls it when either Attributes.FOLLOW_RANGE or Attributes.TEMPT_RANGE changes — though only the follow range goes into the number, and nothing the game itself does changes it after the spawn-time bonus. PathNavigation.setMaxVisitedNodesMultiplier scales the result, and keeps scaling it until PathNavigation.resetMaxVisitedNodesMultiplier puts it back: Bee is the only class that touches either.

And the same length is the map

The same maximum path length — the blocks figure, not the node budget — becomes the radius of the PathNavigationRegion, plus an offset of 8 or 16 depending on which PathNavigation.createPath overload was used, and inside the search it appears twice more: as a test on the current node’s distance from the start and on each neighbour’s walked distance. A villager can find a bed 48 blocks away because its constructor set its required path length to 48, and the larger of the two numbers wins; it cannot find one 60 blocks away no matter how open the ground is. That is the budget behind a villager claiming a bed, whose scan radius is the same 48 for exactly this reason.

That region is built by asking ChunkSource.getChunkNow for every chunk in the cube. A path search never loads a chunk and never blocks — an absent chunk is a null entry that reads as air. This is the same discipline Entity.move uses for collisions, and it is why a search cannot stall a tick waiting for a chunk.

What a block is

NodeEvaluator answers what is this position, to this mob with a PathType: 27 constants, each carrying a default cost, and a negative cost means impassable, not expensive. Nine of the twenty-seven are −1 — PathType.BLOCKED, PathType.LAVA, PathType.FENCE, PathType.LEAVES, PathType.POWDER_SNOW, PathType.DAMAGING, PathType.UNPASSABLE_RAIL and the two closed doors; PathType.WATER is 8, PathType.FIRE 16, PathType.OPEN and PathType.WALKABLE 0. A mob overrides any of them for itself with Mob.setPathfindingMalus, read back through Mob.getPathfindingMalus — which is how the same lava is free ground to a Strider, merely expensive to a ZombifiedPiglin, and a wall to the ordinary Piglin that never overrides it. The question a block is asked is narrower than the answer: PathComputationType has three values — land, water and air — and a block’s own pathability hook sees only which of the three is being planned, never which mob is planning it.

Four evaluators in the pathfinder package cover the movement modes: WalkNodeEvaluator, and the three that specialise it or replace it — FlyNodeEvaluator and AmphibiousNodeEvaluator extend the walker, SwimNodeEvaluator extends NodeEvaluator directly. Two mobs subclass one further for themselves, Frog and Creaking. Each PathNavigation subclass chooses one: GroundPathNavigation, FlyingPathNavigation, WaterBoundPathNavigation, AmphibiousPathNavigation, and WallClimberNavigation on top of the ground one.

Classifying a block is expensive enough to memo. PathTypeCache is a fixed 4,096-entry table owned by ServerLevel, consulted through PathfindingContext — which attaches it only on a ServerLevel and falls back to computing the type from scratch otherwise. It is invalidated one position at a time from ServerLevel.sendBlockUpdated, which is the subject of the last section.

PathFinder.findPath is plain A* over a BinaryHeap of Nodes — one per candidate block position, carrying its PathType and its costs — with the destinations held separately as Targets, a Node subclass that also records whether anything reached it. Three details of the search decide what mobs feel like.

It is bounded twice: by a node count and by a distance. The loop breaks as soon as its visit counter reaches the budget — PathFinder.setMaxVisitedNodes, scaled by the multiplier — and inside the loop the maximum path length is the second bound, gating which nodes are expanded and which neighbours are admitted at all. Under the node budget alone a search through open ground would reach much further than the same budget spends in a maze; the distance bound is what stops it.

The heuristic is inflated. Each neighbour’s h is the best straight-line estimate multiplied by 1.5, which makes the search greedy: it finds a route sooner and the route it finds is not guaranteed to be the shortest. When several targets were reached, the winner is simply the path with the fewest nodes; when none was, it is the path that ends closest to a target, with fewest nodes as the tie-break.

A failed search still returns a path. If no target came within the reach range — measured as a Manhattan distance from the popped node — the finder reconstructs a path to the closest node it managed to reach and marks it not reached. That is what Path.canReach reports, and it is the number that matters rather than “was a path found”: AcquirePoi tests it before claiming a point of interest, and MoveToTargetSink turns a false into a cannot reach memory. A null from PathNavigation.createPath means one of the early exits rather than a search that came back empty-handed — the finder always has a best node to reconstruct towards.

One more thing the search does not do unless asked: it accumulates its closed set, and attaches it to the Path, only while something is subscribed to DebugSubscriptions.ENTITY_PATHS. PathNavigation installs that predicate in its constructor, off the server’s ServerDebugSubscribers. Path has a stream codec for exactly this and no gameplay reason.

sequenceDiagram
    participant MTS as MoveToTargetSink
    participant PN as PathNavigation
    participant PNR as PathNavigationRegion
    participant NE as NodeEvaluator
    participant PF as PathFinder
    participant MoveC as MoveControl

    MTS->>PN: createPath to the walk target, then moveTo with a speed modifier
    PN->>PN: the early exits, then push the pathfind profiler section
    PN->>PNR: build a cube of maxPathLength plus the offset, with getChunkNow
    PN->>PF: findPath — region, mob, targets, maxPathLength, reachRange, multiplier
    PF->>NE: getStart, then getNeighbors per popped node
    NE->>PNR: getBlockState, through PathfindingContext
    Note over NE,PNR: PathTypeCache memoises the PathType per position, on a server level
    PF-->>PN: a Path, reached or best-effort, with canReach set accordingly
    PN->>PN: trimPath, record the stuck-check position, keep the node index
    Note over PN,MoveC: every tick from here
    PN->>MoveC: setWantedPosition for the next node, at speedModifier
    MoveC->>MoveC: tick — the operation back to WAIT, then the yaw and the speed

The call chain is the thing to read: the navigation never talks to the evaluator, the finder does. Below the band, one search has become a position re-issued every tick for as long as the path lasts.

Following it, one tick at a time

PathNavigation.tick advances the node index and calls MoveControl.setWantedPosition with the next node and the speed modifier. It has to do that every tick, because MoveControl.tick sets its own MoveControl.Operation back to MoveControl.Operation.WAIT as it handles the move — the control is a one-shot instruction, not a destination it remembers.

MoveControl.setWantedPosition is the main way a decision, goal or brain, becomes movement, but it is neither the only method nor a single call site. MoveControl.strafe is a second entrance, used by RangedBowAttackGoal and the brain behaviour BackUpIfTooClose; MoveControl.setWait is a third. MoveControl.setWantedPosition itself has twelve callers outside the navigations, in eight classes: the shared goals TemptGoal.ForNonPathfinders and TryFindLiquidGoal, the per-mob goals of Bee, Blaze, Ghast and Vex, Rabbit from the mob itself rather than from a goal, and Fox to pin a sleeping fox where it lies. The first of those is why a happy ghast follows a held item in a straight line through terrain a path search would have routed around: an adult has a navigation like any mob, and none of its goals uses it. An ordinary tempted cow runs the base TemptGoal, which calls the navigation like anything else.

The three controls beside it

Three more controls sit beside it and are the rest of what turns a decision into a pose — all four implementing Control, and the move and look controls re-specialised by movement mode the way the evaluators are, so a SmoothSwimmingMoveControl or a FlyingMoveControl is the same object with different arithmetic. LookControl aims the head, JumpControl fires a jump the mover then executes, and BodyRotationControl keeps the body under the head: while the mob moves it sets the body to the mob’s own yaw, and while it stands still a head that swings more than fifteen degrees drags the body only far enough to stay within Mob.getMaxHeadYRot of it, and a head held still for ten ticks has the body turn to face it over the next ten. BodyRotationControl.clientTick is a leftover name: LivingEntity.tick calls Mob.tickHeadTurn with no side check at all, so it runs on both sides every tick.

The mover itself is Part VI’s movement and collision — the control sets the yaw and calls Mob.setSpeed, which writes LivingEntity.zza with it, and LivingEntity.travel does the rest. For a mob, LivingEntity.xxa is written with anything but zero only by the strafe branch, which pathfinding never takes.

Giving up

PathNavigation.doStuckDetection runs two independent timers, and they answer different questions.

The first half runs every hundred ticks: it compares where the mob is with where it was at the last check or the last PathNavigation.moveTo, against a threshold of the mob’s effective speed times 100 times 0.25 — a quarter of the ground the speed claims. Below that, PathNavigation.isStuck is set and the path is stopped. The effective speed is the speed itself at 1.0 or above and the square of it below, which quietly makes the threshold far more forgiving for slow mobs.

The second half is per node. When the next node changes, the navigation computes a time budget for it — the distance to that node divided by the mob’s speed, times twenty — and holds it against the real ticks counted since the last search that found a path, or the last time out, a count the change of node does not clear, so a clear path walked long enough times out too. Past three times that budget, PathNavigation.timeoutPath resets the counters and stops. This one catches a mob whose path is fine and whose route is blocked by something the search could not see, where the stuck check has not caught it first.

Both endings are the same ending from outside: PathNavigation.isDone becomes true, and whichever behaviour or goal was waiting on the path finds out on its next evaluation.

The one place the world pushes back

Everything above is AI asking the world questions. ServerLevel.sendBlockUpdated is the single call in the other direction. It invalidates the changed position in the path-type cache unconditionally, and then — only if Shapes.joinIsNotEmpty says the collision shape actually changed — walks ServerLevel.navigatingMobs, every mob in the level whether it has a path or not and unbounded by distance, asks each navigation PathNavigation.shouldRecomputePath about the position, and calls PathNavigation.recomputePath on the ones that say yes. That last loop — and only that loop — is wrapped in a re-entrancy flag, which a nested block update or a mob joining or leaving the set checks; either, arriving while the flag is set, logs and, in a development environment, pauses.

That is why closing a door in front of a mob re-routes it and repainting a block does not.

Where to look

PathNavigation is the spine and almost every question on this page is one of its methods: PathNavigation.createPath for the four early exits (and GroundPathNavigation.createPath for the fifth), PathNavigation.updatePathfinderMaxVisitedNodes for the budget, PathNavigation.tick for the following and PathNavigation.doStuckDetection for both timers. Then PathFinder.findPath, which is the A* itself and shorter than you expect, and NodeEvaluator — start with WalkNodeEvaluator, since two of the other three specialise it — for what a block is to a mob. PathType is the 27 constants and their costs, and Mob.getPathfindingMalus is how one mob disagrees with them. MoveControl.setWantedPosition is the seam where a path becomes a direction. Two doors: PathTypeCache, the 4,096-entry memo that makes the classification affordable, and ServerLevel.sendBlockUpdated, the one call in the whole system that runs the other way.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Damage and death

Verified against Minecraft 26.3 · Part VI · An arrow hits a player in full iron with Protection II: six damage becomes two, and if it kills, a message, a loot drop and a death screen.

The arrow lands, the screen goes red, the hearts drop by one, and somewhere behind you the archer’s client plays its hit ding. Six damage left the bow and about two reached your health, which is the part everybody knows. The part nobody sees is what happens when a second arrow arrives four ticks later, inside the flash. If more than ten ticks of invulnerability remain, that hit is worth only its excess over LivingEntity.lastHurt: a weaker one returns without dealing any damage, and a stronger one takes a partial branch that clears an internal took full damage flag. That one flag gates the damage-event broadcast, the knockback, the hurt sound and the red flash alike. Health goes down and none of those happens — which means neither ClientboundDamageEventPacket nor the red flash is a reliable “was hit” signal. It is all in LivingEntity.hurtServer.

The cast

classwhat it decidesthread
DamageSourcewhat hit and who: a direct entity, a causing entity, and — rarely — a position insteadbuilt wherever the hit starts, on the Server thread
DamageTypethe message id, the difficulty scaling, the food cost, a player’s hurt sound, and the kind of death messagea dynamic registry entry, loaded from a data pack and synced to clients
DamageTypeTagsalmost every behavioural branch on the path belowread on the Server thread
LivingEntitythe whole reduction pipeline, the i-frames, the flash, the two attribution references and deaththe Server thread
CombatRulesthe two pieces of arithmetic — armour and enchantment protectionstateless statics
CombatTrackerwhat killed you, and which entry gets the creditthe Server thread, cleared on its own timer
ServerPlayerPvP, the death screen packet, the death message, the inventory dropthe Server thread
Entitythat Entity.hurtServer is abstract — there is no default behaviour to inherit—

Everything above is server-side, and LivingEntity declares no Entity.hurtClient at all: no damage is ever calculated on a client. A client does set health — from LivingEntity.DATA_HEALTH_ID, from ClientboundSetHealthPacket, and to zero on the death event — but it never decides a number, and the signature says so: Entity.hurtServer takes a ServerLevel, so the side is enforced by the compiler rather than by a convention. Which side is allowed to decide anything is authority.

The number the arrow decides

AbstractArrow.onHitEntity builds the source first, because the number needs it: EnchantmentHelper.modifyDamage takes the DamageSource as an argument. It starts from AbstractArrow.baseDamage, 2.0 for an ordinary arrow, runs EnchantmentHelper.modifyDamage over the bow that fired it so Power raises the base, then multiplies by the arrow’s current speed and rounds up with Mth.ceil — so the damage that leaves the bow is an integer, and a slowing arrow does less. AbstractArrow.isCritArrow adds a random bonus of up to half the damage plus one. Six is a fresh arrow at full draw before that bonus, which a full draw always earns, so six is the one draw in five that adds nothing.

DamageSources.arrow then names two entities: the arrow as the direct entity, the shooter as the causing one — or, when the arrow has no owner left, the arrow itself in both roles. DamageSource.getWeaponItem reaches through the direct entity, which is how Breach on the attacker’s weapon reaches the victim’s armour calculation. A source can carry a position instead, though far less often than you would guess: ordinary explosions do not, and fall back to the direct entity in DamageSource.getSourcePosition. Exactly three sources are genuinely positional, and only they put a position on the wire through DamageSource.sourcePositionRaw — DamageSources.badRespawnPointExplosion, /damage … at (DamageCommand), and ExplodeEffect unattributed.

The DamageType behind the source is five fields — message id, DamageScaling, food exhaustion, DamageEffects (which picks the hurt sound, and only for a Player: Player.getHurtSound is its one caller) and DeathMessageType — and it lives in a data pack. There are 51 keys in DamageTypes and 36 tags in DamageTypeTags, and almost every behavioural branch below is tag-driven rather than type-driven. The exceptions are worth listing because they are the whole list: thorns picks its own secondary sound in LivingEntity.playSecondaryHurtSound, wind charge is excluded from mob-anger attribution against an EntityTypeTags.NO_ANGER_FROM_WIND_CHARGE entity, and a ServerPlayer mid-dimension-change is invulnerable to all but DamageTypes.ENDER_PEARL.

Three gates before anything is computed

LivingEntity.hurtServer opens with three early returns: already invulnerable, already dying, or a DamageTypeTags.IS_FIRE source against MobEffects.FIRE_RESISTANCE — a mob-effect immunity sitting outside the reduction pipeline entirely. Invulnerability itself is Entity.isInvulnerableToBase (removed, the invulnerable flag or its timed twin Entity.invulnerableTime, fire immunity, fall immunity) or an enchantment-granted one through EnchantmentHelper.isImmuneToDamage, which is how Frost Walker makes its wearer immune to magma blocks.

Two subclasses get there first. ServerPlayer.hurtServer adds the PvP and team check — ServerPlayer.canHarmPlayer refuses when the level forbids PvP or the two share a team that disallows friendly fire — testing both a Player causing entity directly and an AbstractArrow causing entity by asking it for its owner. ServerPlayer.isInvulnerableTo adds the two conditions that have no tag: mid-dimension-change, and a client that has not finished loading. Then Player.hurtServer refuses a creative player unless the source is DamageTypeTags.BYPASSES_INVULNERABILITY, and applies difficulty scaling — halved and offset on easy, one and a half times on hard, zero on peaceful — but only when DamageSource.scalesWithDifficulty says so. That reads DamageScaling off the type, and DamageTypes.ARROW is when_caused_by_living_non_player: a skeleton’s arrow scales and a player’s does not. Player.isInvulnerableTo is also where GameRules.DROWNING_DAMAGE, GameRules.FALL_DAMAGE, GameRules.FIRE_DAMAGE and GameRules.FREEZE_DAMAGE live — switching one off makes a player immune rather than making the damage smaller.

One number, eight steps, and no step that knows another

Past the gates the number goes down a chain of eight arithmetic steps — five multiplications and three subtractions — each owned by a different piece of the game, and none of them aware of the others.

flowchart TD
    N0["the bow"]
    N1["blocking"]
    N2["freezing"]
    N3["the helmet"]
    N4["i-frames"]
    N5["armour"]
    N6["Resistance"]
    N7["protection"]
    N8["absorption, then health"]

    N0 -- "6.0" --> N1
    N1 -- "6.0" --> N2
    N2 -- "6.0" --> N3
    N3 -- "6.0" --> N4
    N4 -- "6.0" --> N5
    N5 -- "3.12" --> N6
    N6 -- "3.12" --> N7
    N7 -- "2.12" --> N8

The number rides the arrows: four steps in a row take nothing at all, and the whole reduction on a fully armoured victim happens in two.

Which piece of the game owns each of the eight, and what it does to the number:

#stepowned bywhat it does to the numberleaves
—the arrow leaves the bowAbstractArrowMth.ceil of speed times base damage, Power already folded into the base6.0
1blockingDataComponents.BLOCKS_ATTACKS, through LivingEntity.applyItemBlockingsubtracts what the component resolves for this angle and this damage type6.0
2freezingEntityTypeTags.FREEZE_HURTS_EXTRA_TYPESmultiplies by five6.0
3the helmetDamageTypeTags.DAMAGES_HELMETmultiplies by 0.756.0
4the i-frame windowLivingEntity.lastHurtover ten ticks left, only the excess survives6.0
5armourCombatRules.getDamageAfterAbsorbeffective armour over 25, floored at a fifth and capped at 20 — here 48 per cent off3.12
6ResistanceLivingEntity.getDamageAfterMagicAbsorbfive of twenty-five per level, and nothing at all at amplifier four3.12
7protectionCombatRules.getDamageAfterMagicAbsorbthe summed points over 25, capped at 20 — here 8 points, 32 per cent off2.12
8absorptionLivingEntityspent before health is, then CombatTracker.recordDamage and LivingEntity.setHealth2.12

Before the first row there is one gate the chain never sees: Player.hurtServer scales incoming damage by difficulty, and a player’s own arrow is not scaled.

The first link is blocking, and shields are no longer a mechanism, only a vocabulary. LivingEntity.applyItemBlocking asks the item being used — through LivingEntity.getItemBlockingWith, which enforces BlocksAttacks.blockDelayTicks — for a DataComponents.BLOCKS_ATTACKS component, checks the damage type against BlocksAttacks.bypassedBy, computes the angle between the source position and the victim’s head rotation, and lets BlocksAttacks.resolveBlockedDamage sum the reductions in the component’s own list that apply. BlocksAttacks.hurtBlockingItem then charges durability — for a blocking Player only; a mob’s item never wears — and a non-projectile block sends the blocker reeling through LivingEntity.blockedByItem, which is how a Hoglin throws whoever blocked it and a Ravager, unless it is roaring, either stuns itself while shoving them or shoves them harder on a coin flip. An arrow with any piercing level skips all of it before the angle is computed. ShieldItem still exists, the statistic is still Stats.DAMAGE_BLOCKED_BY_SHIELD, and the axe-disables-shield rule survives as LivingEntity.getSecondsToDisableBlocking — which Warden overrides, and which reads the attacker’s Weapon component only while that weapon is also their active item (the sword swing) — feeding Player.blockUsingItem and BlocksAttacks.disable. None of it is shield-specific code any more.

The two multipliers after it almost never fire and are both pure tag lookups: DamageTypeTags.IS_FREEZING against an entity in EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES multiplies by five, and DamageTypeTags.DAMAGES_HELMET against a helmeted victim calls LivingEntity.hurtHelmet and multiplies by 0.75.

Ten ticks in which nothing shows, and ten that protect nothing

LivingEntity.damageCooldownTime is set to 20 by a hit that lands in full and counted down once a tick — from LivingEntity.baseTick for everything except a ServerPlayer, and from ServerPlayer.tick for players, earlier in the tick. LivingEntity.hurtDuration and LivingEntity.hurtTime are set to 10, so the red flash covers half of it.

It is the flashing half that is invulnerable. The test is LivingEntity.damageCooldownTime still being above ten, which is exactly the ten ticks the flash is showing; once the counter falls to ten the next hit takes the ordinary branch, lands in full, and resets both numbers. The second half of what everyone calls the invulnerability window protects nothing at all.

Inside the first half, then, unless the type carries DamageTypeTags.BYPASSES_COOLDOWN, the incoming damage is compared against LivingEntity.lastHurt — what the last hit was worth — and only the excess is applied. A hit that is not bigger returns before any hurt sound, damage packet, knockback of the victim or combat entry. A hit that is bigger applies the difference and clears a local took full damage flag, and everything downstream sits inside a test of that flag: no ServerLevel.broadcastDamageEvent, no Entity.markHurt, no LivingEntity.dealDefaultKnockback, no hurt sound, no reset of the flash. So the first hit in each window is the one everyone sees; a later, stronger one lands its excess with no flash for anyone watching, though its lower health still goes out, and a victim’s own client, which deduces a hit from the drop, flashes anyway.

LivingEntity.resolveMobResponsibleForDamage and LivingEntity.resolvePlayerResponsibleForDamage sit outside that test and run on the silent stronger partial hit as well as the full one — only the weaker hit, which returns before them, leaves no damage and no attribution. They write LivingEntity.lastHurtByMob and LivingEntity.lastHurtByPlayer (with its hundred-tick LivingEntity.lastHurtByPlayerMemoryTime countdown), crediting a tamed Wolf’s work to its owner. LivingEntity.lastDamageSource and LivingEntity.lastDamageStamp are set beside them, but only when the hit counted for something.

Armour, and why big hits punch through it

LivingEntity.actuallyHurt is where the number meets the victim’s gear. LivingEntity.getDamageAfterArmorAbsorb first calls LivingEntity.hurtArmor — which is empty on LivingEntity and overridden only by Player, Horse and Wolf. A skeleton in full iron never wears its armour out. Where it is implemented it routes to LivingEntity.doHurtEquipment: one durability point per four damage, minimum one, per piece, and each piece must separately be Equippable.damageOnHurt, damageable, and pass ItemStack.canBeHurtBy.

The armour formula, in the order it is applied

Then CombatRules.getDamageAfterAbsorb does the arithmetic. Effective armour is the armour points, reduced by the incoming damage divided by (two plus a quarter of toughness), then clamped between CombatRules.MIN_ARMOR_RATIO (0.2) of the nominal points and CombatRules.MAX_ARMOR (20); the fraction taken off is that effective armour over CombatRules.ARMOR_PROTECTION_DIVIDER, which is 25. Big hits punch through armour by design: the subtraction is what makes a 40-damage hit see less armour than a 6-damage one, and Attributes.ARMOR_TOUGHNESS is exactly the term that slows it down. Full iron is 15 points and no toughness, so a 6-damage hit sees 12 effective armour, 48 per cent off, 3.12 left. Breach moves the resulting fraction through EnchantmentHelper.modifyArmorEffectiveness first.

LivingEntity.getDamageAfterMagicAbsorb then applies Resistance and sums EnchantmentHelper.getDamageProtection across every equipment slot. Protection is not a class: it is EnchantmentEffectComponents.DAMAGE_PROTECTION holding a conditional value effect, confined to armour by its own slots declaration in the JSON rather than by the helper, worth one point per level per piece — so Protection II on four pieces is 8. CombatRules.getDamageAfterMagicAbsorb caps that sum at 20 and takes sum over 25 off. 3.12 becomes about 2.12.

Armour is capped twice and floored once. Effective armour never drops below a fifth of nominal and never counts above 20, and protection points cap at 20 as well — so armour alone tops out at 80 per cent reduction, protection at 80 per cent of what is left, 96 per cent combined — so armour alone can never take a hit to nothing. An effect can: Resistance at amplifier four multiplies by zero. What survives comes off absorption first; then, unless absorption took it all, CombatTracker.recordDamage files a CombatEntry, and health takes the rest. Player.actuallyHurt overrides the whole method to add food exhaustion (DamageType.exhaustion, 0.1 for an arrow) and the damage statistics.

Telling everyone, and what a block replaces

The arithmetic is over and the number has landed. What follows is an order, and two of its steps are the ones players notice going missing:

sequenceDiagram
    participant AA as AbstractArrow
    participant SP as ServerPlayer
    participant CT as CombatTracker
    participant SL as ServerLevel
    participant CPL as ClientPacketListener

    AA->>SP: hurtOrSimulate, the arrow's source and 6.0
    SP->>SP: ServerPlayer.hurtServer, canHarmPlayer and teams
    SP->>SP: Player.hurtServer, the creative gate and difficulty
    SP->>SP: LivingEntity.hurtServer, the gates and the multipliers
    SP->>SP: i-frames, where a partial hit clears the flag
    SP->>SP: actuallyHurt, absorption first
    SP->>CT: recordDamage, one CombatEntry
    SP->>SP: then setHealth
    SP->>SP: resolveMobResponsible<br/>ForDamage
    SP->>SP: resolvePlayer<br/>ResponsibleForDamage
    SP->>SL: broadcastDamageEvent, full hits and nothing blocked
    SL-->>CPL: ClientboundDamageEventPacket, every tracker and the victim
    SP->>SP: markHurt, then dealDefaultKnockback
    SP->>SP: indicateDamage, skipped if anything was blocked
    SP-->>CPL: ClientboundHurtAnimationPacket, that one player
    SP->>SP: dead? then checkTotemDeath<br/>Protection

One lane for the victim, because ServerPlayer and LivingEntity are one object: the three hurtServer messages at the top are one virtual call going down the override chain. ServerLevel.broadcastDamageEvent comes before the knockback, not after — and the last message is where the next section takes over.

ServerLevel.broadcastDamageEvent sends the type, three entity ids — the victim, the causing entity and the direct one — and an optional position to every tracking player and the victim, and it runs before the knockback, not after. A successful block replaces it entirely: if the blocking component absorbed anything, BlocksAttacks.onBlocked plays the block sound and no damage event is broadcast at all, so a blocked hit puts no flash on anyone else’s screen. Only then does Entity.markHurt queue the velocity packet, and LivingEntity.dealDefaultKnockback compute a direction (from the projectile for a projectile, from the source position otherwise) for LivingEntity.knockback, which scales it by one minus Attributes.KNOCKBACK_RESISTANCE. ServerPlayer.indicateDamage ends that call, and is skipped when anything was blocked. The hurt sound comes after all of it — after the death check, not with the flash.

Six packets carry the hit itself, beside the sound packets. ClientboundDamageEventPacket (type holder, causing and direct entity ids, optional position) and ClientboundSetEntityMotionPacket go to every tracker, and to the victim when it is a player, ClientboundHurtAnimationPacket and ClientboundSetHealthPacket only ever to one player about themselves, ClientboundSetEntityDataPacket carries LivingEntity.DATA_HEALTH_ID for a mob, and ClientboundEntityEventPacket carries one byte — 3 for death, 60 for the poof, 35 for a totem. Inbound, only the respawn command.

The damage amount never crosses the wire. The client picks a flash, and for your own player a sound, from the type and infers magnitude from health — and only for your own player, in LocalPlayer.hurtTo, the one place a hit is deduced from a health drop. LivingEntity.handleDamageEvent sets LivingEntity.damageCooldownTime to 20 and the flash to 10 and plays the sound (which only your own player’s hurt is heard from; everyone else’s comes from the server), touching health not at all.

Death, or not

If health has reached zero, LivingEntity.checkTotemDeathProtection looks for DataComponents.DEATH_PROTECTION in either hand — unless the source is DamageTypeTags.BYPASSES_INVULNERABILITY, which is why /kill cannot be totemed — and on a hit consumes one, sets health to one, applies DeathProtection’s effects and broadcasts byte 35.

Otherwise LivingEntity.die runs, and its order matters:

flowchart TD
    Z["health has reached zero"]
    T{"DEATH_PROTECTION in a hand, and the source does not bypass?"}
    TOT["consume one, health to one, byte 35"]
    D["LivingEntity.die: kill credit, then LivingEntity.handleKillingBlow"]
    V{"no causing entity, or Entity.killedEntity agrees?"}
    L["the death game event, LivingEntity.dropAllDeathLoot, the wither rose"]
    B["the entity-event byte, to every watcher"]
    P["Pose.DYING"]
    Z --> T
    T -- "yes" --> TOT
    T -- "no" --> D
    D --> V
    V -- "yes" --> L
    V -- "no" --> B
    L --> B
    B --> P

The diamond in the middle is a veto one other entity holds over this one’s loot. The two boxes below it are what the veto cannot reach: the byte goes to every watcher either way, and the pose is set after the byte, not with it.

Kill credit is read from the attribution references written a few lines earlier, and LivingEntity.handleKillingBlow sets LivingEntity.dead. Then — with a causing entity, only if Entity.killedEntity on it agrees; with none, unconditionally — the death game event fires, LivingEntity.dropAllDeathLoot runs and the wither rose is planted. The entity-event byte sits outside that veto and goes out to every watcher before Pose.DYING is set — the byte first, the pose after.

Loot needs a recent player. LivingEntity.dropAllDeathLoot reads killed by a player off LivingEntity.lastHurtByPlayerMemoryTime still being above zero, so an attribution older than a hundred ticks costs the loot context its LootContextParams.LAST_DAMAGE_PLAYER and its luck, and costs the experience drop entirely unless the mob is an always-dropper. LivingEntity.shouldDropLoot gates loot on GameRules.MOB_DROPS and on not being a baby — but Monster.shouldDropLoot drops the baby condition, which is why baby zombies and piglins do drop. LivingEntity.shouldDropExperience mirrors it. Only LivingEntity.dropEquipment is outside both.

The death screen, and what the client does alone

ServerPlayer.die never calls up. It reimplements the sequence, so LivingEntity.handleKillingBlow never runs and LivingEntity.dead stays false on a dead player for the whole death screen. It reads the message from the combat log at the top and calls CombatTracker.recheckStatus at the bottom, so the log survives long enough to be read. GameRules.SHOW_DEATH_MESSAGES gates more than the chat line: with the rule off the message is never assembled (text components owns the assembling) and ClientboundPlayerCombatKillPacket carries an empty component, so the death screen still opens and says nothing. ServerPlayer.die also forgives neutral mobs under GameRules.FORGIVE_DEAD_PLAYERS, drops the inventory through Player.dropEquipment unless GameRules.KEEP_INVENTORY, and calls ServerGamePacketListenerImpl.markClientUnloadedAfterDeath. DeathScreen opens unless GameRules.IMMEDIATE_RESPAWN, and the object that comes back — a new ServerPlayer wearing the old one’s id and connection — belongs to players and sessions.

Two things then happen without a packet of their own. The client kills mobs on its own, from one byte: LivingEntity.handleEntityEvent for byte 3 sets a non-player entity’s health to zero and runs LivingEntity.die locally, so the twenty tick animation in LivingEntity.tickDeath is client-driven, not a consequence of a health update. And CombatTracker clears itself — after CombatTracker.RESET_DAMAGE_STATUS_TIME out of combat or CombatTracker.RESET_COMBAT_STATUS_TIME in it. Four places call the check: a twenty-tick timer in LivingEntity.tick, the top of CombatTracker.recordDamage, LivingEntity.remove, and ServerPlayer.die — so a hit after a long lull discards the old log before filing its entry.

Who gets the credit for a fall

The log is not read in order, and the rule that reads it is the reason for was doomed to fall by. CombatTracker.getMostSignificantFall walks the entries looking for the biggest fall, and when it finds one it credits the entry before it — the hit logged just before the fall’s own entry — taking the fall entry itself only when the fall is the first thing in the log. It keeps a second candidate beside that, the biggest-damage entry that carries a FallLocation. Then the threshold: the fall counts only if it was more than five blocks, and the alternative only if its damage was more than five; below both, the tracker returns nothing and the death message falls back to the ordinary one for the killing blow. So a skeleton’s arrow and then a four-block fall that kills you is hit the ground too hard while trying to escape the skeleton (the source’s own message, crediting the last mob that hurt you), and the same arrow before a ten-block fall is doomed to fall by the skeleton — same two entries, one number apart. The FallLocation is what turns the credited entry into the wording (fell off some vines, fell off a ladder, fell while climbing), and building the sentence out of it belongs to text components.

Everything that calls it

Entity.hurt and Entity.hurtOrSimulate are deprecated final wrappers over the abstract method: the second picks a side and returns Entity.hurtClient off a client level, the first does nothing there. Entity.hurtClient has ten declarations counting the base, and every one only ever answers did this connect — including RemotePlayer, which returns true unconditionally so a client-side arrow can play its own effects without knowing any numbers.

Environmental damage is ticked from more places than base tick suggests. Fire (once per twenty fire ticks) is in Entity.baseTick, lava (four at a time) in Entity.lavaHurt, and suffocation, world-border and drowning damage in LivingEntity.baseTick. Freezing and cramming are not: freezing is in LivingEntity.aiStep every forty ticks, and cramming inside LivingEntity.pushEntities, called near the end of the same method under GameRules.MAX_ENTITY_CRAMMING, just before the drowning a water-sensitive mob takes in water or rain.

Twenty-two classes with no pipeline at all

Everything above this line is LivingEntity’s, and none of it is inherited. Entity.hurtServer is abstract: there is no default behaviour anywhere in the tree, so every branch answers for itself. Fifty-five files override it — a fifty-sixth, Entity itself, only declares it — thirty-three of them LivingEntity and its descendants, ArmorStand among them, a LivingEntity despite having no AI (entity anatomy). The other twenty-two are not living entities at all, and none of them runs armour, i-frames, absorption, the combat tracker or the death sequence itself.

Six patterns cover all twenty-two, and the sharpest are the ones that read the damage number — only four classes do. ItemEntity and ExperienceOrb keep a plain integer of health and subtract from it. VehicleEntity adds damage × 10 to an accumulator that loses one a tick and breaks past 40, which is why any hit over four damage breaks a minecart at once and smaller ones must land close together, and MinecartTNT inherits that accumulator after its own override has looked at the arrow rather than the number. For the other eighteen the answer is a yes or a no: ten do nothing whatever, two resend their motion and nothing else, five are destroyed by one hit of any size (an unfixed frame holding something loses only its item to the first) — an EndCrystal among them, and it is immune to the EnderDragon that eats it — and EnderDragonPart reads nothing, forwarding the whole call, number included, to its parent. Which class does which is the non-living damage table.

For a 1.21-era reader. The hurt cooldown is LivingEntity.damageCooldownTime. invulnerableTime on Entity is now a separate timer of full invulnerability that no hit sets: a skeleton trap gives the horses it adds and every skeleton rider sixty ticks of it, a cushion broken by lightning gives the item it drops twenty, a conversion carries it over, and Entity.isInvulnerable is true while it runs.

Where to look

LivingEntity.hurtServer is the whole first half of this page in one method, and the two overrides that reach it first — ServerPlayer.hurtServer and Player.hurtServer, over the abstract Entity.hurtServer — are the gates. Then LivingEntity.actuallyHurt and the two CombatRules statics it reaches, which are short and settle every argument about armour. DamageSource and DamageType are the what hit you, and DamageTypeTags is where nearly every branch on the page lives, so read the tag file before the type file. For death: LivingEntity.die and then ServerPlayer.die, which does not call it, with CombatTracker and FallLocation for the message. Two doors: BlocksAttacks, the component that replaced shield code, and VehicleEntity, the clearest of the twenty-two branches that inherit none of this. For the numbers as attributes — armour, toughness, knockback resistance — see attributes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

VII · Items and inventories

Verified against Minecraft 26.3 · Part VII · The things you carry: what a stack is, what happens when you use one, how two machines agree about a chestful of them, and the three engines that make them out of data.

A block is a position in a grid and an entity is a thing in the world. An item is neither: it is a stack, and a stack only exists inside something else — a hand, a slot, a chest, a recipe grid, a dropped ItemEntity, a packet. A player recognises the part by the moments that container is showing: the shift-click that lands before anything has answered it, the bow that fires when you let go rather than when it finished drawing, the chest whose contents appear a tick late, the dungeon chest that is empty until somebody opens it. Two of those four are a client guessing ahead of the server, the shift-click and the bow (containers and menus, using an item); the tick-late chest is not a guess at all, but a broadcast that missed its phase, and the dungeon chest is a roll the server has not made yet. Telling those apart is most of what this part teaches. An item is the thing in the game that is never simply somewhere — so almost every page here is about a container, and about which of the two programs is allowed to believe what it holds.

The shape of the part

More than in most parts, an item is where some other system surfaces: BlockItem belongs to Part V · Blocks, BucketItem to Part IV · The world, SpawnEggItem to Part VI · Entities, AttackRange to Part VIII · The player, and Equippable to whatever wears it, armour and saddle alike. In each the item is only the handle.

Three subjects go out the same door. The part stops at the slot: what a player’s own inventory is belongs to player anatomy, how a held stack picks the model you see belongs to models and atlases, and the reason a container click is not on the prediction ledger — no sequence number, no window — belongs to prediction and acknowledgement.

What is left is two tiers. The first three pages are the vocabulary — what a stack is, what using one does, and how a set of them is kept in agreement across the wire. The last five are three engines built out of one pattern — a registry of kinds, a dispatch codec, a data file naming one (data-driven types) — that produce or decorate stacks: recipes, enchantments and loot tables, with enchanting and contexts and predicates beside the last two.

The engines lean on the vocabulary, and where they touch each other (one of enchanting’s five paths runs through loot functions, and an enchantment’s conditions are contexts and predicates’ machine) each page links the other’s machine rather than teaching it. Contexts and predicates is the outlier: its subject is not a stack at all, but the question engine the other engines happen to run on, and it can be watched first.

Every arrow below is what the next page can now assume, not what its code calls. Read down a chain and nothing is missing; read across two and what one owes the other is a link, not a lesson.

flowchart TD
    subgraph V["the vocabulary"]
        IS["1 · Items and stacks"]
        UI["2 · Using an item"]
        CM["3 · Containers and menus"]
    end
    subgraph E["the engines"]
        RE["4 · Recipes"]
        EN["5 · Enchantments"]
        EC["6 · Enchanting"]
        CP["7 · Contexts and predicates"]
        LO["8 · Loot tables"]
    end
    IS -- "a stack is a diff over a prototype" --> UI
    UI -- "and a slot is where one lives" --> CM
    CM -- "an arrangement in a grid" --> RE
    IS -- "a modifier on the stack" --> EN
    EN -- "and how one gets onto a stack" --> EC
    CP -- "which needs no stack at all" --> LO

The two tiers of the part, numbered to the watch order. Every arrow means “and now you can assume this”, so contexts and predicates, the one page after the first with no arrow into it, is one you can watch whenever you like.

Before you start

Data components is the hard prerequisite: a stack is an item plus a component patch, and this part never re-teaches the component system. Codecs, NBT and JSON for the four paths a stack travels, and identifiers and registries and the resource system for where recipes, enchantments and loot tables come from and when. They come from two different places, and the difference bites: recipes and loot tables are both in the reloadable registry layer, so /reload rebuilds both — while enchantments are a world-load dynamic registry that /reload never re-reads, their tags aside.

Three ordering facts matter more than they look. The server tick drains the packet queue before any level ticks, and a click is answered inside that drain; the level tick decides when a menu’s other changes are broadcast (a pickup’s excepted, which the connection phase sends), which is what makes a hopper’s delivery visibly late; and block interaction is how a chest gets opened in the first place, which is where two of these pages start.

Watch in this order

The first three in order, then the engines in any order the arrows allow; contexts and predicates needs none of the first three and can come first.

  1. Items and stacks — an Item holds almost no data and an ItemStack holds a diff, and everything else follows: what makes two of them the same stack, what one may legally hold, and why a client never predicts the wear an item takes.
  2. Using an item — a meal and a bow, which are one machine read two ways. The client’s countdown never stops at zero: the meal ends when a single byte arrives, and the bow ends when you let go.
  3. Containers and menus — a shift-click out of a chest. One packet goes up, nothing comes back, and agreement is silence, because the server adopted the client’s claim as its new baseline.
  4. Recipes — eight planks and an empty middle. No recipe ever crosses the wire, and yet the client holds the whole contents of every recipe it has unlocked.
  5. Enchantments — there are no enchantment subclasses. Fire Aspect is a data-pack record whose “melee only” rule is one loot condition, the burn that follows belongs to something else entirely, and the three enchantments players talk about most are driven from the other end, only one of them with no effect component at all.
  6. Enchanting — the five paths that change what an item is enchanted with, one of which runs backwards. The seed is per player, saved, and sent to the client, which is why the Standard Galactic gibberish is stable and why an anvil never changes what the table is offering.
  7. Contexts and predicates — the engine that answers is this true here. Seventeen of its thirty-one parameter sets never roll a loot table at all: five of them belong to enchantment effects, five are built for a number or a set of slots, one exists only to validate against, and the rest are /execute if predicate, entity selectors, advancement triggers and villager trades.
  8. Loot tables — the worked example, and the part’s last lecture. A dungeon chest is genuinely empty on disk, and the first thing to read it — a hopper will do, and so will breaking it — commits the roll, with no player in it.

Watched as lectures, five and six are the pair to keep together: what an enchantment is and how you get one. Seven and eight are the other pair, and seven is the one Part XIII’s landing page comes back for.

Where the part stops

The part is 581 files and 49,245 lines across world/item, world/inventory and world/level/storage/loot, and 27% of the part’s lines are named on no page in the book. Much of that is an answer rather than a gap: it is the families each page names once and does not enumerate. world/item’s sixty-seven Item subclasses are, all but two, kept for a behaviour hook no component can express; the twenty-eight menu classes in world/inventory are one machine with different slot lists; the forty-two loot functions and twenty loot conditions are one shape each; and the special crafting recipes are the nine the recipe book never shows.

Four things here are explained nowhere: villager trading (VillagerTrades, the bootstrap behind the trade files, is alone the part’s fifth-largest class — the loot machinery a trade runs on is covered, the trades themselves are not), brewing, a recipe kind the recipes page names and does not follow, the creative tabs, and armour identity and trims. A second edition should take them; this one names them and says so.

Reference this part uses

Three were written for it. The weapon helpers — the seven Item.Properties methods that make a weapon and the forty-two items built by one; enchantment hooks — every EnchantmentHelper entry point with the classes that call it, which is where most of the game meets the enchantment system. Loot context parameter sets — all thirty-one, with the keys each one requires and allows. Then the catalogue behind the part’s hard prerequisite, data components — every component type in DataComponents with what it holds; packets for the container and recipe traffic; registries for where each engine’s elements live; naming drift for the predicate library’s move out of critereon; and diagram lanes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Items and stacks

Verified against Minecraft 26.3 · Part VII · A diamond pickaxe sits in a hotbar slot, is compared against its neighbours, is sent to a client, and finally loses its last point of durability.

A diamond pickaxe is in your hotbar. The Item behind it, Items.DIAMOND_PICKAXE, is a single object shared by every diamond pickaxe that has ever existed on this server, and it holds four fields: a description id, a crafting remainder, a feature-flag set, and its own registry holder — Item.descriptionId, Item.craftingRemainingItem, a FeatureFlagSet in Item.requiredFeatures, and Item.builtInRegistryHolder. Not the stack size. Not the mining speed. Not the durability. All of that is data components — and they do not live on the Item either. They live on the item’s Holder.Reference in BuiltInRegistries.ITEM, as a prototype map built at the first data-pack load and rebuilt at every one after (data components), and the stack borrows that map read-only and stores only the ways it differs from it. This page is the object in the slot rather than the component system behind it: what an ItemStack is made of, what makes two of them the same stack, what a stack may legally hold, and what happens to one that runs out of durability. That last is the odd one out in a part where almost everything is predicted locally and corrected afterwards. A client never even guesses at the durability an item spends — the method that spends it demands a ServerLevel outright, and the convenient overloads that take a LivingEntity instead look its level up and silently do nothing when the answer is a client’s.

The cast

classwhat it decidesthread
Itemthe behaviour hooks, and four fields that are not componentsboth
Item.Propertiesthe builder — which produces an initializer, never a component mapclass-init, wherever the bootstrap runs
Holder.Referencewhere an item’s default components live, and whether they exist yetwritten by the thread loading the world, on the Server thread at each /reload, and on the Render thread for a joining client
DataComponentInitializersthe pile of pending default maps, one entry per registered itembuilt on the worker pool on the server, on the Render thread on a joining client
ItemStacka holder, a count, a pop time and a patched map — the mutable thing in a slotboth
PatchedDataComponentMapprototype plus patch, and the copy-on-write flag that makes copying a stack cheapwherever its stack is
ItemStackTemplatethe immutable stack: what a stack looks like inside a component, a particle or a recipeboth
ItemEntitya stack that is an entity, with a five-minute clock and a merge ruleServer, mirrored on the Render thread

Four fields, and only one of them is really data

ItemStack is a final class holding exactly four things: two ints, a registry holder, and the interesting one.

classDiagram
    class ItemStack {
        int count
        int popTime
        Holder~Item~ item
        PatchedDataComponentMap components
    }
    class Item {
        String descriptionId
        ItemStackTemplate craftingRemainingItem
        FeatureFlagSet requiredFeatures
        Holder.Reference builtInRegistryHolder
    }
    class Reference["Holder.Reference, in BuiltInRegistries.ITEM"] {
        the Item
        the item's default DataComponentMap
    }
    class PatchedDataComponentMap {
        the prototype, borrowed
        the patch, owned
    }
    ItemStack *-- PatchedDataComponentMap : the only mutable half
    ItemStack --> Reference : every stack of this item, the one holder
    Reference --> Item
    PatchedDataComponentMap ..> Reference : reads the defaults, never writes them

ItemStack’s four fields and Item’s four, and the arrows between the classes. The dotted one is the shape of the whole system — a stack reads defaults it can never touch.

A stack does not own its defaults and cannot change them: it points at a holder, and the holder owns one DataComponentMap shared by every stack of that item in both programs (a reload rebinds it, and a stack made before keeps the map it was made with). What the patch is made of, and why copying a stack is cheap, belong to data components.

The holder field the figure calls item is read through ItemStack.typeHolder, and it answers Items.AIR’s holder rather than null for an empty stack, which is why ItemStack.getItem never returns null either. The pop time — ItemStack.popTime — is the odd one out: it is the five-tick squeeze the hotbar icon does when something lands in it, set to 5 by Inventory when a stack grows or a damaged item lands in an empty slot, and by ClientPacketListener.handleContainerSetSlot when a slot update makes a hotbar stack larger, counted down by ItemStack.inventoryTick on both sides, and read by Hud.extractSlot, which scales the icon while it is above zero and hands the drawing to GuiGraphicsExtractor either way. It is ordinary shared state that only the client has any use for — and ItemStack.copy carries it across, so the animation survives being copied into a menu slot.

Item.Properties is the builder used at class-init, and its output is not a component map. Item.Properties.component and every convenience over it — Item.Properties.stacksTo, Item.Properties.durability, Item.Properties.food, Item.Properties.tool, Item.Properties.spear, Item.Properties.equippable, Item.Properties.useCooldown — fold one more step onto a DataComponentInitializers.Initializer, a function that will be run against a DataComponentMap.Builder later, with a HolderLookup.Provider in hand. The ones that make a weapon are a different seven — Item.Properties.tool, Item.Properties.spear and five named for what they make — and between them they build forty-two items out of a registry of over a thousand (the weapon helpers).

Between the two halves of that arrangement an Item is a live object with no components at all: the constructor registers an initializer and the map is not built until a reload builds it, so Item.components throws until then and Item.CODEC_WITH_BOUND_COMPONENTS exists to refuse an item whose components are not bound yet. When and on which thread that happens, on the server and on a joining client alike, is data components.

What copying a stack costs, and what crosses the wire

Two properties of that borrowed map do work on the rest of this page, and both belong to data components. The patch is sanitised on every write, so it can never hold a value the item already had — which is what makes the equality table below behave. And the patch map is shared until someone writes to it: ItemStack.copy hands out the same map and sets a copy-on-write flag, and the fork happens on the first mutation. Copying a stack is therefore something the game does without thinking about it, and it does — menus, recipes and ServerPlayerGameMode.destroyBlock all copy constantly, and each copy allocates two small objects and no component data.

The wire form falls out of the same shape and is the second of the four serialisations codecs, NBT and JSON lays side by side: ItemStack.OPTIONAL_STREAM_CODEC writes the count, the item holder and the patch, never the prototype, because the receiver has the same prototype bound to the same holder. A count of zero is the whole encoding of an empty stack.

When two stacks are the same stack

Five static methods on ItemStack answer five different versions of that question, and menus, recipes and the first-person hand each want a different one.

methodcomparesused for
ItemStack.isSameItemthe item holder onlyis this the same kind of thing
ItemStack.isSameItemSameComponentsthe item, then the whole PatchedDataComponentMapstacking, and every are these interchangeable test
ItemStack.matchesthe count as wellcontainer synchronisation (containers and menus)
ItemStack.matchesIgnoringComponentseverything except the component types a predicate excusesthe held-item swap animation
ItemStack.hashItemAndComponentsthe item’s hash and the effective component map’skeying stacks in hash sets

The second row is where the borrowed map pays off. Comparing two stacks of the same item amounts to comparing their patches, and because a patch never holds a value equal to the prototype’s, two pickaxes with the same damage are equal whether one reached that state by being set explicitly or by never being touched.

The fourth row exists for one component. DataComponents.DAMAGE is the only component type in the game declared with DataComponentType.Builder.ignoreSwapAnimation, and FirstPersonHandsAndItems.shouldInstantlyReplaceVisibleItem passes exactly that flag as the predicate — which is why a pickaxe losing a point of durability does not re-play the lower-and-raise animation. And a trap sits under all five rows: ItemStack.EMPTY is a singleton but is not identified by reference, because ItemStack.isEmpty also answers true for Items.AIR and for any count at or below zero.

Two validators, one rule, two spellings

Durability and stackability are mutually exclusive, and the game says so twice — in different places, at different times, against different components.

the reload validatorItemStack.validateStrict
installed byItem.Properties.finalizeInitializer—
rejectsDataComponents.DAMAGE on a stackable itemDataComponents.MAX_DAMAGE on a stackable item, and a count over the maximum
runs insideDataComponentMap.Builder.buildItemInput, ItemStack.applyComponentsAndValidate, and ItemStackTemplate.create and ItemStackTemplate.apply through one private step they share
whenat reload, on the worker pool (a joining client’s on the Render thread)when a command, a template or a component patch builds a stack
on failurethrows, failing the reloaddepends on the caller: ItemInput throws a command syntax error, the other two log and yield ItemStack.EMPTY or restore the previous patch

Neither is reached from a network decode. Exactly one serverbound packet in the protocol carries an ItemStack at all, the creative slot’s, and it is proved a third way — re-encoded rather than validated (codecs, NBT and JSON).

The strict validator reaches one level into a stack’s contents and no further. ItemStack.validateContainedItemSizes runs inside ItemStack.validateStrict over DataComponents.CONTAINER, DataComponents.BUNDLE_CONTENTS and DataComponents.CHARGED_PROJECTILES, checking each contained stack’s count against its own maximum — and it does not re-run the full validation there, so nesting is not followed; a shulker box full of impossible stacks is caught by a command, and not at the creative slot’s door, which never runs the strict validator at all. The bundle is the one that gets a second test of its own: a BundleContents whose weight cannot be computed fails too, though one merely over its capacity passes.

An item, a count, some components — said three ways

ItemInstance is the read-only contract the contents check is written against: ItemInstance.count, ItemInstance.getMaxStackSize, and — through TypedInstance and DataComponentGetter — five TypedInstance.is overloads for tags, holder sets, raw items, holders and resource keys, to which ItemStack.is adds a sixth taking a predicate. Its default ItemInstance.getMaxStackSize answers 1 when DataComponents.MAX_STACK_SIZE is missing — which for a bound item never happens, because the common set puts 64 there.

Two classes implement it. ItemStack is the mutable one that lives in slots. ItemStackTemplate is an immutable record of a Holder<Item>, a count and a raw DataComponentPatch — one the template itself never sanitises against a prototype, so a template read from data can carry a component value equal to the item’s own default, which no ItemStack can. It is what a stack becomes when it is stored inside something else: ItemContainerContents (so a shulker box’s contents are templates, not stacks), BundleContents, ChargedProjectiles, ItemParticleOption, HoverEvent, UseRemainder, the recipe classes, and Item’s own crafting remainder. Its constructor refuses a count of zero or Items.AIR, so there is no empty template; but ItemStackTemplate.create and ItemStackTemplate.apply, which materialise a real stack, run ItemStack.validateStrict on the result and answer ItemStack.EMPTY with a log line rather than throwing.

A pickaxe’s last point of durability

Durability is not one component: ItemStack.isDamageableItem demands DataComponents.MAX_DAMAGE present, DataComponents.UNBREAKABLE absent, and DataComponents.DAMAGE present. Take a diamond pickaxe one block short of breaking, and mine that block.

ServerPlayerGameMode.destroyBlock copies the held stack before touching it — that copy is what Block.playerDestroy later hands the loot table, so the drops are decided by the tool as it was — then calls ItemStack.mineBlock, which delegates to Item.mineBlock and awards Stats.ITEM_USED if the item claims the block. The base Item.mineBlock is pure component work: it reads DataComponents.TOOL, does nothing without one, and damages the stack by Tool.damagePerBlock only on a server level, only when that number is above zero, and only when the block’s destroy speed is not zero — so instant-break plants cost a tool nothing, and neither does a tool that declares no per-block damage.

ItemStack.hurtAndBreak is the way in for almost everything, and the overload that does the work demands a ServerLevel outright. The overloads taking a LivingEntity pattern-match on the entity’s level and silently do nothing on the client, which is why a client never predicts the durability an item spends. The amount then goes through EnchantmentHelper.processDurabilityChange (enchantments), which is how Unbreaking turns a point of damage into no damage at all, and a player with Player.hasInfiniteMaterials short-circuits to zero before even that. If anything survives, the stack fires CriteriaTriggers.ITEM_DURABILITY_CHANGED for a real player, writes the new DataComponents.DAMAGE, and — this being the last point — finds ItemStack.isBroken true, shrinks itself by one, and calls the break hook it was handed. For equipment that hook is LivingEntity.onEquippedItemBroken, which strips the item’s attribute modifiers and broadcasts an entity event (47 for the main hand); ServerPlayer.onEquippedItemBroken adds Stats.ITEM_BROKEN on top.

What the client is told, and what it draws

The client is told in one byte. LivingEntity.handleEntityEvent turns event 47 into LivingEntity.breakItem, which plays DataComponents.BREAK_SOUND from the stack still in that slot and spawns five item particles; the empty slot itself arrives separately, as a container update. Two siblings round the family out: ItemStack.hurtWithoutBreaking clamps one short of the maximum, and ItemStack.hurtAndConvertOnBreak transmutes rather than vanishing.

What the player watched was three methods on Item. Item.isBarVisible is is this stack damaged, Item.getBarWidth scales the damage over the thirteen pixels Item.MAX_BAR_WIDTH names, and Item.getBarColor sweeps a hue from green to red. GuiGraphicsExtractor draws the two-pixel bar under the icon from those three answers and nothing else.

All three read the client’s own copy of the stack, and the client never wrote to it. So the bar does not creep as you mine: it sits at whatever the last slot update said, and jumps when the next one arrives. The one point of durability you just spent is invisible until the server sends the slot back.

The tick a stack gets, and the stack that is an entity

ItemStack.inventoryTick runs on both sides and does exactly one thing there: decrement the pop time. It forwards to Item.inventoryTick only for a ServerLevel — the hook’s parameter is declared as one, so it cannot be otherwise — and exactly two items override that hook, CompassItem and MapItem. It has two callers: Inventory.tick, from Player.aiStep, walks the thirty-six ordinary slots and tells the selected one it is the main hand; EntityEquipment.tick, from LivingEntity.aiStep, walks the worn and held slots of every living entity. A stack that leaves an inventory altogether becomes an ItemEntity, which keeps it in a synched data entry (synched entity data) rather than a plain field, counts up to a 6000-tick lifetime, and folds itself into neighbours through ItemEntity.mergeWithNeighbours — a merge that keeps the smaller of the two ages, so a fresh drop rejuvenates an old one. Blocks reach it through Block.popResource (block breaking).

The seventy-nine classes, and why sixty-seven of them are almost empty

world/item’s own directory holds seventy-nine classes beside its records, enums and interfaces. Two of them are Item and ItemStack and ten are helpers such as Items and ItemCooldowns; the other sixty-seven are one Item subclass each, and all but two exist for the same reason — a behaviour hook that no component can express (BannerItem adds only an accessor, and ProjectileWeaponItem is the bow’s and the crossbow’s shared base). BoneMealItem, HoneycombItem, EnderEyeItem, DebugStickItem, LeadItem and thirty-four more override Item.use, Item.useOn or Item.interactLivingEntity and hold no state of their own. Axes, shovels and hoes are plain Items: stripping, path-making and tilling are one component, DataComponents.BLOCK_TRANSFORMER, that the base Item.useOn reads and runs. That is why a registry of over a thousand items has so few classes behind it (the class hierarchy).

What this page hands off

Everything a stack does when a player holds down the use key — the prediction, the countdown, the consumable and cooldown components, the completion packet — is the next lecture: using an item. How two machines agree about a set of stacks in a screen is containers and menus; the component system itself is data components, catalogued in the components reference. And how an item picks the model, texture and tint you see in the slot is not this part’s subject at all: it is Part XI’s, in models and atlases.

For a 1.21-era reader. AxeItem, ShovelItem and HoeItem are gone: DataComponents.BLOCK_TRANSFORMER does their work, naming a BlockTransformer from a data registry. ItemInHandRenderer is gone too; FirstPersonHandsAndItems decides the swap animation.

Where to look

Open ItemStack first and read its four fields; everything else on this page is a consequence of them. PatchedDataComponentMap beside it is where the copy-on-write flag and the sanitising live, and it is the shortest way to see why two pickaxes with the same damage are equal.

Then Item and Item.Properties, in that order, for the gap between a live item and its components: the builder’s output is a DataComponentInitializers.Initializer, and where it eventually lands is Holder.Reference in BuiltInRegistries.ITEM. Items is the roll call of what was built.

For the durability trace, ServerPlayerGameMode.destroyBlock is the entry point and ItemStack.hurtAndBreak is where it ends up. For the immutable twin, ItemStackTemplate; for the contract both stacks answer, ItemInstance. ItemEntity is the stack that left the inventory, and Inventory.tick and EntityEquipment.tick are the two walks that tick the stacks that did not.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Using an item

Verified against Minecraft 26.3 · Part VII · A player holds the use key on a piece of cooked beef, then holds it on a bow — one countdown, two endings.

You hold the use button on cooked beef and thirty-two ticks later you have eaten it. You hold the same button on a bow and nothing happens at all until you let go. These are the same machine: Item.use starts both, LivingEntity.useItemRemaining counts down on the client and the server for both, ItemStack.onUseTick runs every tick on both sides, and LivingEntity.stopUsingItem ends both. What differs is whether the countdown ever reaches zero, and in play only the meal’s does: a meal is thirty-two ticks long and a bow is set to 72000, an hour. And the client’s countdown does not stop at zero on either. The meal ends because one byte arrives from the server — a ClientboundEntityEventPacket carrying EntityEvent.USE_ITEM_COMPLETE, which is the byte 9, the number this page uses for it from here on. The bow’s shot lives only in its release, so it is fired by a ServerboundPlayerActionPacket instead: the same packet, carrying the same action, that the meal would read as the player changed their mind.

The cast

classwhat it decidesthread
Minecraftwhich key edge starts a use, and which one ends itRender
MultiPlayerGameModethe client’s own copy of the use, and the packet that reports itRender
ServerGamePacketListenerImplwhich handler each packet reaches, and whether the rotation is snapped firstServer
LivingEntitythe countdown, the synched flags, and both endingsboth
ItemStackthe dispatch into the item, and the after-use side effectsboth
ItemItem.use, Item.getUseDuration, Item.releaseUsing, Item.useOnReleaseboth
Consumableeverything the meal does, on both sidesboth
ProjectileWeaponItemammo, spread and the arrow — the counting on both sides, the spawning only on the serverboth, but only the server’s counts

The block-target branch of a use — right-clicking a chest rather than the air — leaves at MultiPlayerGameMode.useItemOn and belongs to block interaction; the stack itself is items and stacks.

The two paths, side by side

the mealthe bow
what Item.use doesthe default body finds DataComponents.CONSUMABLE and calls Consumable.startConsumingBowItem.use overrides it, checks Player.getProjectile, calls LivingEntity.startUsingItem
the refusalConsumable.canConsume asks Player.canEat, for food onlyno arrow anywhere and no infinite materials — InteractionResult.FAIL
Item.getUseDurationConsumable.consumeTicks, 32 for Consumables.DEFAULT_FOOD72000 — an hour, and BowItem’s own override
what ItemStack.onUseTick doesparticles and the chew sound, on both sidesnothing — BowItem does not override Item.onUseTick
how it endsthe count reaches zero on the serverthe use key comes up on the client
the packet that ends itClientboundEntityEventPacket, downwardServerboundPlayerActionPacket, upward
is the ending answeredyes — that is the answerno, nothing answers a release
Item.releaseUsingnot overridden, returns false, so letting go simply abandons the mealBowItem.releaseUsing is where the whole shot lives
ItemStack.useOnReleasefalsealso false
what the client predictsthe start and every tick of the meal, then the ending replayed when event 9 arrivesthe animation, and the release on its own copy, which shoots nothing

ItemStack.useOnRelease is not the row that decides it

The ItemStack.useOnRelease row is the one that looks wrong. It is the obvious name for “this item is finished by letting go” — but it only delegates, and the hook it delegates to, Item.useOnRelease, has exactly one override in the tree, CrossbowItem’s. The bow and the trident take the default false. They are release-ended not because a predicate says so, but because their Item.getUseDuration is an hour long and their Item.releaseUsing does the work. The spyglass takes the default too and is not release-ended at all: at 1200 ticks its countdown really can run out.

What the meal’s component holds, and what it does not

Consumable (DataComponents.CONSUMABLE) is the component the meal column hangs off, and it holds the whole of the use half: Consumable.consumeSeconds, with Consumable.consumeTicks derived from it; the ItemUseAnimation the arm plays; the sound; the particles; and a list of ConsumeEffects to apply at the end — ApplyStatusEffectsConsumeEffect, RemoveStatusEffectsConsumeEffect, ClearAllStatusEffectsConsumeEffect, TeleportRandomlyConsumeEffect and PlaySoundConsumeEffect, whose types are registered in BuiltInRegistries.CONSUME_EFFECT_TYPE. What it does not hold is what the food is worth, which is a second component and Part VIII’s (hunger and experience).

Where the hour comes from

Duration is one base method and eight overrides. Item.getUseDuration’s base body reads Consumable.consumeTicks off the stack if there is a Consumable on it, and otherwise answers the hour for anything carrying DataComponents.BLOCKS_ATTACKS or DataComponents.KINETIC_WEAPON — so a shield and a spear get their long draw from the base method and no override of their own. BowItem, CrossbowItem and TridentItem override it to return the same 72000, the number Item.APPROXIMATELY_INFINITE_USE_DURATION names. At the other end EnderEyeItem returns zero, which makes it the one item in the game whose use is instant by declaration rather than by having no Consumable at all. The remaining four overrides are ordinary finite numbers: the spyglass’s 1200, two hundred each for BrushItem and BundleItem, and InstrumentItem’s, which is the goat horn’s own sounded length in ticks.

Starting: the client finishes before it speaks

Minecraft.handleKeybinds sees the press and calls Minecraft.startUseItem, which sets the four-tick Minecraft.rightClickDelay itself, refuses while LocalPlayer.isHandsBusy, and walks both hands for a target. With nothing under the crosshair it reaches MultiPlayerGameMode.useItem, which, once any pending hotbar change has been sent, opens a prediction window (MultiPlayerGameMode.startPrediction, prediction and acks) and does the rest inside it: it builds the ServerboundUseItemPacket — hand, sequence number and both rotations — then consults the client’s own ItemCooldowns, and runs ItemStack.use locally first only if the item is off cooldown. The packet goes up either way; the cooldown suppresses the prediction, not the report of it. The prediction is complete before the use packet leaves the client, for the meal and the bow alike. Both answer InteractionResult.CONSUME, whose InteractionResult.SwingSource.NONE is why neither swings the arm, although FirstPersonHandsAndItems.itemUsed still runs — the small dip the item makes as the use begins. An instant use, one whose ItemStack.getUseDuration is zero, returns its outcome the other way instead, through InteractionResult.Success.heldItemTransformedTo, which both game modes unwrap and write back into the hand.

On the server, ServerGamePacketListenerImpl.handleUseItem acknowledges the sequence number, snaps the player’s rotation to the one in the packet, and calls ServerPlayerGameMode.useItem. That snap is what makes the release strange later, because the release packet does no such thing. And ServerPlayerGameMode.useItem ends on a deliberate omission: it normally re-sends the player’s inventory with AbstractContainerMenu.sendAllDataToRemote (containers and menus), but not when the use it just ran started a multi-tick one. While you are eating or drawing, the server declines to correct your inventory.

While it runs: the flag on the wire and the flag the client believes

LivingEntity.startUsingItem writes two bits of LivingEntity.DATA_LIVING_ENTITY_FLAGS (synched entity data) — bit one for using, bit two assigned the hand, so a main-hand use clears it — and only on the server. LivingEntity.isUsingItem and LivingEntity.getUsedItemHand read those bits, which is how every other client knows your arm is up.

Your own client does not read them. LocalPlayer.isUsingItem overrides the base and returns a private local flag set by LocalPlayer.startUsingItem; LocalPlayer.getUsedItemHand likewise answers from a local field. Reconciliation happens afterwards, in both directions: LocalPlayer.onSyncedDataUpdated compares the arriving bits with the local flag and will start a use the client never predicted, or stop one it did. The base LivingEntity.onSyncedDataUpdated makes the matching repair on every other entity’s copy — it adopts the held stack and re-derives LivingEntity.useItemRemaining from ItemStack.getUseDuration, which is why a remote player drawing a bow animates correctly although your client never saw the press.

One abandonment rule is shared, and it lives a level above the countdown. LivingEntity.tick calls the private LivingEntity.updatingUsingItem, which compares the hand’s current stack with the remembered one using ItemStack.isSameItem and calls LivingEntity.stopUsingItem if they differ; only on a match does it call LivingEntity.updateUsingItem, the countdown proper. The comparison is item identity, not components, so swapping a bowl for a stew aborts the meal while a durability tick on the bow does not abort the draw.

Every tick, and what the bow does instead

LivingEntity.updateUsingItem offers ItemStack.onUseTick the count before it decrements, so on the server a thirty-two-tick meal is offered 32 down to 1 and never 0; the client, waiting for its byte, goes on to 0 and below. For the meal that one call is the whole visible experience: Consumable.shouldEmitParticlesAndSounds is true once more than Consumable.CONSUME_EFFECTS_START_FRACTION of the duration has elapsed and the remaining count is a multiple of Consumable.CONSUME_EFFECTS_INTERVAL, and Consumable.emitParticlesAndSounds then spawns five item particles through LivingEntity.spawnItemParticles — behind Consumable.hasConsumeParticles, which drinks turn off — and plays the chew sound, which they do not. Level.addParticle does nothing on the server, so the crumbs are pure client simulation.

For the bow the call does nothing whatever: BowItem does not override Item.onUseTick, and the base body is empty. Everything you see while drawing is the renderer reading the same counter the logic is decrementing. FirstPersonHandsAndItemsRenderer computes the draw curve for ItemUseAnimation.BOW from a per-frame copy of LivingEntity.getUseItemRemainingTicks, and the three-stage bow texture is not code at all: items/bow.json is a condition on using_item wrapping a range_dispatch on the use_duration property (UseDuration), scaled so its thresholds are fractions of BowItem.MAX_DRAW_DURATION.

The crossbow is the exception that makes the rule legible. It does override Item.onUseTick, and that body is entirely server-side: it plays the start, middle and end sounds a CrossbowItem.ChargingSounds holds (Quick Charge’s has no middle) at fixed fractions of CrossbowItem.getChargeDuration and, on reaching one, writes DataComponents.CHARGED_PROJECTILES onto the stack. Its client half is a Render-thread computation — CrossbowPull and FirstPersonHandsAndItems both call CrossbowItem.getChargeDuration, which calls EnchantmentHelper.modifyCrossbowChargingTime (enchantments owns the two enchantment values a client is allowed to compute). An enchantment hook, evaluated on the Render thread every frame, to pose an arm and pick one of three textures.

Moving while you use

Neither path slows you through movement code — both read one component. LocalPlayer.modifyInput scales the movement input by LocalPlayer.itemUseSpeedMultiplier, which reads UseEffects.speedMultiplier off the stack, unless the player is riding, and LocalPlayer.isSlowDueToUsingItem blocks sprinting because UseEffects.canSprint is false. The number is a multiplier and not a subtraction: UseEffects.DEFAULT carries 0.2, so the input is scaled to a fifth of what you pressed. That default sits in DataComponents.COMMON_ITEM_COMPONENTS, so every item has one — and neither cooked beef nor the bow overrides it, which is why drawing a bow slows you by exactly as much as eating does, through exactly the same field. Item.Properties.spear is the definition that overrides it outright, with a UseEffects that permits sprinting, suppresses vibrations and multiplies speed by one; the hits that use makes need no packet at all, coming from the server’s use tick itself (the spear).

The component’s remaining field is not about movement at all, and it is the half that runs on the server. UseEffects.interactVibrations is what ItemStack.causeUseVibration consults before letting a use emit a game event, so an item whose component clears the flag starts and finishes silently as far as a sculk sensor is concerned. LivingEntity calls it at both ends of a use — GameEvent.ITEM_INTERACT_START when the timer opens and GameEvent.ITEM_INTERACT_FINISH when it closes — and FishingRodItem and BoneMealItem call it for themselves, because neither ends through the use timer (game events and vibrations).

The two endings

There are two, and they are separate machines that share a last step. The first is the countdown reaching zero, which is the meal.

flowchart TD
    T["LivingEntity.updateUsingItem, both sides, every tick"]
    T --> A["ItemStack.onUseTick, with the count before the decrement"]
    A --> B["decrement LivingEntity.useItemRemaining"]
    B --> C{"reached zero"}
    C -- no --> T
    C -- yes --> D{"on the server"}
    D -- "no, this is the client" --> W["keep counting into the negatives and wait"]:::client
    D -- yes --> E{"ItemStack.useOnRelease"}
    E -- "true, a crossbow" --> T
    E -- "false, everything else" --> F["LivingEntity.completeUsingItem"]:::server
    F --> G["ServerPlayer.completeUsingItem sends ClientboundEntityEventPacket 9 first"]:::server
    G --> H["ItemStack.finishUsingItem, then LivingEntity.stopUsingItem"]:::server
    G -. "the byte lands" .-> X["Player.handleEntityEvent replays LivingEntity.completeUsingItem"]:::client
    W --> X

The client’s arm of the branch reaches nothing on its own. The dotted edge is the whole of what ends a meal on the client — a byte from the server, arriving at a branch that was otherwise waiting for ever.

The second is the key coming up, which is the bow, and nothing in it waits for a reply at all.

flowchart TD
    R["the use key comes up, Minecraft.handleKeybinds"]:::client
    R --> S["MultiPlayerGameMode.releaseUsingItem sends RELEASE_USE_ITEM"]:::client
    S --> P["ServerGamePacketListenerImpl.handlePlayerAction, no ack, no sequence"]:::server
    S --> Q2["LivingEntity.releaseUsingItem on the client's own copy"]:::client
    P --> Q["LivingEntity.releaseUsingItem on the server's"]:::server
    Q --> K["ItemStack.releaseUsing, then LivingEntity.stopUsingItem either way"]
    Q2 --> K
    K -- "Item.releaseUsing returned true" --> L["the shot, then the after-use side effects"]
    K -- "returned false" --> M["the meal is simply abandoned"]

One method run twice, once per copy, and the split at the top is the point — the client releases locally without waiting, and only the server’s run reaches a ServerLevel and so an arrow.

The client’s branch has no exit. Nothing on the client ever reaches LivingEntity.completeUsingItem from the countdown — it is called from Player.handleEntityEvent when event 9 arrives, and nowhere else on that side. The counter meanwhile keeps falling past zero and only the renderer notices: FirstPersonHandsAndItemsRenderer draws a use pose solely while LivingEntity.getUseItemRemainingTicks is positive, so the arm drops at tick 32 whether or not the packet has landed.

And ItemStack.useOnRelease does not mean “ends on release”. It means do not let the countdown finish this, and give it one more tick when the key comes up: LivingEntity.releaseUsingItem calls LivingEntity.updatingUsingItem again when it is true, so a crossbow gets a final CrossbowItem.onUseTick in which it can still latch the charge. No other item asks for that. Release is also not only a key-up: besides the client’s and the server’s key-up paths, LivingEntity.releaseUsingItem is called from five other places — once by LivingEntity.completeUsingItem itself, when the stack turned out not to match the hand; once each by CrossbowAttack and RangedCrossbowAttackGoal, which is how a piglin and a pillager finish loading; and twice by BrushItem.onUseTick, which ends its own use from inside the tick.

The meal, tick by tick

The whole of it is one press, thirty-one ticks in which the meal sends its eater nothing, and one byte, and the three bands below are those three.

sequenceDiagram
    participant LP as LocalPlayer
    participant MPGM as MultiPlayer<br/>GameMode
    participant Wire as the network
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SP as ServerPlayer
    participant Cons as Consumable

    rect rgba(0, 0, 0, 0.04)
        Note over LP,Cons: tick 0, the press
        MPGM->>Cons: startConsuming, reached from Minecraft.startUseItem through Item.use
        Cons->>LP: startUsingItem, once canConsume has asked Player.canEat
        MPGM->>Wire: ServerboundUseItemPacket, hand and sequence and both rotations
        Wire->>SGPL: handleUseItem acks the sequence and snaps the rotation
        SGPL->>SP: the same Item.use, remaining = 32, the two flag bits written
    end
    rect rgba(0, 0, 0, 0.04)
        Note over LP,Cons: ticks 1 to 31, both sides
        LP->>LP: ItemStack.onUseTick, particles and the chew sound once the delay is past
        SP->>SP: the same call, particles discarded, sound broadcast to everyone else
    end
    rect rgba(0, 0, 0, 0.04)
        Note over LP,Cons: tick 32, the count at zero, and only the server ending it
        SP->>Wire: ClientboundEntityEventPacket, EntityEvent.USE_ITEM_COMPLETE
        SP->>Cons: Consumable.onConsume, from ItemStack.finishUsingItem, FoodData.eat inside it
        Wire->>LP: handleEntityEvent replays LivingEntity.completeUsingItem locally
        SP->>Wire: ClientboundSetHealthPacket, same tick, overwrites the prediction
    end
    Note over Wire,SP: a later tick: AbstractContainerMenu.broadcastChanges corrects the count

Three bands, and the count reaches zero in only one of them. Watch the third — the client is told the meal is over by a single byte, and its own countdown had nothing to do with it.

The stack’s own count is the last thing to agree: the menu’s AbstractContainerMenu.broadcastChanges corrects it a tick or more later, which is a container’s business rather than the meal’s.

The replay is the interesting half. Consumable.onConsume runs on both sides and three parts of it do not: the Stats.ITEM_USED award and CriteriaTriggers.CONSUME_ITEM need a ServerPlayer, the ConsumeEffects in Consumable.onConsumeEffects sit behind a server-side guard, and GameEvent.EAT is a no-op because ClientLevel.gameEvent has an empty body. Particles, sound, the ConsumableListener walk that finds FoodProperties, and the ItemStack.consume shrink all run on both — and every one of those client mutations is then overwritten. That is why a chorus fruit’s teleport is never predicted while the hunger bar’s jump is (hunger and experience).

One meal, two exactly-once sound strategies. The chew sound goes through Player.playSound, which on the server names the eater as the entity to exclude, so the server broadcasts it to everyone else; on the eater’s client LocalPlayer.playSound plays it locally without the test. The FoodProperties eat and burp sounds pass no exclusion at all — and ClientLevel.playSeededSound plays a sound only when the excluded entity is the local player — so those reach the eater as the server’s broadcast alone.

The bow, tick by tick

The same lanes, the same opening, and then a release that nobody waits for.

sequenceDiagram
    participant LP as LocalPlayer
    participant MPGM as MultiPlayer<br/>GameMode
    participant Wire as the network
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SP as ServerPlayer

    rect rgba(0, 0, 0, 0.04)
        Note over LP,SP: tick 0, the press
        MPGM->>LP: startUsingItem, remaining = 72000, once BowItem.use has a projectile
        MPGM->>Wire: ServerboundUseItemPacket, hand and sequence and both rotations
        Wire->>SGPL: handleUseItem acks the sequence and snaps the rotation
        SGPL->>SP: startUsingItem too, remaining = 72000, the two flag bits written
    end
    rect rgba(0, 0, 0, 0.04)
        Note over LP,SP: every tick after that, both sides
        LP->>LP: the count falls, onUseTick is empty, the model reads the use duration
        SP->>SP: the count falls, and only the using-item trigger fires
    end
    rect rgba(0, 0, 0, 0.04)
        Note over LP,SP: the tick the key comes up
        MPGM->>Wire: ServerboundPlayerActionPacket, RELEASE_USE_ITEM, sequence zero
        MPGM->>LP: releaseUsingItem, and BowItem.releaseUsing on the client's copy
        LP->>LP: no ServerLevel, so a phantom stack, no ammo, no arrow, then stopUsingItem
        Wire->>SGPL: handlePlayerAction, with whatever rotation the server last heard
        SGPL->>SP: releaseUsingItem, the identical call on the server's copy
        SP->>SP: BowItem.releaseUsing, then<br/>ProjectileWeaponItem.<br/>draw and shoot
        SP->>Wire: Projectile.spawnProjectile, then the add-entity packet
        SGPL-->>MPGM: nothing acknowledges the release itself
    end
    Note over Wire,SP: a later tick: the container sync corrects the arrows and the damage

The same method twice, once per copy — the two self-messages in the third band are BowItem.releaseUsing on each machine, and only the lower one has a ServerLevel to shoot into. The dashed line is the only reply, and it is empty.

BowItem.releaseUsing runs on both sides and gets nowhere on one of them. It measures the draw as BowItem.getUseDuration minus the remaining count, puts it through BowItem.getPowerForTime — a curve on the draw time in seconds, clamped at one after BowItem.MAX_DRAW_DURATION ticks — and returns false below a tenth of full power, which is why a tap of the button neither shoots nor costs durability. Above it, ProjectileWeaponItem.draw decides how many arrows leave the string (EnchantmentHelper.processProjectileCount) and ProjectileWeaponItem.useAmmo decides whether an arrow is spent (EnchantmentHelper.processAmmoUse). Both consult a ServerLevel and fall back to one and zero otherwise, and ProjectileWeaponItem.shoot is itself inside a ServerLevel test — so on the client the draw produces a single phantom arrow marked DataComponents.INTANGIBLE_PROJECTILE, spends nothing and shoots nothing.

On the server it is six enchantment hooks and one ordering worth remembering. Two have already run inside ProjectileWeaponItem.draw; the third, EnchantmentHelper.processProjectileSpread, fans a multishot volley, and each arrow is aimed by BowItem.shootProjectile through Projectile.shootFromRotation using the server’s rotation — which the release packet never updated, so the shot goes where the last movement packet said you were looking. Projectile.spawnProjectile aims, adds the entity to the level, and only afterwards calls Projectile.applyOnProjectileSpawned, which runs EnchantmentHelper.onProjectileSpawned twice when ammo and weapon are different items: once for the arrow’s stack and once for the bow’s. The fifth is out of sight in the arrow itself — the AbstractArrow constructor asks EnchantmentHelper.getPiercingCount off the weapon it was fired from (enchantments). ItemStack.hurtAndBreak takes the durability after each arrow, through the sixth, EnchantmentHelper.processDurabilityChange, and the volley breaks off if the bow dies mid-flight.

Only when Item.releaseUsing returns true does ItemStack.releaseUsing run ItemStack.applyAfterUseComponentSideEffects — the same private step ItemStack.finishUsingItem runs for the meal, converting DataComponents.USE_REMAINDER into the empty bowl and starting DataComponents.USE_COOLDOWN. (For an instant use it runs from ItemStack.use instead, and only for a zero-duration success.) Cooldowns are grouped rather than per-item: ItemCooldowns.getCooldownGroup returns UseCooldown.cooldownGroup when the component names one and the item’s Identifier otherwise, both sides own an ItemCooldowns — the client’s is a real prediction, consulted before it will even predict a use — and ServerItemCooldowns.onCooldownStarted mirrors the server’s as one ClientboundCooldownPacket naming a group.

What the ending never carries

The release is the least-answered packet in the pipeline. ServerGamePacketListenerImpl.handlePlayerAction treats ServerboundPlayerActionPacket.Action.RELEASE_USE_ITEM in one line — call LivingEntity.releaseUsingItem, return — with no ack, no sequence number consumed, not even a spectator check. Everything the client learns about its own shot arrives as ordinary world traffic. The arrow is the quick one: adding it to the level starts its tracking inside the same call, so the spawn packet leaves on that tick. The spent ammo and the bow’s damage wait for a container slot update (containers and menus) and the cleared using-flag for entity data it has already acted on, both a tick or more later. The shoot sound is broadcast with no exclusion, so — by the same rule as the burp — the shooter hears the server’s copy and never their own.

The completion is barely richer. There is no you ate this packet: event 9 tells the client to re-derive the outcome from components it already holds, and ClientboundSetHealthPacket corrects the hunger it got wrong, the container sync the count. The single override of Item.finishUsingItem in the whole tree is SpyglassItem.finishUsingItem, which plays a sound — the spyglass being the one item whose completion is worth a sound of its own.

That override is reached only by the countdown running out, all 1200 ticks of it. Letting go takes the other door: SpyglassItem also overrides Item.releaseUsing — the fourth and last override of it in the tree, after the bow, the crossbow and the trident — plays the same sound there through the same private SpyglassItem.stopUsing, and returns true. So the spyglass gets its sound either way, through two overrides that meet only in that helper. That is the last thing this page has to say about the two endings: they are not one path with a switch on it. They are two.

For a 1.21-era reader. There is no bow-pull item property class left to hunt for. The old pulling / pull pair is now the shared UseDuration range-select property plus a using_item condition, both declared in the item’s JSON. The crossbow keeps a bespoke property, CrossbowPull, only because its denominator is enchantable. ItemInHandRenderer is gone; FirstPersonHandsAndItems and FirstPersonHandsAndItemsRenderer do its work.

Where to look

LivingEntity is the whole machine, and four of its methods in this order are the page: LivingEntity.startUsingItem, the private LivingEntity.updatingUsingItem that decides whether to keep going, LivingEntity.updateUsingItem that counts, and LivingEntity.releaseUsingItem for the other ending. LivingEntity.completeUsingItem beside them is the one the client never reaches from its own countdown.

For where a use begins, read down: Minecraft.startUseItem, then MultiPlayerGameMode.useItem for the prediction window, then ServerGamePacketListenerImpl.handleUseItem for the rotation snap, then ServerPlayerGameMode.useItem for the inventory re-send it declines to do. ItemStack.use is where both sides dispatch into the item.

Consumable is the meal in one class. BowItem is the bow, and ProjectileWeaponItem above it holds the two methods that decide how many arrows leave and whether any is spent; CrossbowItem is the one ranged weapon that uses the tick hook and the one item that sets ItemStack.useOnRelease. UseEffects shapes the use while it runs and UseCooldown outlives it, and FirstPersonHandsAndItems and FirstPersonHandsAndItemsRenderer between them are everything you see.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Containers and menus

Verified against Minecraft 26.3 · Part VII · A player shift-clicks a stack out of a chest: one packet goes up, the server re-runs the same code against the real container, and nothing comes back down.

A stack of cobblestone sits in the top-left slot of a chest, and a shift-click lands it in the hotbar before anything has answered. One ServerboundContainerClickPacket leaves for the server; the server runs the same method against the real ChestBlockEntity, gets its own answer, compares the two — and if they agree, sends nothing at all. That is the steady state: one packet up, zero packets down. There is no transaction acknowledgement in this protocol; agreement is silence. What makes silence safe is the part nobody expects. The click packet does not carry the stacks the client thinks it produced; it carries each changed slot as an item, a count and a CRC32C hash per component its patch adds, and the server writes those claims into its own per-slot record of what the client believes before it compares any slot. The record is the server’s, and what a click is allowed to fill it with is a hash and never a stack.

The cast

classwhat it decidesthread
Containerstorage, and nothing about who is looking at it — Container.getItem, Container.setItem, Container.stillValidwherever its owner runs
AbstractContainerMenuthe slot list, the cursor, the state id, the click path, and two independent records of what has already been reported — one for the advancement listeners, one for the wireServer and Render
SlotGUI policy — Slot.mayPlace, Slot.mayPickup, Slot.getMaxStackSize — and the guarded mutations a click goes throughServer and Render
Inventorythe player’s own storage, present as slots in nearly every menu that opens — the lectern’s one book slot is the exceptionServer and Render
MenuType / MenuProviderthe registry entry with the screen-side constructor, and the server-side factory a block hands to ServerPlayer.openMenuRender / Server
ContainerSynchronizerthe channel from a menu’s diff to the connection, and the writer of every slot update a click causes but one — one per ServerPlayer, shared by every menu that player opensServer
RemoteSlotwhat the server believes the client is holding in one slot, as either a stack or a hashServer (RemoteSlot.PLACEHOLDER on the Render thread)
HashedStackthe client’s claim about one slot after its own clickcreated on the client, matched on the server

The chest you see is not the chest

The server’s ChestMenu is built from a MenuProvider that ChestBlock.getMenuProvider produces and ChestBlock.useWithoutItem hands to ServerPlayer.openMenu (block interaction), which is a fixed sequence: close whatever menu was already open, take the next container id, ask the provider for a menu, send ClientboundOpenScreenPacket, and then ServerPlayer.initMenu — which attaches the listener and the synchronizer, and whose attaching of a synchronizer is what sends the single ClientboundContainerSetContentPacket carrying the contents. The screen arrives before the contents do, and both arrive after the menu object exists on the server. Its Container is the real ChestBlockEntity — or a CompoundContainer over both halves, for a double chest, from a separate anonymous provider that unpacks both halves’ loot tables itself. The client’s ChestMenu is built from MenuType’s screen-side constructor by MenuScreens, and that constructor makes a fresh SimpleContainer. The client’s chest contents have no connection to the block entity beyond the packet stream. Its AbstractContainerMenu.stillValid would be a lie for the same reason — SimpleContainer.stillValid returns true unconditionally — but the question is never put to it: every call site of AbstractContainerMenu.stillValid in the game is on the server. A client never closes its own menu because it walked away.

Three asymmetries between the two copies

The second asymmetry, after the fresh container above, is the synchronizer. Only ServerPlayer.initMenu ever calls AbstractContainerMenu.setSynchronizer, so a client menu has none, and every one of its RemoteSlots stays RemoteSlot.PLACEHOLDER, whose RemoteSlot.matches always answers true, so the wire half of AbstractContainerMenu.broadcastChanges is inert on the client by construction. The listener half is not: BeaconScreen, ItemCombinerScreen, LecternScreen and the creative inventory all register a real ContainerListener, which is how the anvil’s rename field repopulates itself after a slot changes.

The last of the asymmetries is the one the other pages of this part lean on. A block-anchored menu reaches the world through a ContainerLevelAccess, and the client’s copy holds ContainerLevelAccess.NULL, whose ContainerLevelAccess.evaluate runs nothing and returns an empty optional, so its ContainerLevelAccess.execute runs nothing either. Anything a menu does inside that call — matching a recipe, spending lapis, chipping an anvil — is therefore skipped wholesale on the client, whatever the body itself tests. What the client really runs in those cases is whatever guard sits in front of the call, which is why a client copy of a menu can still refuse a click it cannot afford.

Twenty-eight subclasses, one machine

There are twenty-eight AbstractContainerMenu subclasses in world/inventory, and after this page’s chest, the crafting grid, the anvil and the enchanting table the rest are the same machine with a different slot list, LoomMenu, CartographyTableMenu, BeaconMenu, BrewingStandMenu, CrafterMenu, LecternMenu, DispenserMenu, HopperMenu, ShulkerBoxMenu, MerchantMenu and the two mount menus among them. A station menu is a Container — the block entity’s, or a scratch one the menu builds for itself on each side — a list of Slots carrying the GUI policy, and, for a furnace’s progress or an anvil’s price, a data slot or a ContainerData. ItemCombinerMenuSlotDefinition is the small vocabulary the anvil and the smithing table are laid out from; SlotRanges beside it is not a menu’s at all, but the table of slot names commands and predicates read.

Three smaller facts that will otherwise trip you

A menu with a null MenuType cannot be opened the ordinary way, because ServerPlayer.openMenu asks for its type and AbstractContainerMenu.getType throws. That is why the player’s own InventoryMenu is pinned to InventoryMenu.CONTAINER_ID, zero, and built independently on both sides, and why the two AbstractMountInventoryMenu subclasses — the horse’s and the nautilus’s, which pass a null type up to AbstractContainerMenu explicitly — need a ClientboundMountScreenOpenPacket of their own instead of ClientboundOpenScreenPacket. A chest boat is not one of them: it opens an ordinary ChestMenu through ServerPlayer.openMenu like any other container. Every other menu takes an id from ServerPlayer.nextContainerCounter, which cycles 1 to 100 and so never collides with the player’s own zero.

Slot.mayPlace and Container.canPlaceItem are unrelated questions. The first is GUI policy and defaults to true without consulting the container at all; the second is what hopper automation reads. A player and a hopper can have different rights over the same slot, and neither of them is asking the other’s question.

Container.getMaxStackSize defaults to 99. The familiar 64 comes only from the per-stack overload, which takes the minimum with the item’s own maximum — so a player and an item can disagree about a slot’s capacity the same way a player and a hopper disagree about its contents.

One shift-click, end to end

A three-row ChestMenu numbers slots 0–26 for the chest, 27–53 for the player’s three main inventory rows and 54–62 for the hotbar, because AbstractContainerMenu.addStandardInventorySlots adds the main rows before the hotbar. Watch what the wire carries, and what it does not.

sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant ChestM as ChestMenu
    participant Wire as the network
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant ACM as Abstract<br/>ContainerMenu
    participant RemS as RemoteSlot

    MPGM->>MPGM: copy every slot's stack, before touching anything
    MPGM->>ChestM: clicked with QUICK_MOVE, on the client's own copy
    ChestM->>ChestM: quickMoveStack, then moveItemStackTo backwards
    MPGM->>Wire: ServerboundContainerClickPacket, state id plus changed slots as hashes
    rect rgba(0, 0, 0, 0.04)
        Note over MPGM,RemS: the server tick that drains the packet
        Wire->>SGPL: handleContainerClick
        SGPL->>ACM: suppressRemoteUpdates, then the same clicked on the real chest
        SGPL->>ACM: setRemoteSlotUnsafe per claimed hash, then setRemoteCarried
        ACM->>RemS: receive, dropping any concrete stack it held
        SGPL->>ACM: resumeRemoteUpdates, then broadcastChanges
        ACM->>RemS: matches
        RemS->>RemS: the hash agrees, so it becomes a copy of the server's stack
    end
    Note over Wire,RemS: agreement is silence, and AbstractContainerMenu.synchronizeSlotToRemote sends nothing

One shift-click as one packet up and nothing down. Read the diagonal: the client runs the click, the server re-runs the identical call on the real chest, and the comparison happens only after both — the note at the foot is where a reply would have been.

The press. AbstractContainerScreen.mouseClicked resolves the hovered slot, sees an empty AbstractContainerMenu.getCarried and a held shift, and chooses ContainerInput.QUICK_MOVE on the press. Two near neighbours are release-path instead, from AbstractContainerScreen.mouseReleased: a shift-click with a non-empty cursor, and the shift-double-click sweep, which issues a separate ContainerInput.QUICK_MOVE — and a separate packet — for every matching slot in the same container.

The snapshot, then the prediction. MultiPlayerGameMode.handleContainerInput copies every slot’s stack before touching anything, because that copy is what the packet’s diff is built against. Then it runs AbstractContainerMenu.clicked on the client’s own menu. The ContainerInput.QUICK_MOVE branch loops AbstractContainerMenu.quickMoveStack while the clicked slot keeps yielding the same item, and it runs only for button 0 or 1, a non-negative slot index and a slot that Slot.mayPickup allows.

The move, and its two passes. ChestMenu.quickMoveStack sees the index is inside the chest and calls AbstractContainerMenu.moveItemStackTo over the player’s range with the backwards flag, which scans from the last slot down — the hotbar first, from the right. That method makes two passes. The first runs only if the stack is stackable at all, and merges into every existing compatible stack across the whole range (ItemStack.isSameItemSameComponents, topped up to Slot.getMaxStackSize). The second runs only if something is left, finds the first empty slot that Slot.mayPlace accepts, places what that slot’s cap allows and breaks. One empty slot per call — which is why the caller loops. Note which pass consults policy: the merge pass never asks Slot.mayPlace, so a slot that would refuse the item on placement can still be topped up by a shift-click when it already holds a matching stack. Note also what ChestMenu.quickMoveStack never calls: Slot.onTake. InventoryMenu.quickMoveStack does. Shift-clicking out of a chest and out of the crafting result are structurally different operations.

The packet. The client diffs the post-click slots against its snapshot, hashes each changed stack with HashedStack.create, and sends ServerboundContainerClickPacket with the container id, the client’s last-known state id, the slot, the button, the ContainerInput, up to 128 changed slots as hashes, and the cursor as one more.

The real move. The server takes the ladder below, then — with AbstractContainerMenu.suppressRemoteUpdates held — runs the identical AbstractContainerMenu.clicked path against the real ChestBlockEntity and the real Inventory. Slot.setChanged reaches BlockEntity.setChanged, which calls Level.blockEntityChanged to mark the chunk and Level.updateNeighbourForOutputSignal to re-derive the comparator output (block entities, and diodes and observers for what a comparator does with it). The whole click is wrapped in a try/catch that builds a crash report category naming the menu class, the slot and the button.

Installing the claim, then comparing. AbstractContainerMenu.setRemoteSlotUnsafe writes each hash the client sent into the matching RemoteSlot through RemoteSlot.receive, and RemoteSlot.Synchronized discards any concrete stack it was holding and keeps the hash alone. An out-of-range index is logged at debug and ignored rather than rejected. Then AbstractContainerMenu.setRemoteCarried, then AbstractContainerMenu.resumeRemoteUpdates, then AbstractContainerMenu.broadcastChanges, which makes one pass over the slots offering each to AbstractContainerMenu.triggerSlotListeners for advancements and to AbstractContainerMenu.synchronizeSlotToRemote for the wire, then the cursor, then the data slots. Where the hash agrees, RemoteSlot.Synchronized promotes it to a concrete copy of the server’s own stack and nothing is sent; where it disagrees, that one slot gets a ClientboundContainerSetSlotPacket of its own with a fresh state id.

The advancement channel sees one state per click, not one per slot touched: AbstractContainerMenu.triggerSlotListeners runs only from AbstractContainerMenu.broadcastChanges and AbstractContainerMenu.broadcastFullState — and for a chest nothing calls back into the menu mid-click to reach either, a bundle click aside. That is a fact about the chest, not about menus: several menus call AbstractContainerMenu.slotsChanged from Container.setChanged or Slot.setChanged (the anvil’s and the smithing table’s ItemCombinerMenu containers, the crafter’s CrafterSlot, the grindstone’s, the lectern’s), a bundle click calls it on whatever menu is open, and the base AbstractContainerMenu.slotsChanged is a bare AbstractContainerMenu.broadcastChanges, which the menus that override it reach only if they call up to it. The click’s own broadcast runs after AbstractContainerMenu.resumeRemoteUpdates, so suppression is not in force by then, and ServerPlayer’s ContainerListener filters to slots that are not a ResultSlot and whose container is the player’s own Inventory before firing CriteriaTriggers.INVENTORY_CHANGED anyway.

The ladder the server climbs before it believes you

ServerGamePacketListenerImpl.handleContainerClick is four tests and a fork, and the interesting thing about it is how much of it ends in nothing sent rather than a correction. The packet reaches the handler on the server thread through PacketUtils.ensureRunningOnSameThread, and then:

the testwhat a failure does
does AbstractContainerMenu.containerId match the open menudropped in total silence — nothing logged, nothing sent
is the player a spectator, or dead or dyingAbstractContainerMenu.sendAllDataToRemote, a full resync, and the click never runs
AbstractContainerMenu.stillValidlogged at debug, nothing sent, and the menu is not closed here
AbstractContainerMenu.isValidSlotIndexlogged at debug, nothing sent, nothing corrected

Past all four the fork begins, and its order is the load-bearing part.

flowchart TD
    ST["compare the packet's state id with the menu's, before anything is applied"]:::server
    ST --> AP["AbstractContainerMenu.suppressRemoteUpdates, the click, the hashes, resume"]:::server
    AP --> Q{"was that state id stale?"}
    Q -->|"stale"| FULL["AbstractContainerMenu.broadcastFullState: one set-content packet, fresh state id"]:::server
    Q -->|"current"| BC["AbstractContainerMenu.broadcastChanges: every slot against its RemoteSlot"]:::server

The compare happens above the apply and the branch below it. Read the two edges out of the diamond as the whole difference a stale state id makes — not whether the click runs, only how its result is published.

The state id is compared before the click is applied and acted on after, so a click that quotes a stale id still runs, and still runs first. Its result is simply published wholesale instead of diffed.

AbstractContainerMenu.isValidSlotIndex deserves suspicion. It accepts −1, accepts AbstractContainerMenu.SLOT_CLICKED_OUTSIDE, and otherwise only asks whether the index is below the slot count — so every negative index passes it. Each branch of the click that needs a lower bound therefore tests for one itself. The two that do not, ContainerInput.SWAP and the painting phase of ContainerInput.QUICK_CRAFT, index the list directly, and the click’s own try/catch turns the failure into a ReportedException rather than swallowing it: what swallows it is PacketProcessor, which logs a game-listener error and carries on. An out-of-range click is therefore neither corrected nor fatal, but it is loudly logged, it leaves the menu’s remote updates suppressed until a later click succeeds (the resume the handler would have run next never runs), and it closes nothing: ServerGamePacketListenerImpl.handleContainerClose for its part validates nothing at all, not even the container id, and goes straight to ServerPlayer.doCloseContainer.

Why hashes, and why only in one direction

HashedStack.create produces either HashedStack.EMPTY or a HashedStack.ActualItem of item holder, count and a HashedPatchMap — one CRC32C integer per added component plus the plain set of removed ones. Each integer comes from running the component’s own codec into HashOps.CRC32C_INSTANCE, a DynamicOps whose output is the hash (codecs, NBT and JSON owns that mechanism). Nothing is serialised on the way and there is no intermediate byte form. HashedPatchMap.matches then checks the removed set, the added count, and each component’s hash in turn.

Two things follow. The client is asserting a belief, not authoring state — a hashed component cannot be turned back into its value, so a client that lies here can only fail to match — and each claimed slot costs an integer per component rather than a re-encoded DataComponentPatch. The asymmetry is exact: only the client ever calls HashedStack.create, and only the server ever calls HashedStack.matches. Hashing is not free-standing on the server either: ServerPlayer’s ContainerSynchronizer carries a 256-entry component-hash cache shared by every menu that player opens, and ContainerSynchronizer.createSlot hands each new RemoteSlot.Synchronized a HashedPatchMap.HashGenerator backed by it.

The other channel: DataSlot, and why it is never silent

One channel is deliberately outside all of this. DataSlot and ContainerData — furnace progress, enchanting cost, lectern page — travel as ClientboundContainerSetDataPacket, whose id and value are written as shorts, and they carry two independent baselines: DataSlot.checkAndClearUpdateFlag for the listeners and AbstractContainerMenu.remoteDataSlots for the network, compared separately. A slot is a DataSlot.shared view onto an array the menu already keeps, a DataSlot.standalone int the menu owns, or one entry of a ContainerData through DataSlot.forContainer, and each one that differs costs its own packet. The network comparison is a plain integer test, so a furnace’s progress bar is not covered by the hash-agreement silence and is re-sent every time it changes.

The state id, and the three places it moves

AbstractContainerMenu.incrementStateId is a wrapping 15-bit counter — it masks to 32767 — and it answers exactly one question: has the client applied every slot correction I have sent? It has three call sites in the whole game. Two are inside ServerPlayer’s synchronizer, behind ContainerSynchronizer.sendInitialData and ContainerSynchronizer.sendSlotChange; ContainerSynchronizer.sendCarriedChange and ContainerSynchronizer.sendDataChange do not bump it — and ClientboundSetCursorItemPacket carries no id whatever, while ClientboundContainerSetDataPacket carries the container’s but never a state id. The third is CraftingMenu.slotChangedCraftingGrid, which bumps the id on its way past while writing the crafting result’s packet by hand — the one write on the click path that goes around the synchronizer, and the exception the cast row promised. The client never generates one: AbstractContainerMenu.setItem and AbstractContainerMenu.initializeContents simply store whatever arrived and quote it back on the next click. A click quoting a stale id means corrections are still in flight, and the server stops diffing and resends everything.

Where in the tick a broadcast happens

Not one place, and the difference is observable. Packets are drained before any level ticks (the server tick), so a click and any correction it produces both happen at the top of the tick and reach the client the same tick. ServerPlayer.tick then calls AbstractContainerMenu.broadcastChanges again from the level’s entity phase, which runs before the block-entity phase (the level tick). And ServerPlayer.doTick — driven by the connection after every level has finished — repeats the AbstractContainerMenu.stillValid distance test and broadcasts nothing of its own; what does broadcast inside it is a pickup, because ServerPlayer.take, which the player’s own tick reaches for every item, orb or arrow it collects, calls AbstractContainerMenu.broadcastChanges.

The drain answers more than slot clicks. A menu button click — the lectern’s page turn, the enchanting table’s offer, the loom’s pattern, the stonecutter’s choice — is answered inside its own handler the same way: ServerGamePacketListenerImpl.handleContainerButtonClick calls AbstractContainerMenu.broadcastChanges directly, the moment AbstractContainerMenu.clickMenuButton accepts, so the correction leaves on the same packet drain that brought the click; pick-block and the creative slot broadcast inside their handlers too.

Back in the tick, the distance test happens twice and only the first brings a broadcast of its own. A hopper that pushes an item into a chest whose menu is open therefore runs in the block-entity phase, after that tick’s regular broadcast, and nothing calls back into the menu to say so — SimpleContainer.setChanged is empty, BlockEntity.setChanged marks the chunk and re-derives the comparator output and stops there, Inventory.setChanged only bumps a counter. You see the hopper’s item one tick late, unless a pickup in the player’s own tick broadcasts first.

What the client does with a correction, and what closing rescues

Almost nothing, is the answer to the first. ClientPacketListener.handleContainerSetSlot checks the container id, writes the stack into that one slot and stores the state id that came with it. There is no rollback and nothing to roll back: the client’s menu is the only copy of its own prediction, so a correction overwrites it and the stack snaps. Nor, after a click the server ran to its end, is a mispredicted slot ever left standing — the server compares every slot against its record on every broadcast, not only the ones the click claimed, so a wrong guess is corrected on that tick or on the next one the menu is broadcast in.

Closing has its own surprise. The cursor belongs to the menu, not the player, so closing one would destroy it: AbstractContainerMenu.removed rescues it explicitly, dropping it in the world if the player has been removed for any reason but a change of dimension, or has disconnected, and calling Inventory.placeItemBackInInventory otherwise, the whole method gated on being a ServerPlayer, which is what makes that safe. The rescue sends one ClientboundSetPlayerInventoryPacket per slot it fills, and runs before AbstractContainerMenu.transferState, which copies both the listener baseline and the remote beliefs across every container-and-slot pair the closing menu and InventoryMenu share. For a chest that is the 36 main and hotbar slots — not armour, not the offhand, not the 2×2 grid, not the crafting result — so changes to any of those are re-sent, and so is every slot the rescue has just filled, which it wrote before the beliefs were copied.

The seven click kinds

ContainerInput has seven values, and the button number means something different in nearly every one. ClickAction — ClickAction.PRIMARY and ClickAction.SECONDARY — is not on the wire; it is derived inside AbstractContainerMenu.doClick on both sides and handed to the item override hooks ItemStack.overrideStackedOnOther and ItemStack.overrideOtherStackedOnMe, which is how a BundleItem intercepts a click before ordinary slot logic runs.

valuethe gesturewhat the button number meanspackets per gesture
ContainerInput.PICKUPthe ordinary click0 left, 1 right — and on AbstractContainerMenu.SLOT_CLICKED_OUTSIDE, drop all or drop one1
ContainerInput.QUICK_MOVEshift-click0 or 1, the same in a real slot — and outside the window, the drop-all and drop-one of ContainerInput.PICKUP1, or one per matching slot for the shift-double-click sweep
ContainerInput.SWAPa hotbar key, or Inventory.SLOT_OFFHAND for the offhand keythe destination index: 0–8, or 401
ContainerInput.CLONEcreative middle-clickignored, but the player must pass Player.hasInfiniteMaterials1
ContainerInput.THROWQ over a slot, cursor empty — and also a click outside the window with an empty cursor, which the server’s branch then ignores because it demands a non-negative index0 drops one, 1 (control-Q) drops the stack and keeps going while the slot yields the same item1
ContainerInput.QUICK_CRAFTdrag-paintinga packed mask that AbstractContainerMenu.getQuickcraftHeader and AbstractContainerMenu.getQuickcraftType split into a phase (start, continue, end) and a mode (even split, one each, clone)painted slots plus two — AbstractContainerScreen.quickCraftToSlots sends a start, one continue per slot, and an end
ContainerInput.PICKUP_ALLdouble-click to collect0 scans forwards, 1 backwards, over two passes that skip already-full stacks first1

An unknown id on the wire is not rejected. ContainerInput’s id mapper is built with a zero-out-of-bounds strategy, so a malformed click decodes as ContainerInput.PICKUP.

Two paths that are not this protocol at all

Creative mode is a parallel protocol, not a variation

CreativeModeInventoryScreen overrides AbstractContainerScreen.slotClicked and drives a menu of its own, and its writes go up as ServerboundSetCreativeModeSlotPacket. That is the only serverbound packet in the game that carries an ItemStack at all, and the server takes it verbatim: ServerGamePacketListenerImpl.handleSetCreativeModeSlot puts the client’s stack into the slot through Slot.setByPlayer, behind only a Player.hasInfiniteMaterials check, a feature-flag check, a slot range of 1 to 45 and a count cap — then writes the same stack into the server’s record of the client’s belief with AbstractContainerMenu.setRemoteSlot, to suppress the echo. (Other packets let a client author text the server keeps — a rename, a sign, a jigsaw block — but no other lets it author a stack.) A negative slot number means drop it in the world instead, and that branch is the one place here with a rate limiter on it.

The crafting result is a second, unsuppressed channel

The result slot’s packet is written by hand and goes straight down the connection, bumping the state id on its way past and bypassing ContainerSynchronizer entirely (recipes). What matters here is that it fires during a click and is therefore the one path AbstractContainerMenu.suppressRemoteUpdates does not cover: that flag guards AbstractContainerMenu.synchronizeSlotToRemote and its two siblings, and nothing else.

Everything else a menu puts on the wire is bookkeeping around those two — opening and closing, the full resync, the button clicks, the crafter toggle, the bundle selection, the hotbar key — and the whole set is one table in the packet catalogue. Only one of them surprises: ServerboundSetCarriedItemPacket is the hotbar selection, which despite the name has nothing to do with the cursor; the packet the server sends for the same selection is ClientboundSetHeldSlotPacket.

Two things about a menu’s opening are worth keeping here. A structure chest fills itself on the first open, and that is the roll rather than the sync (loot tables). And none of the click protocol is data-driven at all: BuiltInRegistries.MENU supplies the type and nothing else, which is why nothing on this page has a JSON file behind it.

Where to look

Follow the click in the order the two machines run it. MultiPlayerGameMode.handleContainerInput is the snapshot and the prediction; AbstractContainerMenu.doClick is the branch table both sides share, with AbstractContainerMenu.moveItemStackTo under the shift-click. ServerGamePacketListenerImpl.handleContainerClick is the ladder, and AbstractContainerMenu.broadcastChanges is where the silence is decided.

For the belief mechanism, read RemoteSlot and then HashedStack: one is the server’s record, the other is what a click is allowed to put in it. HashOps under them is the trick that makes a hash out of a codec with no bytes in between.

For the object model, Container and Slot are the two halves nobody expects to be separate, and ChestMenu beside InventoryMenu is the shortest way to see what a menu subclass adds. MenuType and MenuProvider are the two ends of opening one, and MenuScreens is where the client’s copy is built from a constructor the server never runs.

Two doors lead out of this protocol rather than through it: CraftingMenu.slotChangedCraftingGrid writes the result slot by hand, and ServerGamePacketListenerImpl.handleSetCreativeModeSlot is the one place a client’s stack is taken at its word.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Recipes

Verified against Minecraft 26.3 · Part VII · Eight planks go around the empty centre of a crafting table, a chest appears in the result slot, and the client is never told which recipe it was.

You lay eight planks around the empty middle square of a crafting table and a chest appears in the slot on the right. What the server did was take a trimmed copy of the grid, scan every crafting recipe it has loaded in alphabetical order until one matched, call a Recipe.assemble that ignored your planks entirely, and push the answer down the wire in a packet written by hand, outside the menu’s ordinary bookkeeping. What it never did was tell you which recipe matched — and it could not have, because no Recipe ever crosses the wire. Every RecipeSerializer is a record of a MapCodec and a StreamCodec, and no game packet uses the stream half: the only reference to Recipe.STREAM_CODEC anywhere in the tree is inside RecipeHolder.STREAM_CODEC, which has no call sites at all. For the recipes it has unlocked the client holds the whole contents — pattern, dimensions, ingredients, result — as a RecipeDisplay. What it is denied is the identity, every recipe it has not unlocked, and any authority whatever over the outcome.

The cast

classwhat it decidesthread
Recipeten methods and no result getter — a result leaves through Recipe.assemble, Recipe.display, or a stonecutter’s StonecutterRecipe.resultDisplaydata, and a Recipe object never leaves the server
RecipeManagerthe loaded set, four indexes derived from it, and the server’s RecipeAccessthe worker pool for its constructor; the Server thread after, the first finalize on whichever thread builds the server
RecipeMapthe immutable store, holding exactly two indexes: RecipeMap.byType and RecipeMap.byKeybuilt on the worker pool, swapped in and read on the Server thread
Ingredientwhether one stack satisfies one slot — a HolderSet of items wearing a predicate faceboth sides
CraftingInputthe trimmed grid a crafting recipe is matched against, and the presence index a shapeless one is matched withServer, and a result slot’s ResultSlot.onTake builds one on the Render thread too
ResultSlotthe order of the endgame: award first, then look the recipe up again, then decrementServer, mirrored on the Render thread’s menu
ServerRecipeBookwhich recipes this player has unlocked, and which of them still glowServer
ClientRecipeContainereverything the client knows about recipes as recipes: nine RecipePropertySets and the stonecutter’s input setRender, rebuilt wholesale from each packet

Loading: one scan, one swap, and four indexes built later

RecipeManager is not a reload listener. RegistryDataLoader reads the recipe files into a reloadable registry, Registries.RECIPE, and a fresh RecipeManager builds its RecipeMap from that registry in its constructor, both on the worker pool; on a reload the Server thread then swaps the new manager in. That is the two-phase shape of the resource system without a listener — and the second phase does something unusual.

flowchart TD
    W["RegistryDataLoader reads data/ns/recipe into Registries.RECIPE"]:::worker
    W --> M["RecipeManager constructor builds its RecipeMap"]:::worker
    M --> A["MinecraftServer.reloadResources swaps in the new manager"]:::server
    A -. "the four indexes below are empty, not stale" .-> F
    F["RecipeManager.finalizeRecipeLoading, called by MinecraftServer"]:::server
    F --> P1["nine RecipePropertySet indexes"]:::server
    F --> P2["SelectableRecipe.SingleInputSet"]:::server
    F --> P3["RecipeManager.allDisplays, indexed by RecipeDisplayId"]:::server
    P3 --> P4["RecipeManager.recipeToDisplay, key to displays"]:::server

The reload’s two phases — the worker pool’s two boxes above, the Server thread’s below — and the gap between them. The dotted edge is the one the section is about: the swap is not what builds the four indexes, and between those two boxes they hold nothing.

Each index applies its own feature-flag filter as it is built: an ingredient is dropped from a RecipePropertySet unless every item in it is enabled, the stonecutter’s set keeps an entry only if input and result display are both enabled, and a display is kept only if its result and its crafting station are. Three tests of three different things, all against one FeatureFlagSet, the level’s, and the same filter ItemStack.isItemEnabled applies to an assembled stack two sections below.

Three things in that picture are worth saying out loud.

The scan is sorted, and the sort is the whole ordering story. ResourceManagerRegistryLoadTask.load registers the recipes sorted by their files’ Identifiers, RecipeMap keeps that order, and Identifier.compareTo compares the path first and the namespace only to break a tie. So foo:acacia_boat sorts ahead of minecraft:zzz, matching is deterministic across restarts, and the same order fixes the numbering of every display id below.

The indexes are not built with the map. RecipeManager.finalizeRecipeLoading has exactly two call sites, both of them in MinecraftServer, and neither is on the worker pool. Between the swap and that call the four derived indexes are empty rather than stale: a reload builds a fresh RecipeManager, and its constructor builds the RecipeMap but leaves all four at their empty values, so the recipe book, the property sets and the stonecutter index describe nothing at all rather than describing the pack you just replaced. Nothing can catch the game in that state — the swap and the call are five statements apart in one lambda on the Server thread — which is the only reason the gap is allowed to exist.

The last two indexes are a pair: RecipeManager.allDisplays is the flat list, and RecipeManager.recipeToDisplay maps a recipe key to the entries it produced in it.

A RecipeDisplayId is a list index, not an identifier. It is a record wrapping a single int, and the int is the position the entry took in the flat display list. Reload an unchanged pack and every id comes back identical; add one recipe near the front and everything after it shifts. The server does not try to work out which ids moved — ServerRecipeBook.sendInitialRecipeBook re-sends the player’s whole book with the replace flag set. Nor is the list one entry per recipe: Recipe.display returns a list, and TransmuteRecipe returns one display per legal material count — up to TransmuteRecipe.MAX_MATERIAL_COUNT, eight — so a single recipe can occupy eight consecutive ids.

The same walk decides what a recipe is, and it forgives more than you would expect. A non-special recipe whose PlacementInfo.isImpossibleToPlace — the cause being an ingredient tag that resolved to nothing, which Ingredient.CODEC cannot reject because only a literal empty list is illegal — is logged as unplaceable and then kept. It stays in RecipeMap, where its empty ingredient matches no stack and so no manual craft, and it still gets a RecipeDisplayEntry, one whose ingredient list is present but empty; and RecipeDisplayEntry.canCraft on an empty ingredient list is trivially true, so the book paints as craftable a recipe nobody can make. Only gate six of the auto-fill, below, stops the click.

It is not only shaped and shapeless

RecipeSerializers registers twenty-two serializers and fourteen of them are crafting-table recipes. Nine of those fourteen are CustomRecipes — matching and assembly written in Java, though eight of the nine read their ingredients and base result from JSON. CustomRecipe hard-codes Recipe.isSpecial true, Recipe.group empty and PlacementInfo.NOT_PLACEABLE, and not one of the nine overrides Recipe.display, so a special recipe contributes nothing to the display list in the first place. Eight of the nine register under an id beginning crafting_special_, which is where the name comes from; the ninth, DecoratedPotRecipe, registers as crafting_decorated_pot and is a CustomRecipe all the same. The nine are the crafting recipes the book never shows: FireworkStarRecipe, FireworkStarFadeRecipe, FireworkRocketRecipe, BookCloningRecipe, BannerDuplicateRecipe, ShieldDecorationRecipe, MapExtendingRecipe, DecoratedPotRecipe and RepairItemRecipe, which is the one this part comes back for: it fuses two items of one kind that have durability and carries every curse from both inputs onto the result (enchanting). Of the five crafting-table serializers that remain, ShapedRecipe and ShapelessRecipe are the pair everyone knows, and DyeRecipe, ImbueRecipe and TransmuteRecipe are genuine NormalCraftingRecipes — data-driven, with hand-written matching, hand-built placement info, and the exotic SlotDisplay variants to draw themselves with (SlotDisplay.OnlyWithComponent and SlotDisplay.DyedSlotDemo for the dye recipe, SlotDisplay.WithAnyPotion for the imbue). So special and neither shaped nor shapeless are not the same set, and the difference matters, because it is special that the book cannot show.

Eight planks: the trace

Putting the last plank into a crafting grid is two traces, the second on a later click, and only the second one reaches the recipe book.

sequenceDiagram
    participant CraftM as CraftingMenu
    participant TCC as TransientCrafting<br/>Container
    participant RM as RecipeManager
    participant ResultC as ResultContainer
    participant Wire as the network

    rect rgba(0, 0, 0, 0.04)
        Note over CraftM,Wire: the tick the eighth plank lands
        CraftM->>TCC: asCraftInput, trimming the empty rows and columns
        CraftM->>RM: getRecipeFor CRAFTING, this input, this level, no hint
        RM-->>CraftM: the first RecipeHolder that matches, in registry order, or nothing
        CraftM->>ResultC: setRecipeUsed, refused under LIMITED_CRAFTING
        CraftM->>ResultC: setItem 0, the assembled stack
        CraftM->>Wire: ClientboundContainerSetSlotPacket, bumping the state id
    end

The result slot is filled and announced in the tick the grid changed, and nothing has been taken yet. The packet at the foot is written by hand rather than left to the menu’s own diffing pass.

Nothing above happens on the client: the client’s CraftingMenu is handed ContainerLevelAccess.NULL and matches nothing. What arrives is the last arrow. The second half of the trace is a later click, and only then does a recipe reach the book.

sequenceDiagram
    participant ResultS as ResultSlot
    participant ResultC as ResultContainer
    participant SRB as ServerRecipeBook
    participant RM as RecipeManager
    participant TCC as TransientCrafting<br/>Container
    participant Wire as the network

    rect rgba(0, 0, 0, 0.04)
        Note over ResultS,Wire: a later click, the player takes the result
        ResultS->>ResultS: checkTakeAchievements runs before a cell is emptied
        ResultS->>ResultC: awardUsedRecipes, which then nulls the stored holder
        ResultC->>SRB: addRecipes, through ServerPlayer.awardRecipes
        SRB->>Wire: ClientboundRecipeBookAddPacket
        ResultS->>RM: getRecipeFor again, holder or no holder
        ResultS->>TCC: Container.removeItem, one cell at a time, each followed by its remainder
        Note over ResultS,TCC: every removal re-enters CraftingMenu.slotsChanged
    end

Taking the stack is where the recipe reaches the book, and where the grid is emptied a cell at a time — each removal starting the first figure again.

The grid changes. Writing a plank into the menu’s TransientCraftingContainer calls AbstractContainerMenu.slotsChanged, and both crafting overrides of it end in the same static, CraftingMenu.slotChangedCraftingGrid. They reach it differently: CraftingMenu.slotsChanged goes through ContainerLevelAccess.execute, while InventoryMenu.slotsChanged calls the static directly, gated only on having a ServerLevel. The two-argument CraftingMenu constructor — the client’s — is handed ContainerLevelAccess.NULL, which runs nothing at all (containers and menus), so the client never matches anything.

Trimming. Every recipe kind is matched against a RecipeInput, and there are exactly four implementations of it in the game: CraftingInput for a grid, SingleRecipeInput for the one-slot stations — the four AbstractCookingRecipe kinds and the stonecutter’s, all of them SingleItemRecipes — SmithingRecipeInput for the smithing table’s three, and BrewingInput for a brewing stand’s bottle and reagent. CraftingContainer.asPositionedCraftInput produces a CraftingInput with the empty border rows and columns removed and the offset beside it, which is why a shaped recipe works anywhere in the grid; CraftingContainer.asCraftInput is the same call with the offset thrown away, and it is what matching uses. Its constructor also fills a StackedItemContents, and does it twice unusually: it accounts every stack as one item, and it calls StackedItemContents.accountStack directly rather than the filtered StackedItemContents.accountSimpleStack the recipe book uses. Both choices point the same way — this index is a presence set for shapeless matching, not the arithmetic the auto-fill does, and it deliberately does not apply the auto-fill’s filter. What that filter is, and who it catches out, is the last section of this page.

Matching is a linear scan. RecipeMap.getRecipesFor exits immediately on an empty input, then streams the recipes of that RecipeType and filters them through Recipe.matches, and RecipeManager.getRecipeFor takes the first. ShapedRecipePattern.matches compares the ingredient count, then demands exactly matching trimmed dimensions, then tries the mirrored layout before the straight one — unless the pattern is symmetrical, which Util.isSymmetrical settles once in the constructor. The chest’s ring of planks is symmetrical, so only the straight pass ever runs, and a one-column pattern is always symmetrical too. ShapelessRecipe.matches rejects on count, short-circuits the single-slot case, and otherwise hands the presence index to StackedItemContents.canCraft, which is a thin wrapper: the bipartite search itself lives in the generic StackedContents that StackedItemContents holds and delegates to.

Three accelerations sit on top of that scan, and each belongs to a different caller. RecipeManager.getRecipeFor takes an optional hint and tests it before scanning; the auto-fill supplies one through CraftingMenu.finishPlacingRecipe, so the recipe it has just laid out is the first thing re-matched — and the base AbstractCraftingMenu.finishPlacingRecipe is a no-op, so the player’s own 2×2 grid never gets one. RecipeManager.CachedCheck remembers the last successful key and re-hints with it — AbstractFurnaceBlockEntity holds one per block entity, BrewingStandBlockEntity another, and CampfireBlock hands one to CampfireBlockEntity.cookTick. And RecipeCache, ten entries held statically by CrafterBlock and keyed on the grid contents, caches misses as well as hits, and invalidates on object identity: it keeps a weak reference to the manager and wipes itself the moment the level hands back a different one, which works because a reload builds a new RecipeManager.

The gate, and then the result. RecipeCraftingHolder.setRecipeUsed returns false — leaving the result slot empty — when GameRules.LIMITED_CRAFTING is on, the recipe is not special, and ServerRecipeBook.contains says no. Limited crafting is enforced here, in the result slot, not in matching. Past it, Recipe.assemble produces the stack — for every crafting recipe but the hand-written ones it ignores its input entirely and materialises a stored ItemStackTemplate (items and stacks) — ItemStack.isItemEnabled filters that against the level’s feature flags, and ResultContainer.setItem stores it.

Pushing it. AbstractContainerMenu.setRemoteSlot forces the server’s belief about slot zero and then a ClientboundContainerSetSlotPacket is sent by hand, incrementing the state id on its own way past — outside the diffing that containers and menus describes, and outside AbstractContainerMenu.suppressRemoteUpdates, which guards only that diffing and so cannot reach a hand-written send. It is sent even when the result is empty.

Taking it, in an order that surprises. ResultSlot.remove counts what was taken, and then ResultSlot.onTake runs ResultSlot.checkTakeAchievements before anything is consumed. That calls ItemStack.onCraftedBy, which awards Stats.ITEM_CRAFTED and runs Item.onCraftedBy, and then RecipeCraftingHolder.awardUsedRecipes, which fires CriteriaTriggers.RECIPE_CRAFTED for every recipe, special ones included, and then — for a non-special recipe only — calls ServerPlayer.awardRecipes and so ServerRecipeBook.addRecipes, unlocking it, firing CriteriaTriggers.RECIPE_UNLOCKED, sending ClientboundRecipeBookAddPacket, and nulling the stored holder. The next thing ResultSlot.onTake does is look the recipe up all over again — not because of that null, but because ResultSlot.getRemainingItems never consults the stored holder on any path, special recipes included. CraftingRecipe.getRemainingItems — whose default implementation is spelled CraftingRecipe.defaultCraftingReminder, the typo Mojang’s — maps each slot through Item.getCraftingRemainder, an ItemStackTemplate or nothing. Then one item leaves each occupied cell and the remainder goes back into the emptied slot, or merges with what is left there, or goes to the inventory, or is dropped. Every one of those removals re-enters AbstractContainerMenu.slotsChanged, so the result slot is recomputed eight times on the way out.

A crafter block runs the same machinery through RecipeCache and fires its own advancement trigger, CriteriaTriggers.CRAFTER_RECIPE_CRAFTED, not the player’s.

What the client actually gets

The client’s entire recipe API is two methods. RecipeAccess declares RecipeAccess.propertySet and RecipeAccess.stonecutterRecipes and nothing else, and exactly two classes implement it: RecipeManager on the server and ClientRecipeContainer on the client, the latter holding a map and a set and having no other content at all. Neither method’s type is a Recipe, but the type is not what keeps recipes off the wire: on the server the stonecutter set carries a recipe in every entry, and the codec below is what drops it.

ClientboundUpdateRecipesPacket goes out twice: once from PlayerList as a player joins, once to everybody from PlayerList.reloadResources. It carries two things. The RecipePropertySets are flat sets of items, and RecipePropertySet.test is item identity only — the client’s slot predicate ignores components entirely. The stonecutter’s SelectableRecipe.SingleInputSet is written by SelectableRecipe.SingleInputEntry.noRecipeCodec, which serialises the input Ingredient’s contents and the option’s SlotDisplay and drops the recipe on the floor: SelectableRecipe.noRecipeCodec decodes an empty optional in its place.

Those sets exist so that menus can answer may this item go in this slot — and route a shift-click on the strength of it — without knowing a single recipe. The recipe classes behind those stations are as thin as the sets suggest: SingleItemRecipe is the shape StonecutterRecipe and the cooking recipes extend, and SmithingTransformRecipe and SmithingTrimRecipe are the smithing table’s two — one that replaces the item, one that writes an ArmorTrim component onto it. SmithingMenu builds its three input slots out of the three smithing sets, AbstractFurnaceMenu holds whichever of RecipePropertySet.FURNACE_INPUT, RecipePropertySet.BLAST_FURNACE_INPUT and RecipePropertySet.SMOKER_INPUT its type was constructed with — the fourth set, RecipePropertySet.CAMPFIRE_INPUT, reaches no menu at all and is read by CampfireBlock on a right-click — BrewingStandMenu reads the two brewing sets, and StonecutterMenu asks the stonecutter set the same question through SelectableRecipe.SingleInputSet.acceptsInput.

The book gets more, and still no names

The book gets something far richer and still anonymous: a RecipeDisplayEntry per display, carrying the display id, the RecipeDisplay itself, a group index, a RecipeBookCategory, and an optional ingredient list used for one thing only — deciding whether the entry shows as craftable. RecipeDisplay and SlotDisplay are dispatched registries of their own (Registries.RECIPE_DISPLAY, Registries.SLOT_DISPLAY, and Registries.RECIPE_BOOK_CATEGORY for the categories RecipeBookCategories fills), and their stream codecs are very much used: five RecipeDisplay types, one per station shape, and eleven registered SlotDisplay variants, which SlotDisplay.resolve turns into concrete stacks against a ContextMap that SlotDisplayContext builds. The chest’s single ingredient reaches the client as a SlotDisplay.TagSlotDisplay over minecraft:planks (tags) — everything needed to draw the recipe, and nothing at all about its name.

The recipe book: unlocked, glowing, and filled in for you

ServerRecipeBook stores four things: a display resolver, the settings, ServerRecipeBook.known — an identity set of recipe keys — and ServerRecipeBook.highlight, the subset still new enough to glow. It is saved in the player NBT as ServerRecipeBook.Packed and read back by ServerRecipeBook.loadUntrusted, which validates every key against the live RecipeManager and logs and drops the ones that no longer resolve (codecs). A fresh player’s is genuinely empty, and stays empty until something fills it. The ordinary route is not crafting at all: every recipe in vanilla that is not special has an advancement whose reward names it, so unlocking the advancement is what fills the book (advancements), and a new world’s opening minutes are that machinery running. Crafting a recipe you had not unlocked adds it too, which is the path this trace took. ClientRecipeBook never sees any of that. It holds RecipeDisplayEntrys by display id, and ClientRecipeBook.rebuildCollections groups them into RecipeCollections by category and then by group index, which is why one button in the book cycles through all thirteen kinds of plank.

The tabs are narrower than the recipe types. RecipeBookType has four values — crafting, furnace, blast furnace and smoker — and exactly five concrete menus extend RecipeBookMenu to claim them, under two abstract halves: AbstractCraftingMenu gives CraftingMenu and InventoryMenu, both answering RecipeBookType.CRAFTING, and AbstractFurnaceMenu gives FurnaceMenu, BlastFurnaceMenu and SmokerMenu, one per remaining value. StonecutterMenu and SmithingMenu are not RecipeBookMenus at all, so the stonecutter and the smithing table have no book and no auto-fill, even though RecipeBookCategories.STONECUTTER and RecipeBookCategories.SMITHING exist to categorise them — and the anvil and the grindstone were never recipes at all (enchanting).

Craftability is decided twice

Craftability is decided on the client and then decided again on the server. RecipeCollection.selectRecipes asks RecipeDisplayEntry.canCraft against a StackedItemContents that RecipeBookComponent fills from the player’s inventory, purely to choose which entries show as craftable. A lying client gains nothing by it, because the placement re-checks and CraftingMenu.slotChangedCraftingGrid runs a full Recipe.matches afterwards regardless.

Seven gates before a single plank moves

Auto-fill itself runs entirely on the server. Clicking an entry sends ServerboundPlaceRecipePacket carrying nothing but a container id, a RecipeDisplayId and a shift flag, and ServerGamePacketListenerImpl.handlePlaceRecipe puts it through seven gates:

  1. the player is not a spectator, and the packet’s container id is the open menu’s;
  2. AbstractContainerMenu.stillValid still holds for the open menu;
  3. RecipeManager.getRecipeFromDisplay resolves the index to a RecipeManager.ServerDisplayInfo;
  4. ServerRecipeBook.contains says this player has unlocked the parent recipe;
  5. the open menu really is a RecipeBookMenu;
  6. the recipe’s PlacementInfo.isImpossibleToPlace says no — which catches PlacementInfo.NOT_PLACEABLE and any placement whose ingredients came out empty;
  7. and only then does RecipeBookMenu.handlePlacement run.

AbstractCraftingMenu.handlePlacement calls AbstractCraftingMenu.beginPlacingRecipe, which on a CraftingMenu raises a private flag that makes CraftingMenu.slotsChanged a no-op while the shuffling happens — a CraftingMenu override, so the 2×2 grid inside InventoryMenu re-matches on every single write — and calls ServerPlaceRecipe.placeRecipe, which counts before it clears: a dry run of emptying the grid back into the inventory, a tally of what the player has, a calculation of how many crafts that allows, clamped to the smallest maximum stack size among the items chosen, and only then the real clear and the layout through PlaceRecipeHelper.placeRecipe. Whether it may drop items in order to clear the grid is simply Player.isCreative, so for a survival player whose inventory cannot take back what the grid holds the whole call returns RecipeBookMenu.PostPlaceAction.NOTHING — no fill and no ghost. When the ingredients merely are not there it returns RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE instead, and the server sends ClientboundPlaceGhostRecipePacket carrying the RecipeDisplay itself.

The filter that greys out a recipe you could craft by hand

One filter runs underneath all of that and catches players out. Inventory.isUsableForCrafting rejects any stack that is damaged, enchanted or renamed, and it gates both halves of the auto-fill: the tally, through StackedItemContents.accountSimpleStack, and the actual pull, through Inventory.findSlotMatchingCraftingIngredient. Matching does not gate on it at all — that is what CraftingInput’s direct call to StackedItemContents.accountStack buys. So the book can grey out a recipe that a manual craft with those very items would have accepted without complaint, and the difference between the two is one method name.

Where to look

Read RecipeAccess first. It declares two methods, it is the whole of what a recipe system has to offer a client, and everything else on this page is an argument about why it is that short.

Then the server’s side in loading order: ResourceManagerRegistryLoadTask for the scan, MinecraftServer.reloadResources for the swap and RecipeManager for the four indexes, with RecipeMap beneath it for the two the store keeps. Recipe is the interface every kind implements, and ShapedRecipePattern is the one piece of matching worth reading line by line, for the mirrored pass. CustomRecipe is the nine the book never shows.

For the trace, CraftingMenu.slotChangedCraftingGrid is the whole of what happens when a plank lands, and ResultSlot.onTake is the whole of what happens when you take the chest — in that surprising order. ServerPlaceRecipe is the auto-fill, and Inventory.isUsableForCrafting is the filter under it.

For the client’s half, RecipeDisplay and SlotDisplay are what a recipe becomes when it may not be itself, and ClientRecipeBook is where they land.

Before this page: containers and menus, for the synchroniser that the result slot goes around, and items and stacks for ItemStackTemplate.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Enchantments

Verified against Minecraft 26.3 · Part VII · A player hits a zombie with a Fire Aspect sword, and everything that makes the zombie burn is data.

You swing a Fire Aspect sword at a zombie and the zombie catches fire. Go looking for the class that does that — the one with the on hit, set them alight method — and there is nothing to find. There are no enchantment subclasses. The forty-three vanilla enchantments are JSON files in the built-in data pack, Enchantments is a bag of forty-three registry keys that only five files outside the data generator ever name, and the whole Fire Aspect is melee only rule is one loot condition, sitting on one entry of a DataComponentMap that no item ever holds.

That is the pattern the rest of the page is about. An enchantment is a named modifier that holds no code; every behaviour in the game an enchantment can change is something another system asks about at a well-defined moment: one of its component keys, a tag that names it, or its id in loot data.

The cast

classwhat it decidesthread
Enchantmentthe record — a description, a definition, an exclusive set and a map of effects. No behaviour of its ownbuilt at data-pack load, read on both sides
Enchantment.EnchantmentDefinitionwhat it can go on, what it costs, which slots it counts indata-pack load
EnchantmentEffectComponentsthe thirty-one keys, one per moment an effect can actstatic, both sides
EnchantmentHelperthe static hook surface — walks stacks and slots, calls into the record, holds no stateServer for every entity and location effect; some entry points, two value effects among them, on the Render thread
ConditionalEffect / TargetedConditionalEffectwhether this effect fires here, and on whomServer
EnchantmentEntityEffectthe thing that finally happensServer only — the signature demands a ServerLevel
ItemEnchantmentsthe id-to-level map on the stack, under one of two componentsboth sides
EnchantedItemInUsethe stack, the slot it was found in, its owner and a break callback — the bundle an entity or location effect receives, with a slot that may be emptyServer

A record with a definition and a bag of components

Enchantment is a record of four things: a description Component, an Enchantment.EnchantmentDefinition, a HolderSet of enchantments it is exclusive with, and a DataComponentMap of effects. Enchantment.getEffects looks a component type up in that map and returns an empty list if it is absent, which is the whole dispatch mechanism.

The definition is where the enchanting rules live. It carries the supported items and a narrower, optional set of primary items — both normally item tags — so Enchantment.isPrimaryItem falls back to the supported set when the second is absent, while Enchantment.isSupportedItem only ever asks the first. It carries the weight, the maximum level, two Enchantment.Cost curves (a base plus a per-level-above-first increment, read by Enchantment.Cost.calculate), the anvil cost, and a list of EquipmentSlotGroup — the slots in which this enchantment counts at all, tested by Enchantment.matchingSlot. The codec bounds the weight to 1–1024 and the maximum level to 1–255, which is Enchantment.MAX_LEVEL; Enchantment.getMinLevel is the one number that is not data, hardcoded to 1.

What a stack carries is not that record but ItemEnchantments, an immutable id-to-level map under DataComponents.ENCHANTMENTS — or, for an enchanted book’s inert set, under DataComponents.STORED_ENCHANTMENTS. It is changed only through ItemEnchantments.Mutable, whose ItemEnchantments.Mutable.upgrade merges by maximum and clamps at 255. EnchantmentInstance, a holder-and-level pair with a EnchantmentInstance.weight delegate, is what the weighted selection path is built on — and that path, along with the table, the anvil, the grindstone, the providers and /enchant, is a separate machine with its own arithmetic, on enchanting.

Thirty-one keys, three registries and one curve

EnchantmentEffectComponents registers thirty-one component types into BuiltInRegistries.ENCHANTMENT_EFFECT_COMPONENT_TYPE. Twenty-four hold a list of ConditionalEffect (or TargetedConditionalEffect); the other seven are plain — EnchantmentEffectComponents.ATTRIBUTES, two sound lists, two unconditional values, and the two Unit-valued flags EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP and EnchantmentEffectComponents.PREVENT_ARMOR_CHANGE, true by being present.

Three registries supply the effect objects, and two of the three overlap almost entirely — which is the only thing about them worth remembering. EnchantmentValueEffect, the odd one out, has six shapes and modifies a running number (AddValue, MultiplyValue, SetValue, RemoveBinomial, ScaleExponentially and AllOf.ValueEffects). EnchantmentEntityEffect has fifteen and does something to an entity — Ignite, DamageEntity, ApplyMobEffect, SummonEntityEffect, AllOf.EntityEffects and ten more. EnchantmentEntityEffect extends EnchantmentLocationBasedEffect, whose registry has sixteen entries: those same fifteen ids plus one. That extra one is attribute, EnchantmentAttributeEffect, which installs an AttributeModifier (attributes) and is, beside that registry’s own all_of (AllOf.LocationBasedEffects), the only effect that is location-based without also being an entity effect.

The curve that scales a level

LevelBasedValue turns a level into a number, and six shapes are in its dispatch registry (LevelBasedValue.Linear, LevelBasedValue.Clamped, LevelBasedValue.Fraction, LevelBasedValue.LevelsSquared, LevelBasedValue.Exponent, LevelBasedValue.Lookup). LevelBasedValue.Constant is deliberately not among them: it is the other arm of an either-codec, which is what makes a bare float legal anywhere a curve is expected.

And the condition twenty-four keys sit behind

ConditionalEffect is an effect plus an optional LootItemCondition. TargetedConditionalEffect adds two EnchantmentTarget fields — which side of the fight the enchantment lives on (TargetedConditionalEffect.enchanted) and which side it lands on (TargetedConditionalEffect.affected) — except for the equipment-drops variant, whose codec reads only the first and pins the second to EnchantmentTarget.VICTIM.

Both implement Validatable, and the effect codecs are one of only two places in the game where a context mismatch is a hard error at decode time rather than a logged warning. The other is VillagerTrade, and nothing else in the game gets the treatment (contexts and predicates owns the three ways a parameter goes missing and the two that are fatal). The consequence here is worth the trip: a post_attack effect asking about a block state fails to load, rather than failing quietly at runtime a thousand hits later.

Seven families of moment

Everything above does nothing until something calls EnchantmentHelper. The enchantment package is called from all over the game, so the artefact worth keeping is a table of who calls what: every entry point with its callers is the enchantment hook table, and these are the seven kinds of moment its effect hooks fall into; the rest of the table stores, looks up or chooses enchantments.

familya representative hook or twowho makes it real
damage and protectionEnchantmentHelper.modifyDamage, EnchantmentHelper.getDamageProtectionServerPlayer.getEnchantedDamage, LivingEntity.getDamageAfterMagicAbsorb
post-attack effectsEnchantmentHelper.doPostAttackEffectsWithItemSource, EnchantmentHelper.doPostPiercingAttackEffectsPlayer.itemAttackInteraction, LivingEntity.postPiercingAttack
durability and dropsEnchantmentHelper.processDurabilityChange, EnchantmentHelper.processEquipmentDropChanceItemStack.hurtAndBreak, Mob.dropCustomDeathLoot
projectiles and the weapon in handEnchantmentHelper.processProjectileCount, EnchantmentHelper.getPiercingCountProjectileWeaponItem.draw, the AbstractArrow constructor
location and tick effectsEnchantmentHelper.runLocationChangedEffects, EnchantmentHelper.tickEffectsLivingEntity.onChangedBlock, LivingEntity.baseTick
experience and repairEnchantmentHelper.processBlockExperience, EnchantmentHelper.modifyDurabilityToRepairFromXpBlock.tryDropExperience, ExperienceOrb.repairPlayerItems
the flag questionsEnchantmentHelper.has, EnchantmentHelper.hasTagArmorSlot.mayPickup for Curse of Binding, four blocks for the four prevents tags

The location row is the one that keeps state: LivingEntity.activeLocationDependentEnchantments remembers which location-based effects are running in which slot, so the system can tell became active from still active, and so an attribute effect can be taken off cleanly by EnchantmentHelper.stopLocationBasedEffects when the armour comes off.

How one hook fires

Most hooks in those seven rows are the same shape underneath. A system reaches a moment and asks EnchantmentHelper; the helper picks which stacks to walk, filtering by slot if it was given one; the record picks which effect entries apply; a loot condition decides whether this one fires. Only then does anything happen. The flag row is the exception and is the reason the shape is worth drawing: EnchantmentHelper.has and EnchantmentHelper.hasTag build no context and run no condition: the first asks the record whether the key is present at all, the second whether the enchantment is in a tag.

flowchart TD
    Caller["a system reaches a moment"] --> EH{"which EnchantmentHelper entry point"}
    EH -- "EnchantmentHelper.has, EnchantmentHelper.hasTag" --> Flag["ask the record or its tags: is it there at all"]
    EH -- "any of the other six" --> Walk["walk one stack, or every EquipmentSlot of one entity"]
    Walk --> Slot["on the slot-aware walk, keep entries Enchantment.matchingSlot accepts"]
    Slot --> Comp["read the list under one EnchantmentEffectComponents key"]
    Comp --> Target["for the targeted keys, keep entries whose enchanted target matches"]
    Target --> Ctx["build a LootContext on that hook's parameter set"]
    Ctx --> Cond{"ConditionalEffect.matches runs the LootItemCondition"}
    Cond -- "no" --> Drop["nothing happens"]
    Cond -- "yes" --> Apply["fold a value, or run the effect on the affected entity"]

One shape, and the flag row’s way round it. The flag row answers from the record and its tags alone, with no context built and no condition run.

The LootContext in the middle is the same machinery loot tables and /execute if predicate use, and Enchantment builds five of them — Enchantment.damageContext plus four private siblings for items, entities, locations and block hits, one per parameter set the components name. Contexts and predicates owns that half.

Fire Aspect, from the click to the flame

Fire Aspect as data is one small JSON file: supported and primary items are tags, weight 2, maximum level 2, anvil cost 4, dynamic cost curves, one slot (mainhand), and one effect — a EnchantmentEffectComponents.POST_ATTACK entry whose enchanted target is the attacker, whose affected target is the victim, whose effect is Ignite with a linear duration of four seconds per level, behind one damage-source predicate requiring a direct hit.

sequenceDiagram
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Player as Player
    participant EH as EnchantmentHelper
    participant Ench as Enchantment
    participant Ignite as Ignite
    participant Entity as Entity

    SGPL->>Player: attack, once handleAttack has passed the range checks
    Player->>Player: createAttackSource, whose direct and causing entity are one
    Player->>EH: doPostAttackEffects<br/>WithItemSource,<br/>on a hit that landed
    EH->>EH: the victim's whole equipment first, then the attacker's main hand
    EH->>Ench: doPostAttack for the main-hand stack, in the ATTACKER pass
    Ench->>Ench: TargetedConditional<br/>Effect.matches asks <br/>if the hit was direct
    Ench->>Ignite: apply, with the victim as the affected target
    Ignite->>Entity: igniteForSeconds, raised only if the new value is larger
    rect rgba(0, 0, 0, 0.04)
        Note over Ignite,Entity: a later tick, and every tick after it while the fire lasts
        Entity->>Entity: baseTick sets the shared flags byte through SynchedEntityData
        Note over Entity: the flame travels as a data packet, the enchantment never does
    end

Six lanes between the click and the flame. The band is where the enchantment has already finished — what the other player sees is a SynchedEntityData flag, and one point of damage a second is the entity’s own business.

The swing, and the source that is the whole melee rule. ServerGamePacketListenerImpl.handleAttack runs the range checks and calls Player.attack — unless the main-hand item has DataComponents.PIERCING_WEAPON, in which case the packet is dropped and none of this happens (see the spear for its own path). Player.createAttackSource then asks ItemStack.getDamageSource, and every branch of it reaches the single-entity DamageSource constructor — the one that sets the direct entity and the causing entity to the same object, making DamageSource.isDirect true. An arrow’s source has an arrow as the direct entity and a player as the causing entity, so it is false. That one comparison, read by one loot condition in one JSON file, is why Fire Aspect never fires through a bow.

The damage, then the hook. ServerPlayer.getEnchantedDamage — the override, not Player’s base version, which returns its argument unchanged — folds Sharpness and friends in through EnchantmentHelper.modifyDamage, and the hit goes through Entity.hurtOrSimulate to LivingEntity.hurtServer (damage and death). Only if that returned true does Player.itemAttackInteraction call EnchantmentHelper.doPostAttackEffectsWithItemSource.

The one method has three branches. The helper walks the victim’s whole equipment first, with EnchantmentTarget.VICTIM as the pass — that is Thorns’ lane, and why Thorns works from a chestplate while Fire Aspect does not work from boots. Then, for a living causing entity, it walks the one weapon stack it was handed, in the EnchantmentTarget.ATTACKER pass, keeping enchantments whose declared slots include the main hand. Its third branch takes a causing entity that is not living: a slotless walk with no filter at all, which only fires when a break callback was supplied, and whose only vanilla caller is ThrownTrident. An attacker’s armour can never contribute a post-attack effect.

That second branch is worth one more sentence, because the slot it names is a label rather than a fact. It hands EquipmentSlot.MAINHAND to the filter whatever the weapon actually is, and so does EnchantmentHelper.doPostPiercingAttackEffects; KineticWeapon.damageEntities reaches Player.stabAttack (a mob’s LivingEntity.stabAttack) with whichever slot the use was in. An off-hand spear’s enchantments are therefore tested against the main-hand slot group. The slot question itself is asked two ways: the two EnchantmentHelper.forEachModifier overloads test different things — the EquipmentSlot one asks Enchantment.matchingSlot, the EquipmentSlotGroup one asks whether the definition declares that exact group — and ItemStack.forEachModifier uses one of each. One more pair like it is worth knowing before you grep: the instance method is spelled Enchantment.modifyArmorEffectivness, Mojang’s typo, while the helper beside it is EnchantmentHelper.modifyArmorEffectiveness.

The condition and the target. Enchantment.doPostAttack keeps the entries whose enchanted target matches the pass, builds the enchanted-damage context — victim, level, origin, damage source, and both attacking entities as optional parameters — and runs the predicate. The affected field then picks who receives the effect: attacker, direct entity, or victim. Fire Aspect says victim, and if that works out to null the effect is dropped in silence.

The burn. Ignite.apply calls Entity.igniteForSeconds, which floors to ticks and hands Entity.igniteForTicks a number it applies only if it is larger than the counter already there — while clearing any freeze regardless, so re-hitting a burning target with a weaker Fire Aspect does nothing except thaw it. No damage happens here. Entity.baseTick deals it, one point every twentieth tick, on a ServerLevel, skipped in lava and replaced by Entity.clearFire for a fire-immune entity. The same method sets shared flag zero through Entity.setSharedFlagOnFire (synched entity data), which becomes Entity.displayFireAnimation on the client — whose own Entity.baseTick, finding no ServerLevel, clears the fire counter instead of burning.

Three famous ones driven from the other end

The three enchantments players talk about most are driven from the other end: something else finds each one on the stack and does the famous work, and only one of the three has no effect component at all.

Fortune lives in the loot table. ApplyBonusCount and BonusLevelTableCondition read the tool parameter out of a LootContext and call EnchantmentHelper.getItemEnchantmentLevel on it — a level, not an effect. Looting is the same trick twice more: EnchantedCountIncreaseFunction and LootItemRandomChanceWithEnchantedBonusCondition both read the attacking entity parameter and call EnchantmentHelper.getEnchantmentLevel, which walks the slots of a LivingEntity the enchantment counts in and keeps the best. Fortune has no effect component whatsoever, and Looting’s only one is an equipment_drops entry, which raises the chance a mob drops its equipment and plays no part in its loot table (loot tables).

Mending inverts it once more: ExperienceOrb.repairPlayerItems asks EnchantmentHelper.getRandomItemWith for a damaged stack carrying EnchantmentEffectComponents.REPAIR_WITH_XP, so the orb drives the repair and the item is found. What runs on the item is one number: its EnchantmentEffectComponents.REPAIR_WITH_XP entry, a multiply by two that EnchantmentHelper.modifyDurabilityToRepairFromXp applies, makes each point of experience buy two points of durability.

What runs on the client, and why no entity effect does

No entity or location effect can: EnchantmentEntityEffect and EnchantmentLocationBasedEffect both demand a ServerLevel in their signatures, which is the same mechanical guarantee the loot system uses. But two values do. Enchantment.modifyUnfilteredValue takes only a RandomSource, and its two users are Enchantment.modifyCrossbowChargeTime and Enchantment.modifyTridentSpinAttackStrength.

Both are visible as gameplay. CrossbowItem.getChargeDuration is called by three entity renderers, by FirstPersonHandsAndItems and by the CrossbowPull item property, so Quick Charge is evaluated on the Render thread every frame a crossbow is being drawn — an enchantment hook in the frame loop, to pose an arm and pick one of three textures. And MultiPlayerGameMode.releaseUsingItem runs TridentItem.releaseUsing on the client’s own copy, which asks EnchantmentHelper.getTridentSpinAttackStrength, so Riptide’s strength is computed client-side too, which is what lets the riptide push be predicted at all (using an item).

The rest is drawing, or a question with no effect behind it. The drawing is ItemEnchantments.addToTooltip for the tooltip, ItemStack.hasFoil for the glint, and EnchantmentHelper.forEachModifier for the attribute lines, which means the client evaluates the LevelBasedValue curve itself. The questions are Curse of Binding’s flag, which ArmorSlot.mayPickup and Equippable.swapWithEquipmentSlot ask while a click or a use is predicted, and Riptide’s sound, which EnchantmentHelper.pickHighestLevel chooses before the ServerLevel test. And the anvil and the grindstone build their results on the client’s copy of the menu too, with the same helpers the server uses. That is what the cast’s some entry points on the Render thread covers.

Where the forty-three live, and when they are read

At world load, and not again. Registries.ENCHANTMENT and Registries.ENCHANTMENT_PROVIDER are both in RegistryDataLoader.WORLD_REGISTRIES, the dynamic registries built once when a world opens (identifiers and registries) — not in RegistryLayer.RELOADABLE, where the loot tables and predicates live. /reload rebuilds those and every recipe and leaves the enchantments themselves as the world found them, their tags aside, which it re-reads with every other registry’s; changing an enchantment takes a restart. That is the sharpest difference between this part’s three engines, and the one a data-pack author meets first.

What crosses the wire is usually an id and nothing else. Registries.ENCHANTMENT is in RegistryDataLoader.SYNCHRONIZED_REGISTRIES with the full Enchantment.DIRECT_CODEC, but RegistrySynchronization.packRegistry sends a bare Identifier for every element whose pack the client already has — for a vanilla client against a vanilla server, all forty-three. Full definitions cross only for a data pack’s custom or overridden enchantments. Beyond that, ClientboundUpdateTagsPacket for EnchantmentTags and, per stack, the DataComponents.ENCHANTMENTS component: registry ids and levels.

One JSON file, four effect objects, no Java

Enchantments.LUNGE is the only user of EnchantmentEffectComponents.POST_PIERCING_ATTACK in the game, and it is the whole pattern in one file. Its single effect is an AllOf.EntityEffects of four — a ChangeItemDamage, an ApplyExhaustion scaled per level, an ApplyEntityImpulse forward with its vertical component scaled away, and a PlaySoundEffect holding three sounds, indexed by level rather than shuffled — behind a four-clause condition checking that the user is not riding, not elytra-flying, not in water, and either not a player, in creative, or fed.

Four effect objects, four predicates, no Java, and a lunge that shoves you forward and tires you out. That is what an enchantment is.

Questions players ask

Why is an enchanted book inert? Because every effect hook reads DataComponents.ENCHANTMENTS and nothing else, through the private walks (EnchantmentHelper.runIterationOnItem among them) or, for Mending’s EnchantmentHelper.getRandomItemWith, directly. A book’s set lives under DataComponents.STORED_ENCHANTMENTS, and the routing between the two is keyed on the exact item Items.ENCHANTED_BOOK (enchanting owns that routing). The component is not so exclusive: ItemStack puts DataComponents.STORED_ENCHANTMENTS in any stack’s tooltip, and the anvil prices its right-hand input as a book by testing for that component rather than for the item (enchanting) — so a data pack can make a stick priced like one, but never make it behave like one.

Does Fire Aspect cook the loot? Not through the enchantment. The cooking is SmeltItemFunction, an ordinary loot function on the mob’s own table, behind a condition that passes when the victim is on fire or the direct attacker’s main hand carries an enchantment in EnchantmentTags.SMELTS_LOOT — a tag whose only member is Fire Aspect (loot tables). So the sword you swung is read twice by two systems that know nothing about each other: once for its effect component, and once, much later, as a name in a tag.

Where to look

Enchantment is four fields and will take ten minutes; read it first, because everything else on this page is about how those four fields are consulted. Enchantment.EnchantmentDefinition inside it is the enchanting rules, and EnchantmentEffectComponents beside it is the list of thirty-one moments.

Then EnchantmentHelper, which is the entire behaviour surface and holds no state. Read the private EnchantmentHelper.runIterationOnItem and its slot-aware siblings before any public entry point: they are the walk, and most hooks above them are a thin wrapper around one of them. The hook table is the map of which callers reach which.

For the effect side, ConditionalEffect and TargetedConditionalEffect are the two shapes a conditional entry takes, and Ignite is the smallest effect worth reading — three lines that set an entity on fire. LevelBasedValue is the curve underneath most of them.

For what a stack carries rather than what an enchantment is, ItemEnchantments; for the selection machinery this page deliberately leaves alone, EnchantmentInstance and then enchanting.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Enchanting: the five paths, and what each one is allowed to do

Verified against Minecraft 26.3 · Part VII · A player reads three offers off an enchanting table and buys one, and then the same sword meets the four other paths that change what it is enchanted with — an anvil, a grindstone running backwards, a spawning pillager, and a command.

A sword goes in the left slot of an enchanting table and three lapis in the right, and the table answers with three lines of Standard Galactic Alphabet, three level numbers, and — if you hover — one enchantment named outright. None of that is guessed. The server has already run the entire selection, and the client holds the answer as ten numbers the menu syncs. One of those ten is Player.enchantmentSeed, and it is the reason the page is worth a lecture: one number per player, saved in the player file, carried across death and dimension change, sent to the client, and re-rolled by nothing in the game but the enchanting table itself and a load that finds it still zero. Spend thirty levels at an anvil and come back and the table is offering exactly what it offered before — and the gibberish is in the same handwriting, because the client is drawing it from the low sixteen bits of that same number.

The table is one of five paths that change what a stack is enchanted with — four of them adding and the grindstone taking away — and they differ far more than the shared vocabulary suggests. This page is about those differences. A sixth writer hides outside all of them, in the crafting grid: RepairItemRecipe carries every curse from both inputs onto the item it makes (recipes). What an enchantment is — the record, the effect components, the hooks that fire in combat — is the next page along and is not re-taught here.

The cast

classwhat it decidesthread
EnchantmentMenuthe three offers, the clue, and what the click costsServer, with a Render-thread copy that can only say no
EnchantmentHelperthe cost curve, the weighted selection, and the write every path ends inwhichever side asks
Playerthe seed and the levelsServer
AnvilMenuthe merge arithmetic and the priceServer
GrindstoneMenuthe removal made on purpose, and the refundServer
EnchantmentProviderwhat a mob’s spawn equipment getsServer
EnchantRandomlyFunctionchest loot and villager tradesServer
EnchantCommandthe operator’s path, and the fewest checksServer

EnchantWithLevelsFunction, SetEnchantmentsFunction and EnchantedCountIncreaseFunction are the other three loot functions, named below where they differ; EnchantmentScreen is the client half of the table and gets its own section.

The five paths at a glance

Read this table as the page’s table of contents rather than as something to use now: every cell is a compressed forward reference, and each row below is the section that unpacks it.

enchanting tableanvilgrindstoneproviders and loot/enchant
what it costs1, 2 or 3 levels and the same count of lapisthe full price in levels, and a chance the anvil chipspays you, in orbs at the blocknothingnothing
the gate on the itemItemStack.isEnchantable — enchantable and not already enchantedEnchantmentHelper.canStoreEnchantmentsdamageable or already enchantedDataComponents.ENCHANTABLE, with three exceptions (below)any non-empty main-hand item
which item filterEnchantment.isPrimaryItem — the narrow setEnchantment.canEnchant — the supported setn/aone of three, depending on the path (below)Enchantment.canEnchant
the level ceilingwhatever the cost brackets allowclamped to Enchantment.getMaxLeveln/aclamped, except SetEnchantmentsFunctionrejected above Enchantment.getMaxLevel
exclusivityfiltered out mid-selectiondropped, and it raises the pricecurses survive, everything else goesfiltered among the cost-based picks, unasked by the restrejected with an error
randomnessthe player’s saved seednone in the arithmetic; a 12% roll for the chipnone in the strip; a roll on the refundthe random source its caller hands innone
decided onserver, with the click predictedserver, with the price syncedserverserverserver

Every row of that table differs. The one thing that would not — and so has no row — is the last step, where all five converge. Not on the same method: the table, /enchant, EnchantRandomlyFunction and EnchantWithLevelsFunction go through ItemStack.enchant, the grindstone, SetEnchantmentsFunction and the providers (through EnchantmentHelper.enchantItemFromProvider) call EnchantmentHelper.updateEnchantments, and the anvil writes with EnchantmentHelper.setEnchantments. But on the same decision, and that is where the page starts.

The one question all five ask

The three write methods above funnel into one private line, EnchantmentHelper.getComponentType, so all five paths ask it in the end — and it does the thing worth knowing before any of them make sense.

It routes by item identity: the component the write lands in is DataComponents.STORED_ENCHANTMENTS if the stack is Items.ENCHANTED_BOOK and DataComponents.ENCHANTMENTS otherwise — a hard identity test against one item, not a tag. That is why every path that can be handed a plain Items.BOOK transmutes it first, a creative player’s anvil apart, through ItemStack.transmuteCopy or by building a fresh stack. The transmute is not cosmetic: enchant a plain book and the levels would land in the active component and the book would start working.

EnchantmentHelper.updateEnchantments adds one more rule of its own: it silently does nothing if the component is absent, returning ItemEnchantments.EMPTY on a null read. In practice every item gets DataComponents.ENCHANTMENTS from DataComponents.COMMON_ITEM_COMPONENTS, so the case arises only when a patch removes the component from a stack, and then the table and the anvil refuse the item and every other path becomes a no-op, with no error anywhere (data components).

The merge itself is ItemEnchantments.Mutable.upgrade: keep the higher level, cap at 255, ignore a level of zero. Only ItemEnchantments.Mutable.set can lower a level, and only the anvil — where it clamps an over-maximum level down — SetEnchantmentsFunction and the crafting grid’s RepairItemRecipe reach for it. The grindstone does not lower anything; it removes.

What it costs, and who pays

The enchanting table’s headline number is not its price. The level requirement for a slot is the cost the table computed for it — up to thirty at the bottom slot — but the amount Player.onEnchantmentPerformed actually subtracts is the slot’s index plus one, and the lapis consumed is the same one, two or three.

what the forum sayswhat the decompile does
the bottom offer costs thirty levelsEnchantmentMenu.clickMenuButton requires thirty levels and takes three
more bookshelves make better enchantmentsmore bookshelves raise the cost, which is what buys higher levels and extra enchantments, and EnchantmentHelper.getEnchantmentCost floors the bottom slot at twice the shelf count
the anvil’s “Too Expensive” is a level capit is a result cap — at a price of forty or more AnvilMenu.createResult empties the output slot unless the player has infinite materials

The anvil charges what it shows, and taxes what came before

The anvil is the opposite: it charges the whole displayed price, through Player.giveExperienceLevels with a negative amount, in AnvilMenu.onTake, and then rolls a small chance to damage or destroy the block. The price is a prior-work tax read from DataComponents.REPAIR_COST on both inputs, plus one per repair material consumed, plus two when merging with a second item of its kind repairs it, plus Enchantment.getAnvilCost times the resulting level for every enchantment transferred (halved with a floor of one when the addition is a book), plus one for a rename or for clearing a name. The larger of the two inputs’ DataComponents.REPAIR_COST is then doubled and incremented by AnvilMenu.calculateIncreasedRepairCost and written onto the result — the whole of the prior-work spiral, and the one step a pure rename skips. Two cases escape that arithmetic: an input stack of more than one item sets the price to 40, before the prior-work tax is added, the moment any enchantment transfers, which — forty being exactly the threshold at which the result is withheld — makes enchanting a stack not expensive but forbidden outside creative; and a rename with no other change is capped at 39, which is why renaming never hits “Too Expensive”.

The grindstone pays you

The grindstone runs the transaction backwards. It strips everything not in EnchantmentTags.CURSE, turns an emptied Items.ENCHANTED_BOOK back into a plain Items.BOOK with ItemStack.transmuteCopy, and rebuilds DataComponents.REPAIR_COST from zero, so a clean item leaves with its prior work erased. The refund is the sum of Enchantment.getMinCost at each stripped level, halved upward with a random bonus of up to one less than that half again, and it arrives as orbs from ExperienceOrb.award at the block — on the ground, not in the player (hunger and experience).

What each path is allowed to add

Two predicates are doing the work, and the difference between them is the difference between the enchantments an axe is offered and the enchantments an axe can hold.

Enchantment.canEnchant asks whether the item’s type is in the definition’s supported set; Enchantment.isSupportedItem asks exactly the same question of a stack and is called from nowhere but Enchantment.isPrimaryItem, which asks the supported question and the narrower falling back to the supported set when the definition names no primary items. The narrow one lives in EnchantmentHelper.getAvailableEnchantmentResults, which only EnchantmentHelper.selectEnchantment calls — so the table, the cost-based providers and EnchantWithLevelsFunction all use it, and the anvil and /enchant do not. That is the three-way split the table’s which item filter cell defers: the selection paths ask the narrow question, EnchantRandomlyFunction asks the supported one when its only_compatible flag is on and the target is not a plain book, and SingleEnchantment and SetEnchantmentsFunction ask neither. The three exceptions in the gate row are the three that do not ask the narrow question: each skips DataComponents.ENCHANTABLE. In vanilla exactly five enchantments declare a narrower primary set than their supported one. Three of them are melee enchantments whose supported set reaches axes and whose primary set stops at swords and spears, which is why no enchanting table has ever offered Sharpness on an axe while every anvil will put it there. A fourth does the same to the mace, and the fifth is Thorns, offered only on a chestplate and wearable anywhere.

The anvil uses Enchantment.canEnchant on its left slot, overridden to true when that target is an Items.ENCHANTED_BOOK or the player has infinite materials, so books collect anything. Its right slot is judged differently, and by a different question: whether the addition is a book, for the halved price, is DataComponents.STORED_ENCHANTMENTS being present rather than the item being Items.ENCHANTED_BOOK. One side tests the item, the other tests the component. Its arithmetic per transferred enchantment is short: the same level on both sides merges to one higher, different levels take the maximum, and the winner is clamped to Enchantment.getMaxLevel. An enchantment the target cannot take is dropped and costs nothing; one that conflicts with something already on the result is dropped and adds one to the price per conflicting pair — the anvil’s one punitive rule besides the stack rule above. If nothing survives, the result slot is emptied.

The ceilings, and who ignores them

/enchant is the shortest path and, contrary to its reputation, not the laxest. EnchantCommand rejects a level above Enchantment.getMaxLevel before it looks at any target, then per target requires a LivingEntity whose LivingEntity.getMainHandItem is non-empty, then checks Enchantment.canEnchant and EnchantmentHelper.isEnchantmentCompatible against what the stack already carries — and from there it is the same tail as everything else. What it skips is the primary filter, the enchantability component and the cost, not the supported-items or level rules. It also accepts a level of zero, which passes every check, reports success, and changes nothing.

The genuine ceiling-breaker is elsewhere. SetEnchantmentsFunction writes through ItemEnchantments.Mutable.set, whose only clamp is 255, with no reference to Enchantment.getMaxLevel at all: a loot table can hand out Sharpness 200 and nothing else on this page can.

Exclusivity, one test wherever it is asked

Where it is asked at all, exclusivity is one static method — Enchantment.areCompatible, wrapped by EnchantmentHelper.isEnchantmentCompatible and EnchantmentHelper.filterCompatibleEnchantments — and it is symmetric, failing if either side’s exclusive set names the other, and failing an enchantment against itself. Only the anvil and /enchant ask it against what the stack already carries; the selection asks it only among its own picks, and EnchantRandomlyFunction, SingleEnchantment and SetEnchantmentsFunction never ask it.

Where the randomness comes from

The anvil and the grindstone roll a die each — for the chip and for the refund — and everything else but /enchant (and, with vanilla’s constant levels, SetEnchantmentsFunction) rolls one to decide what you get. But not in the same place. Six things roll what an item gets in this game, and four of them reach the shared arithmetic in EnchantmentHelper.selectEnchantment: the table, the two cost-based providers, and EnchantWithLevelsFunction through EnchantmentHelper.enchantItem. The remaining two roll their own and much more simply — SingleEnchantment samples a level for an enchantment it already knows, and EnchantRandomlyFunction picks one at random off the loot context’s random source. So the arithmetic below is the cost-based path, and it is a short method with four distinct sources of variance stacked on one another — the last of which is a fresh roll from fifty, tested against the cost, once per extra enchantment.

flowchart TD
    A["a cost arrives, from the table or from a provider"] --> B{"does the stack have DataComponents.ENCHANTABLE"}
    B -- no --> Z["empty list, and the caller adds nothing"]
    B -- yes --> C["raise it by two rolls scaled by Enchantable.value"]
    C --> D["scale by a triangular span of 15 percent, clamp at 1"]
    D --> E["EnchantmentHelper.getAvailableEnchantmentResults: primary here, or a plain book"]
    E --> F["per survivor, the highest level whose bracket holds the value"]
    F --> G{"any candidates at all"}
    G -- no --> Z
    G -- yes --> H["weighted pick by Enchantment.getWeight"]
    H --> I{"a roll from fifty is at most the cost"}
    I -- no --> Y["the list, out"]
    I -- yes --> J["drop every candidate incompatible with the last pick"]
    J --> K{"anything left"}
    K -- no --> Y
    K -- yes --> L["weighted pick again, then halve the cost"]
    L --> I

Four sources of variance in a row, and then a loop. The cost halves on every pass through it, which makes each extra enchantment rarer than the last; the first extra roll, taken before any halving, is certain from a cost of forty-nine.

The enchantability perturbation is the first place the item matters: Enchantable.value — gold’s is famously high — widens two independent rolls that only ever push the cost up. The span that follows is triangular rather than flat, so the extremes are rare. The bracket test in EnchantmentHelper.getAvailableEnchantmentResults walks levels downward and stops at the first fit, so a high cost buys a high level of one enchantment rather than more of them. Buying more is the loop’s job: the cost halves after every extra pick, so by the fourth pass the roll is nearly always lost — while from a cost of forty-nine up the first extra roll is certain.

The table adds one more layer. EnchantmentMenu.slotsChanged seeds its RandomSource with Player.enchantmentSeed for the three costs, then re-seeds it with the seed plus the slot number before each selection, which is why the three offers are independent of each other and yet reproducible. EnchantmentHelper.getEnchantmentCost returns zero outright for an item with no DataComponents.ENCHANTABLE, and a slot whose cost came out below its own index plus one is zeroed too. The weight each surviving candidate carries into the pick is Enchantment.getWeight, straight off the data-pack record.

Bookshelves reach it as a plain integer. EnchantmentMenu walks EnchantingTableBlock.BOOKSHELF_OFFSETS — a fixed list of thirty-two offsets, the outer ring of a five-by-five footprint at two heights — and EnchantingTableBlock.isValidBookShelf requires the block at the offset to be in BlockTags.ENCHANTMENT_POWER_PROVIDER and the block between it and the table to be in BlockTags.ENCHANTMENT_POWER_TRANSMITTER (tags). That between position halves the X and Z offsets but leaves Y alone, so the upper ring’s gap is checked at the bookshelf’s own height, not the table’s. The clamp to fifteen happens inside EnchantmentHelper.getEnchantmentCost, not in the walk.

What is decided on which side

The clue you hover is not a hint about what you might get: it is a genuine member of the exact list you will get. EnchantmentMenu.slotsChanged runs the selection for real, shows one entry of the result at random and throws the rest away, and EnchantmentMenu.clickMenuButton runs the same selection again from the same seed and slot and applies all of it. The one wrinkle is the plain book, which has one random entry deleted from its list before either use — unless the list has only one entry, which survives.

sequenceDiagram
    box transparent your machine
    participant EScr as EnchantmentScreen
    participant CEM as EnchantmentMenu
    end
    participant Wire as the network
    box transparent the server
    participant EM as EnchantmentMenu
    participant EH as EnchantmentHelper
    end

    Note over EM,EH: slot 0 fills, and <br/>EnchantmentMenu.<br/>slotsChanged runs on this side
    EM->>EM: walk BOOKSHELF_OFFSETS, count the valid shelves
    EM->>EH: getEnchantmentCost three times, from the player seed
    EM->>EH: selectEnchantment per slot, re-seeded with the seed plus the slot
    EH-->>EM: a list per slot, one entry of which becomes the clue
    EM->>Wire: one<br/>Clientbound<br/>ContainerSetDataPacket<br/>per changed slot, from broadcastChanges
    Wire->>CEM: the costs and the clues, the seed there since the menu opened
    Note over EScr,CEM: EnchantmentNames.<br/>initSeed fixes <br/>the alphabet for this seed
    EScr->>CEM: clickMenuButton, whose level access here is NULL
    CEM-->>EScr: true only if the lapis and the levels are there, or materials are infinite
    EScr->>Wire: ServerboundContainerButtonClickPacket, via MultiPlayerGameMode
    Wire->>EM: clickMenuButton, the identical call on the server's copy
    EM->>EH: selectEnchantment again, same seed and slot, same list
    Note over EM,EH: Player.onEnchantmentPerformed takes the levels and re-rolls the seed
    EM->>EH: updateEnchantments once per entry, through ItemStack.enchant
    Note over EM,EH: the lapis goes, Stats.ENCHANT_ITEM, CriteriaTriggers.ENCHANTED_ITEM
    EM->>EM: slotsChanged again, and an enchanted item gets no offers
    EM->>Wire: the zeroed costs, the cleared clues and the new seed, from the handler's broadcastChanges

One menu class, two copies, and the boundary drawn. The client’s EnchantmentMenu answers the click before any packet is sent, from the cost it was sent, the item and lapis in its slots, and the player’s levels or infinite materials.

The predicted click is the sharpest thing on that diagram. EnchantmentScreen.mouseClicked calls EnchantmentMenu.clickMenuButton on its own local menu and only sends the packet — a ServerboundContainerButtonClickPacket, through MultiPlayerGameMode — if that call returns true. The server’s run of the same method is where Stats.ENCHANT_ITEM is awarded and CriteriaTriggers.ENCHANTED_ITEM fires. On the client the menu’s level access is ContainerLevelAccess.NULL, whose evaluation returns an empty optional without running the action at all — so the entire enchanting body is skipped, and what the client really evaluates is the guard in front of it: the lapis count, the level requirement, and Player.hasInfiniteMaterials. The affordability check is real on both sides; the enchanting is real on one. Each changed value crosses as one ClientboundContainerSetDataPacket. Containers and menus has the data-slot channel, and where a broadcast happens has the button click’s own timing.

All ten are ordinary DataSlot entries, and nine of them are DataSlot.shared views onto three arrays of three: the costs, the enchantment ids, and the levels. The tenth is the one DataSlot.standalone, holding the seed. So a clue is a pair — which enchantment, and at what level — which is why three offers cost six slots, and why the tooltip can say Sharpness III rather than just Sharpness. A clue with nothing to show carries −1, and a cost 0. They reach the client one ClientboundContainerSetDataPacket each as AbstractContainerMenu.broadcastChanges diffs them against its remote copy. The enchantment half of each pair is a numeric registry id that EnchantmentScreen resolves against its own registry copy — the same registry copy that ItemEnchantments’ stream codec needs to name the enchantments on any stack the client is sent. The seed slot is the only route by which Player.enchantmentSeed ever reaches a client, and what crosses is its low sixteen bits: it is written to the player file as XpSeed, re-rolled on read if it loads back as zero, copied unconditionally by ServerPlayer.restoreFrom when a respawn builds a new player (death, or the way out of the End), and named in no packet of its own. EnchantmentNames.initSeed then seeds one shared RandomSource with it per frame and EnchantmentNames.getRandomName draws three or four words from a fixed list in the alt font — same seed, same three lines, every time.

The paths that never show a player anything

The provider path, at spawn

Mob.enchantSpawnedEquipment (entity lifecycle) calls EnchantmentHelper.enchantItemFromProvider, which looks a provider up in Registries.ENCHANTMENT_PROVIDER and hands the stack’s mutable enchantment map to EnchantmentProvider.enchant. Registries.ENCHANTMENT_PROVIDER is a world-load dynamic registry like Registries.ENCHANTMENT itself and EnchantmentProviderTypes is the dispatch of kinds inside it (data-driven types). EnchantmentsByCost and EnchantmentsByCostWithDifficulty go through EnchantmentHelper.selectEnchantment, so a mob’s gear is rolled by exactly the arithmetic the table uses, with the local difficulty widening the cost; SingleEnchantment skips selection entirely, upgrading one named enchantment to a sampled level clamped only to that enchantment’s own range, never asking whether the item supports it. Six of the seven providers VanillaEnchantmentProviders registers are that third kind.

The loot path, and the trades on it

The loot path runs wherever a loot table does, and villager trades are on it: a VillagerTrade carries a list of LootItemFunctions applied to what it gives, and the librarian’s enchanted book is EnchantRandomlyFunction with its only_compatible item check turned off, followed by a filter that discards the trade if the book came out with nothing stored in it. EnchantWithLevelsFunction is the cost-based one, in chest loot, fishing and trades alike, and calls EnchantmentHelper.enchantItem — the same selection again; SetEnchantmentsFunction names its enchantments, and vanilla gives it constant levels. Both random ones can set DataComponents.ADDITIONAL_TRADE_COST when the context offers LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED, which is how a strong enchantment makes a trade dearer (loot tables, contexts and predicates). EnchantedCountIncreaseFunction sits in the same package and is the odd one out: it adds nothing, reading a level off the killer with EnchantmentHelper.getEnchantmentLevel to add to a drop count. It consumes this page’s output rather than producing any.

The seventh producer, which is not a path at all

One more producer belongs in nobody’s mental model of enchanting. CreativeModeTabs builds the creative enchanted books with EnchantmentHelper.createBook — maximum level only in the tab, every level in the search, through CreativeModeTabs.generateEnchantmentBookTypesOnlyMaxLevel and CreativeModeTabs.generateEnchantmentBookTypesAllLevels.

Where to look

EnchantmentHelper.selectEnchantment is the one method to read whole. It is short, four callers on two of the five paths run it, and every piece of arithmetic the flowchart above draws is in it, including the halving loop. EnchantmentHelper.getEnchantmentCost beside it is where the bookshelves land and EnchantmentHelper.getAvailableEnchantmentResults is the primary-item filter it calls.

For the table as a machine, read EnchantmentMenu top to bottom: EnchantmentMenu.slotsChanged computes the offers and EnchantmentMenu.clickMenuButton runs the same computation again and applies it, which is the page’s whole point about the clue. EnchantmentScreen is the client’s side, whose copy of the menu can only say no, and EnchantmentNames is the gibberish.

For the other four paths, one class each and they are all small: AnvilMenu.createResult for the price, GrindstoneMenu for the strip and the refund, EnchantmentProvider with VanillaEnchantmentProviders for a mob’s gear, and EnchantCommand for the shortest path of the five.

ItemStack.enchant, EnchantmentHelper.updateEnchantments and the anvil’s EnchantmentHelper.setEnchantments are the ends the paths meet at, and Enchantment.areCompatible is the exclusivity test wherever one is asked.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Contexts and predicates

Verified against Minecraft 26.3 · Part VII · A command asks is this true, here, of this entity? — and the machinery that answers is the same machinery that decides what a chest contains.

/execute if predicate example:in_the_rain run say wet is a question about the world, and the world is not something a data-pack file may look at. Before anything can be tested, the server has to turn here into something a JSON file is allowed to interrogate: a position, maybe an entity, nothing else. It does that by building a small typed map of parameters and handing it to an object loaded out of a registry, which returns a boolean. That machinery is usually called the loot system, because most of it lives in net/minecraft/world/level/storage/loot — but the keys and the sets it is built from live in net/minecraft/util/context and know nothing about loot at all. Seventeen of the thirty-one parameter sets never roll a loot table, and the set that is enforced is always the caller’s: a loot table’s own declared type is read only by the load-time validator, once a reload, and never consulted by a roll.

Two packages, one machine

The machine is five classes in two packages, and which class sits in which package is the fact the rest of the page hangs off.

flowchart TD
    Callers["block breaks, deaths, chests, commands, selectors, advancements, trades, enchantment effects, fuel, composters"]
    Client["SlotDisplayContext, on the client"]:::client
    subgraph U["util/context"]
        CK["ContextKey, an Identifier plus a static type"]
        CKS["ContextKeySet, the required keys and the allowed"]
        CMap["ContextMap, the checked bag of values"]
    end
    LPar["LootParams, the immutable inputs"]:::server
    LCtx["LootContext, one invocation"]:::server
    CK -->|"declared required or optional by ContextKeySet.Builder"| CKS
    CKS -->|"ContextMap.Builder.buildAndValidate validates against it"| CMap
    Callers -->|"each brings its own key set"| CKS
    Client -->|"builds a ContextMap of its own"| CMap
    CMap -->|"wrapped, with a ServerLevel beside it"| LPar
    LPar -->|"plus a random source and a resolver"| LCtx

Every arrow leads down toward a checked map or the invocation built on it, and the box is the whole of the general-purpose half. The three classes inside it name no item, block, entity or loot table; the two below it are the loot package, and everything that asks a question is outside on the right.

The dividing line is the whole argument of this page. ContextKey, ContextKeySet and ContextMap are three small general-purpose classes in net/minecraft/util/context with no dependency on items, blocks, entities or loot. Everything below them is in the loot package, and everything above them is whoever wants a question answered. Whatever rolls a loot table is one such caller. The others are commands, entity selectors, advancement triggers, villager trades, every conditional enchantment effect, a furnace’s or brewing stand’s fuel and a composter’s layers — and, on the client, SlotDisplayContext, which builds a ContextMap of its own so a recipe book entry can resolve itself into stacks. World generation builds one too, in NoiseChunk, with no key set at all: it is how the blender and the beardifier reach the density functions.

The cast

classpackagewhat it ownswhen
ContextKeyutil/contextone parameter’s name and its Java type, and nothing else—
ContextKeySetutil/contextwhich keys a call site must supply, and which it maybuilt once at class-init
ContextMaputil/contextthe values, and the only place the contract is enforcedServer for loot; the Render thread builds its own for a recipe display, and world generation one per NoiseChunk
LootParamsstorage/lootthe ServerLevel, the map, the dynamic-drop callbacks, the luckServer
LootContextstorage/lootone invocation: the random source, the reference resolver, the visited stackServer
LootItemConditionstorage/loot/predicatesa predicate over a LootContext — the boolean answerServer
ContextFloatProviderstorage/loot/providersa float over a LootContext — the numeric answer, beside ContextIntProvider for an intServer
ValidationContextstorage/lootat load, whether an element asked for a key its set does not havethe worker pool

A key is a name with a type welded to it

ContextKey is an Identifier and a phantom type parameter. It has no value, no default, no validation and no registry — ContextKey.vanilla just makes one in the minecraft namespace, and only twenty-one exist: the sixteen static fields of LootContextParams, the one on SlotDisplayContext that lets a client draw a recipe, and four that world generation declares on Blender and Beardifier. The type parameter is what makes the rest of the system safe: LootContextParams.ORIGIN is a key of Vec3, LootContextParams.TOOL a key of ItemInstance (the read-only item view), LootContextParams.ENCHANTMENT_LEVEL a key of a boxed int, and a reader gets that type back without a cast.

Those twenty-one keys are Java, and a data-pack author never types one of their names. What a JSON file writes instead is a target — this, attacker, target_entity, tool, block_entity — and LootContextArg is the small translator between the two, resolving a target to a key out of three enums nested in LootContext: LootContext.EntityTarget with six entity keys, LootContext.BlockEntityTarget and LootContext.ItemStackTarget with one each. One slot source, RangeSlotSource, adds a target outside them, container, for LootContextParams.CONTAINER.

All three enums resolve through LootContextArg.SimpleGetter, which reads the map with LootContext.getOptional, the one way a context hands out a parameter’s value. A target the current set does not carry therefore evaluates to nothing instead of taking the tick down — which is the second of the three ways a parameter can be missing, two sections on.

A set is a contract, and the caller signs it

ContextKeySet holds two sets: ContextKeySet.required and ContextKeySet.allowed, the second the union of required and optional, and its ContextKeySet.Builder refuses to make one key both in either order. LootContextParamSets registers thirty-one of them into the built-in registry BuiltInRegistries.CONTEXT_KEY_SET, which is what LootContextParamSets.CODEC reads when a data pack names a set by id — and the ids do not always match the field names: LootContextParamSets.PIGLIN_BARTER is barter and LootContextParamSets.ALL_PARAMS is generic. Every set’s keys are tabulated in Loot context parameter sets.

LootContextParamSets.ALL_PARAMS deserves its own warning, because it is the default a loot table with no declared type gets (LootTable.DEFAULT_PARAM_SET) and the set every standalone predicate, item modifier, slot source and number provider file is validated against — and it is all of them. It requires all sixteen keys, and LootContextParamSets.validate fails the bootstrap if any other set allows a key it does not require. The practical consequence: a standalone predicate file is never flagged at load for a key it reads, so one that asks about a block state or a damage source loads clean even though LootContextParamSets.COMMAND — the set /execute if predicate builds — carries neither.

Three ways a parameter can be missing

A mismatch between what a call site has and what an element wants is caught at three different moments, with three different costs, and the three are not alternatives — an element can pass the load-time check and still come back empty at read time. ContextMap.Builder.buildAndValidate is where the contract is enforced, and it is the only place: it throws if the values collected include a key the set does not allow, and again if the set requires a key that is absent. Note what it compares — the keys the caller supplied against the set the caller named. Nothing on this path ever sees the loot table.

  1. At build time. ContextMap.Builder.buildAndValidate throws, naming the offending keys. That is a programming error rather than a data one, and on a tick it takes the tick down with it (a command catches it and fails).
  2. At read time. LootContext.getOptional is the one way a context hands out a parameter’s value (LootContext.hasParameter only asks whether one is there), and it returns nothing for a missing key, so the conditions and functions degrade quietly rather than fail — LocationCheck, MatchBlock and EnchantmentActiveCheck all answer false. So the standalone-file validator’s silence about a block state is survivable: from /execute if predicate such a predicate is quietly false, not fatal.
  3. At load time. ValidationContext.validateContextUsage compares what an element declares it reads — LootContextUser.getReferencedContextParams, overridden by twenty-seven classes — against ContextKeySet.allowed, and reports the difference.

The load-time check is the loose one

It checks against allowed, not required, so an element that reads an optional key passes validation and can still take path 2 at runtime. And what ReloadableServerRegistries.validateLootRegistries does with the collected problems is log them as warnings and fail the reload only on a reference cycle: a predicate that asks for a parameter it cannot have loads fine and misbehaves later. The same validation is a hard error in exactly two places, both applied by a codec — Validatable.validatorForContext for VillagerTrade, and its list form Validatable.listValidatorForContext for every conditional effect in EnchantmentEffectComponents. Neither a trade nor an enchantment effect can name a predicate by id: both are decoded with the world’s registries, before the predicate registry is loaded, so RegistryFileCodec rejects the reference outright.

Inputs, then one invocation

LootParams is the immutable half: a ServerLevel, the ContextMap, a map of LootParams.DynamicDrop callbacks keyed by Identifier, and a float of luck. The ServerLevel is the mechanical guarantee that no loot context is built on the client — LootParams.Builder takes one in its constructor and LootContext.Builder.create dereferences the server off it for the registries, so a ClientLevel cannot produce a context at all.

LootContext is the per-invocation half, and it adds three things LootParams does not have: the chosen RandomSource, a HolderGetter.Provider that resolves references to other loaded elements, and a set of LootContext.VisitedEntry used as a recursion guard. LootContext.pushVisitedElement returns false when the element is already present, which is how LootTable.getRandomItemsRaw refuses a table already rolling — it logs an infinite loop and rolls nothing. The guard is a stack, not a history: LootContext.popVisitedElement takes the entry off again on the way out, so rolling the same table twice in one evaluation is fine and only genuine re-entrancy trips it. Both of the command call sites below — /execute if predicate and the selector’s predicate option — seed it with the top-level predicate before testing, but no predicate reads it back: one that references itself by name never loads, because Validatable.validateHolder reports the cycle as a fatal ValidationContext.RecursiveElementReferenceProblem.

Named random sequences belong to the context, not to the table. LootContext.Builder.create takes an optional Identifier and picks a random source three ways, in order: an explicit source or a non-zero seed handed to LootContext.Builder.withOptionalRandomSeed, else MinecraftServer.getRandomSequence for the named sequence, else Level.getRandom. RandomSequences is a SavedData (level data and rules) that derives each sequence’s seed from the world seed, a salt and the sequence id, which is what makes a named sequence reproducible across restarts. A loot table supplies that identifier from its own field — and so does TradeSet.randomSequence, read by AbstractVillager.addOffersFromTradeSet, which is why a villager’s trade selection is reproducible by the same mechanism and has nothing to do with loot. What a zero seed in that order means for a chest belongs to loot tables.

What reads a context

LootContextUser — what did you read out of the map — is extended by five data-driven types: LootItemCondition, LootItemFunction, NbtProvider, ScoreboardNameProvider and SlotSource. The number providers read the same map through two interfaces of their own, ContextFloatProvider and ContextIntProvider. Each of the seven has its own registry of kinds (data-driven types), and three of them carry the traffic. LootItemCondition is a predicate over a LootContext with twenty types registered in LootItemConditionTypes, from LootItemEntityPropertyCondition and LocationCheck to EnchantmentActiveCheck and MatchBlock; ContextFloatProvider returns a float and has twenty-eight, from ConstantValue and UniformGenerator to EnchantmentLevelProvider, and ContextIntProvider returns an int and has twenty-three. Their codecs are forgiving in the same shape: LootItemCondition.CODEC, ContextFloatProviders.CODEC and ContextIntProviders.CODEC each take a bare id as a reference to an element of their own registry, and the two number codecs take a bare number as a ConstantValue. NbtProvider and ScoreboardNameProvider are the two small ones — a tag out of an entity, a block entity or command storage, and a scoreboard name, fixed or out of a context target — and LootItemFunction belongs to loot tables. One of the seven lives outside the loot package altogether: SlotSource, in net/minecraft/world/item/slot, answers a SlotCollection rather than a boolean or a number, and its six registered kinds compose — GroupSlotSource, FilteredSlotSource, RangeSlotSource, LimitSlotSource, ContentsSlotSource and EmptySlotSource. Its consumers are the SlotLoot entry, which is how a loot table names a slot of the entity it is rolling for, and SlotSourceArgument, the slots argument of /item and /execute if items.

The advancement system tests conditions directly: the player half of every trigger but ImpossibleTrigger, SimpleCriterionTrigger.SimpleInstance.player, is one LootItemCondition in a Holder, named by id or written inline, and EntityPredicate.wrap folds an EntityPredicate into a LootItemEntityPropertyCondition to make one. The rest of net/minecraft/advancements/predicates need not touch a context: a trigger instance can hold an ItemPredicate or a LocationPredicate outright and test it with no context at all, which ConsumeItemTrigger and DistanceTrigger both do.

Who asks, and with which set

Twenty-eight callers. The six in bold ask a question with no loot table anywhere in sight, and six more roll no table either; read the six and skim the rest, which are here as the evidence for the count at the end of the section.

callersetwhen
BlockBehaviour.getDropsLootContextParamSets.BLOCKany block break (block breaking)
Block.dropFromBlockInteractLootTableLootContextParamSets.BLOCK_INTERACTbeehives, cave vines, carving a pumpkin, sweet berries
LivingEntity.dropFromLootTableLootContextParamSets.ENTITYdeath (damage and death)
LivingEntity.dropFromShearingLootTableLootContextParamSets.SHEARINGsheep, mooshrooms, snow golems, bogged
LivingEntity.dropFromGiftLootTableLootContextParamSets.GIFThero gifts, cat gifts, chicken eggs, sniffer digs
LivingEntity.dropFromEntityInteractLootTableLootContextParamSets.ENTITY_INTERACTbrushing an armadillo
RandomizableContainer.unpackLootTableLootContextParamSets.CHESTfirst touch of a structure container
ContainerEntity.unpackChestVehicleLootTableLootContextParamSets.CHESTfirst touch of a chest minecart or chest boat
FishingHook.retrieveLootContextParamSets.FISHINGreeling in — luck is the hook’s plus the owner’s
BrushableBlockEntityLootContextParamSets.ARCHAEOLOGYbrushing suspicious sand or gravel
PiglinAi.getBarterResponseItemsLootContextParamSets.PIGLIN_BARTERbartering
Mob.createEquipmentParamsLootContextParamSets.EQUIPMENTthe EquipmentUser.equip path, for a spawner’s mob
VaultBlockEntityLootContextParamSets.VAULTa trial chamber vault’s display item and its reward
TrialSpawner.ejectReward, TrialSpawnerStateData.getDispensingItemsLootContextParamSets.EMPTYa trial spawner’s reward and its dispensed items
AdvancementRewards.grantLootContextParamSets.ADVANCEMENT_REWARDadvancement loot
Enchantment.damageContext and its four siblingsthe four enchanted sets plus LootContextParamSets.HIT_BLOCKan enchantment effect’s condition
BaseContainerBlockEntity.getLootContextLootContextParamSets.CONTAINER_PROCESSa furnace’s or brewing stand’s fuel
ComposterBlock.addLayerLootContextParamSets.BLOCK_INTERACThow many layers an item adds to a composter
LootCommandblock, entity, chest or fishing/loot
ItemCommands.applyModifierLootContextParamSets.COMMAND/item … modify and the from … modifier forms
ItemCommands.getSlotsFromProviderLootContextParamSets.COMMAND_SLOT_SOURCEthe slots of /item and /execute if items
LootContextSourcesthe three command_compute sets/compute and /data modify … compute
ExecuteCommandLootContextParamSets.COMMAND/execute if predicate
EntitySelectorOptionsLootContextParamSets.SELECTORa selector’s predicate argument
EntityPredicate.createContextLootContextParamSets.ADVANCEMENT_ENTITYevery advancement trigger that tests an entity
ItemUsedOnLocationTrigger, AnyBlockInteractionTriggerLootContextParamSets.ADVANCEMENT_LOCATIONplacing or using an item on a block
DefaultBlockInteractionTriggerLootContextParamSets.BLOCK_USEright-clicking a block, for an advancement
AbstractVillager.addOffersFromTradeSetLootContextParamSets.VILLAGER_TRADErolling and filtering a villager’s offers

Fourteen roll a table; seventeen never do

The bold rows have no loot table anywhere in sight: a boolean, or in the villager’s case a MerchantOffers, is the whole output. ValidationContextSource even keeps a cached LootContextParamSets.ADVANCEMENT_ENTITY context around, because so much of the advancement tree validates against it.

Count sets rather than rows and the picture is starker. Fourteen of the thirty-one are named above by a caller that goes on to roll a LootTable. The other seventeen never do. Six are the bold rows — LootContextParamSets.COMMAND counts among them, because its other caller, /item’s modifier forms, applies an item modifier rather than a table. Five are the enchantment sets, built only by Enchantment to decide whether an effect fires. Five more are built for a number or a set of slots: the three command_compute sets, LootContextParamSets.COMMAND_SLOT_SOURCE and LootContextParamSets.CONTAINER_PROCESS. The seventeenth is LootContextParamSets.ALL_PARAMS, which is never used to build a ContextMap at all: it exists solely as a validation context and as LootTable.DEFAULT_PARAM_SET. That is why this page is not called loot tables.

Four facts about a question with two keys in it

/execute if predicate is a short complete use of the machine: one command, one ContextMap, one predicate, and no loot table anywhere in it.

sequenceDiagram
    participant ExecC as ExecuteCommand
    participant LootP as LootParams
    participant CMap as ContextMap
    participant LootC as LootContext
    participant LIC as LootItemCondition

    Note over ExecC: the argument already holds a Holder, resolved at parse time
    ExecC->>LootP: LootParams.Builder on the level, ORIGIN required, THIS_ENTITY optional
    LootP->>CMap: ContextMap.Builder.<br/>buildAndValidate against LootContextParamSets.COMMAND
    CMap-->>LootP: the checked map, or a throw on a bad key
    ExecC->>LootC: LootContext.Builder.create with no random sequence
    Note over LootC: the random source is Level.getRandom, the resolver is the reloadable registries
    ExecC->>LootC: pushVisitedElement, seeding the recursion guard with this predicate
    ExecC->>LIC: test
    LIC->>LootC: getOptional, the one way to a parameter, null for a missing key
    LIC-->>ExecC: a boolean, and the branch is taken or not

The whole machine, once, with no loot in it. Read the two dashed returns — the first is the only place the contract is enforced, and the last is the only thing the caller gets back.

Three details in that picture are worth pulling out, and a fourth about the call site it does not draw.

The predicate is resolved before the command runs. The argument type is ResourceOrIdArgument.LootPredicateArgument, which accepts either a registry id or an inline SNBT object and hands back a Holder — a reference in the first case, a direct holder over a freshly parsed condition in the second. An unknown id fails at parse time, as a command syntax error, not at execution.

LootContextParamSets.COMMAND is thin. It requires LootContextParams.ORIGIN and optionally allows LootContextParams.THIS_ENTITY, and that is all: a predicate run from /execute cannot see a block state, a damage source or a tool, whatever the standalone-file validator let through. The selector’s version, LootContextParamSets.SELECTOR, differs in exactly one way — it makes the entity required, because a selector always has one.

There is no random sequence. ExecuteCommand passes an empty optional, so Level.getRandom is what a LootItemRandomChanceCondition inside the predicate draws from — not correlated between runs, not reproducible across restarts.

The selector path is the same code, and it is not in the diagram. EntitySelectorOptions builds its context per candidate entity, inside the predicate it hands the parser, and looks the condition up itself through the reloadable registries rather than through the argument type — so a missing predicate there is a silent false, not a syntax error.

Six registries, rebuilt on /reload, and none of them networkable

LootDataType is the six loot registries expressed as data: LootDataType.TABLE over Registries.LOOT_TABLE, LootDataType.PREDICATE over Registries.PREDICATE, LootDataType.MODIFIER over Registries.ITEM_MODIFIER, LootDataType.SLOT_SOURCE over Registries.SLOT_SOURCE, and LootDataType.FLOAT_PROVIDER and LootDataType.INT_PROVIDER over Registries.CONTEXT_FLOAT_PROVIDER and Registries.CONTEXT_INT_PROVIDER. Each pairs a registry key with a LootDataType.ContextGetter — the function that says which ContextKeySet an element of that type is validated against. Every type but tables gets the constant LootContextParamSets.ALL_PARAMS; tables get LootTable.getParamSet, and that context getter is its only caller in the game. That is the sense in which a table’s declared type is never checked at runtime.

Reloadable, and never networkable

All six live in RegistryLayer.RELOADABLE and are rebuilt by every reload (the resource system), which is what makes them the reachable half of the data-driven game: a predicate a pack changed is live after /reload. One step of that rebuild matters here — a predicate names its tags against the ones this reload has just read, so a tag a pack added in the same reload resolves, though what it holds is bound only when the reload is applied. None of the six appears in RegistryDataLoader.SYNCHRONIZED_REGISTRIES, the list RegistrySynchronization.isNetworkable tests, so none is ever packed for a client. What crosses the wire is only the result — a container packet, an item entity, a command’s success.

Both dependants outside this part are Part XIII’s: its commands — this page owns /execute if predicate itself, and what Part XIII owns is the engine it runs in and the selector argument whose predicate option is the other caller — and the advancement system, whose triggers build a context per tested entity or interaction.

For a 1.21-era reader. LootContextParam is ContextKey and LootContextParamSet is ContextKeySet, both moved out of the loot package to net/minecraft/util/context. The predicate library that consumes them has left the misspelled critereon package for net/minecraft/advancements/predicates and net/minecraft/advancements/triggers. NumberProvider is now ContextFloatProvider and ContextIntProvider, and ConditionReference and ContextAwarePredicate are gone: a Holder does their work. The drift table has the rest.

Where to look

Three files, in this order, are the whole argument: ContextKey, ContextKeySet and ContextMap, in net/minecraft/util/context. They are short, they import nothing from the loot package, and reading them is the fastest way to see that the loot system’s foundation is not about loot.

Then LootContextParams and LootContextParamSets for the sixteen loot keys and the thirty-one sets — the second is one long list of builders and repays skimming rather than reading. LootParams and LootContext above them are the two halves of one invocation, and LootContext.Builder.create is where the random source is chosen three ways.

LootItemCondition is the interface with twenty implementations; open two of them, LocationCheck for the ordinary shape and InvertedLootItemCondition for one that holds another in a Holder, and then Validatable.validateHolder for the load-time cycle check in use. ValidationContext.validateContextUsage is the load-time check, and LootDataType is where it is told which set to check against.

For the callers, ExecuteCommand and EntitySelectorOptions are two small complete examples of building a context by hand.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Loot tables

Verified against Minecraft 26.3 · Part VII · A player opens a dungeon chest for the first time, and every item in it comes into existence between the click and the screen.

You break into a mossy cobblestone room, there is a chest against the wall, and you right-click it. Between that click and the inventory screen the server does something it will do exactly once for this chest: it looks up a loot table, rolls it, scatters the results across the empty slots, and throws the recipe away. Until that moment the chest is genuinely empty on disk — the region file holds a table key, a seed where the chest was given one, and no items at all. And the roll is a one-shot in the strictest sense: RandomizableContainer.unpackLootTable clears the stored key before it rolls, and the thing that triggers it is not “a player opens this chest” but “anything reads this container”. A hopper underneath taking one item, or a comparator behind the wall asking how full it is, will commit the roll with no player present, for good — and in vanilla that costs nothing but a bastion chest’s advancement, since no chest table reads your luck.

The typed parameters the table is handed, the predicates it tests and the sets those belong to are not loot machinery — they are the general context engine that enchantment effects, advancement triggers, /execute if predicate and villager trade filters all run on, and contexts and predicates is where they are explained. Loot is that engine’s largest client, fourteen of its thirty-one parameter sets, and one consequence of borrowing an engine is worth knowing before you start: a table’s own declared type is read once at load, by the validator, and never compared against the parameters it is actually handed. Whether a chest table works is entirely down to what the caller put in the map. This page is the worked example: what a table is, how one draw picks an item, and why the chest was empty.

The cast

classwhat it decidesthread
LootTablethe parameter set, an optional random sequence, the pools, the table’s own modifier, and the ways out — LootTable.fill into a container, LootTable.getRandomItems into a list, and the unsplit LootTable.getRandomItemsRaw a nested table usesServer
LootPoolwhether the pool runs at all, and how many draws it makesServer
LootPoolEntryContainerthe entry algebra — ComposableEntryContainer.expand answers did I contributeServer
UniformContainerBaseweight and quality — one of the two places luck reaches anythingServer
LootItemFunctionforty-two registered kinds, all conditional, one optional modifier per tier — several functions make one SequenceFunctionServer
RandomizableContainerthe stored table key and seed, and the one-shot unpackServer
LootContextwhich random source this draw uses, and the recursion guardServer
ReloadableServerRegistriesloading the eight reloadable registries, and validating the six loot ones among themthe worker pool

The chest was empty before you got there

Nothing wrote items into that chest at world generation. MonsterRoomFeature places the room, then places up to two chests, giving each up to three tries at a position — every try needing an air block with exactly one solid horizontal neighbour — and for each chest that lands calls RandomizableContainer.setBlockEntityLootTable with BuiltInLootTables.SIMPLE_DUNGEON and a fresh long from the feature’s own random. What lands on the live ChestBlockEntity is a key and a seed, and nothing else. Structure pieces do the same through StructurePiece.createChest and StructurePiece.createDispenser. BuiltInLootTables holds a hundred and twenty-one named keys and two per-dye-colour families for sheep — one under entities and one under shearing — spread over fifteen directories that are worth a glance for the range of things that roll a table at all: not only chests and dispensers but brushing, shearing, harvesting, carving, decorated pots, mob equipment, spawners and a charged creeper’s.

The seed reaches disk on the next save, and from then on the emptiness is self-perpetuating. RandomizableContainer.trySaveLootTable writes the key — and the seed only when it is non-zero — and answers yes, I handled this, so ChestBlockEntity.saveAdditional never writes an item list. RandomizableContainer.tryLoadLootTable answers the same way on the way back in, and ChestBlockEntity.loadAdditional skips reading items entirely. A chest nobody has touched has no inventory in any file the game has ever written.

What counts as reading it

The opening said anything reads this container, and the rule is exactly that literal. RandomizableContainerBlockEntity overrides five of the container methods so that each unpacks before answering: RandomizableContainerBlockEntity.isEmpty, RandomizableContainerBlockEntity.getItem, RandomizableContainerBlockEntity.removeItem, RandomizableContainerBlockEntity.removeItemNoUpdate and — the surprising one — RandomizableContainerBlockEntity.setItem. Writing into an unrolled chest rolls it first, and the write then replaces whatever the roll put in that slot.

Every way a chest is rolled without its player goes through one of those five, and every one of them unpacks with a null player, so the roll gets no Attributes.LUCK and fires no CriteriaTriggers.GENERATE_LOOT:

  • A comparator reaches Container.getItem over every slot through AbstractContainerMenu.getRedstoneSignalFromContainer (what a comparator can see).
  • A hopper underneath asks the same question before it takes anything — and so does a hopper pointing into the chest, which checks whether the destination is full before it pushes. Both directions commit the roll by reading.
  • Breaking the block does it too, and this is the one players do not expect: BlockEntity.preRemoveSideEffects drops a container’s contents by walking Container.getItem over every slot, so mining an untouched dungeon chest rolls it and scatters the result on the ground, with no player in the roll.

What does not unpack is as precise. Clearable.clearContent and Container.getContainerSize do not, and neither does saving — which is why /data get block on an unopened chest reports the loot table key instead of committing the roll, and why a structure that has never been visited costs nothing to keep.

From the click to the screen

Right-clicking a dungeon chest is one call that does four things in an order worth watching: it resolves a provider, commits the roll, builds a menu, and only then tells the client anything.

sequenceDiagram
    participant SPGM as ServerPlayer<br/>GameMode
    participant SP as ServerPlayer
    participant CBE as ChestBlockEntity
    participant LT as LootTable
    participant ChestM as ChestMenu
    participant Wire as the network

    Note over SPGM,CBE: ChestBlock.useWithoutItem picks the provider, and a single chest is its own
    SPGM->>SP: openMenu, handed the block entity
    SP->>CBE: createMenu, which opens by asking canOpen
    CBE->>CBE: unpackLootTable, inside createMenu, before a menu exists
    Note over CBE,LT: RandomizableContainer.<br/>setLootTable to null <br/>BEFORE the roll
    CBE->>LT: fill, on the CHEST set, with the stored seed
    LT->>LT: getRandomItemsRaw, every pool, inside createStackSplitter
    LT->>LT: getAvailableSlots, then shuffleAndSplitItems across them
    LT->>CBE: setItem, once per surviving stack
    CBE->>ChestM: threeRows over the now-filled container
    SP->>Wire: ClientboundOpenScreenPacket, and only then the contents
    SP->>ChestM: setSynchronizer, from inside ServerPlayer.initMenu
    ChestM->>Wire: one ClientboundContainer<br/>SetContentPacket,<br/>from sendAllDataToRemote

The roll is three messages deep inside RandomizableContainerBlockEntity.createMenu, and the screen is the last three arrows. Look at where the chest is filled relative to where the client is told anything — the contents exist before the screen is asked for.

The click arrives as ServerPlayerGameMode.useItemOn and reaches ChestBlock.useWithoutItem (block interaction), which asks ChestBlock.getMenuProvider for something to open. A single chest is its own provider — the combiner hands back the ChestBlockEntity. A double chest is the interesting case: it gets an anonymous provider wrapped round a CompoundContainer that requires both halves to pass RandomizableContainerBlockEntity.canOpen, unpacks both loot tables itself, and never enters either block entity’s own menu factory.

Opening goes through ServerPlayer.openMenu, which closes whatever menu was already open, allocates a container id, and calls RandomizableContainerBlockEntity.createMenu. Both gates on opening are in one predicate: RandomizableContainerBlockEntity.canOpen adds a spectator clause to the lock check it inherits from BaseContainerBlockEntity.canOpen, and the spectator half bites only while a table is still pending — so a spectator cannot commit the roll by peering into an unopened chest, though they can open one that has already been rolled.

The unpack is RandomizableContainer.unpackLootTable, and its order matters more than anything else on this page. It looks the key up through ReloadableServerRegistries.Holder.getLootTable — which answers LootTable.EMPTY for a missing key, never null — fires CriteriaTriggers.GENERATE_LOOT if a ServerPlayer is doing the opening, and then clears the stored key with RandomizableContainer.setLootTable, before a single die is rolled. It builds the parameters with LootContextParams.ORIGIN at the block centre and, only if a player is present, that player’s Player.getLuck — the live value of Attributes.LUCK, whose default is zero and which nothing but a potion or a command moves (attributes) — and LootContextParams.THIS_ENTITY. Then it calls LootTable.fill with the container, those parameters and the stored seed.

The screen comes last and is no part of the roll. The menu itself is a ChestMenu, built by ChestMenu.threeRows over the container the roll has just filled. ServerPlayer.openMenu sends ClientboundOpenScreenPacket and then calls ServerPlayer.initMenu, which attaches the listener and the synchronizer; AbstractContainerMenu.setSynchronizer ends in AbstractContainerMenu.sendAllDataToRemote, and that is the single ClientboundContainerSetContentPacket carrying the freshly rolled contents (containers and menus).

One roll, drawn

LootTable.fill runs every pool of the table, each pool makes some number of independent draws, and each draw picks at most one entry. That draw is the engine’s smallest complete unit, and it narrows the whole way down — from however many entries a pool declares, through the expansion where composites become candidates, to at most one candidate.

flowchart TD
    A["LootPool.addRandomItems"] --> B{"the pool condition, if there is one"}
    B -->|"it fails"| Z["the pool contributes nothing"]
    B -->|"it passes, or none"| C["draws: rolls plus floor of LootPool.bonusRolls times luck"]
    C --> D["one draw, repeated that many times"]
    D --> E["expand every entry container, in declaration order, into candidates"]
    E --> K["weight: floor of weight plus quality times luck, clamped at zero"]
    K --> L{"is that above zero?"}
    L -->|"no"| M["dropped from this draw entirely"]
    L -->|"yes"| N["kept, and added to the running total"]
    N --> O{"how many candidates survived?"}
    O -->|"none, or the total is zero"| Z2["this draw yields nothing"]
    O -->|"exactly one"| P["taken, with no roll to pick it"]
    O -->|"two or more"| Q["one RandomSource.nextInt over the total, then walk subtracting weights"]

A pool’s draws, each narrowing to at most one candidate. Note the middle arm out of the last diamond, where a draw with one surviving candidate is taken without a roll to pick it.

The one box that hides a fan-out is expand, and what it expands are the five containers:

the containerwhat expanding it yields
AlternativesEntryan or: stops at the first child that contributes
SequentialEntryan and: stops at the first child that does not
EntryGroupevery child expands, and the contribution is ignored
TagEntry in expand modeone candidate per item in the tag
NestedLootTableone candidate that runs every table in its set, or in expand mode one per table

What becomes of the stack that survives — the tiers of modifiers, the splitter, the shuffle and the write — is the rest of this section, the trace above and the scatter below; it is a straight line and needs no second picture.

The algebra is boolean, not weighted. ComposableEntryContainer.expand returns a plain did I contribute, and each composite folds its children into one of those in its own CompositeEntryBase.compose — the two-child case literally calling ComposableEntryContainer.or or ComposableEntryContainer.and, and the longer cases a hand-written loop with the same short-circuit. So AlternativesEntry behaves like a boolean or and SequentialEntry like a boolean and — neither is a weighted choice between branches, and the validator warns about an AlternativesEntry whose non-final children carry no conditions, since a leaf with none always contributes and hides every alternative after it. Nine entry types are registered in LootPoolEntries — one more registry of kinds in a page made of them (data-driven types) — and the thing each one produces is a LootPoolEntry, the candidate the funnel above weighs. They are the leaves LootItem, EmptyLootItem, TagEntry, NestedLootTable, DynamicLoot and SlotLoot, and the composites AlternativesEntry, SequentialEntry and EntryGroup. TagEntry has two modes and they are not variations on a theme: expanded, it becomes one weighted candidate per item in the tag, each carrying the entry’s own modifier; unexpanded, it is a single candidate that emits every item in the tag at once, the modifier running on each.

Luck touches exactly two things, and neither is what players think it is. LootPool.bonusRolls is multiplied by luck and floored to add whole extra draws, and UniformContainerBase.EntryBase.getWeight is weight + quality × luck, floored, then clamped at zero. Because a candidate whose weight comes out at zero or below is discarded rather than merely made rare, a negative quality with enough luck removes an entry from the pool altogether. No vanilla chest table uses either: quality appears only in the fishing table, where Luck of the Sea feeds the hook’s luck. Everything else a player calls luck on a drop is a loot function or condition reading an enchantment level off the tool or off the killer, which is why Fortune has no effect component at all and why Looting’s one, a higher chance of dropping the mob’s equipment, is not what adds to its loot.

Functions apply innermost first. Nothing on this path returns a stack: a draw is handed a consumer to push its results into, and each tier wraps that consumer before passing it down, with LootItemFunction.decorate over its one optional modifier, several functions at one tier being a single SequenceFunction. So a drop passes the entry’s modifier first, then any enclosing composite’s, then the pool’s, then the table’s. All forty-two registered functions, SequenceFunction among them, extend LootItemConditionalFunction, whose LootItemConditionalFunction.apply is final and hands the stack back untouched when its own condition fails — which is why a function with a failing condition is a no-op and not a veto on the drop. Most also mutate the stack in place and return it (SetItemFunction, SmeltItemFunction, DiscardItem, SetRandomDyesFunction, and the three enchanting functions handed a plain book, return a different one), which is safe because every leaf hands out something of its own: LootItem and TagEntry construct fresh stacks and SlotLoot emits copies. DynamicLoot is the exception: it calls straight out to a callback the caller registered with LootParams.Builder.withDynamicDrop, and the one ShulkerBoxBlock.getDrops registers hands back the block entity’s live stacks uncopied, though no vanilla table names it; the one that does, the decorated pot’s sherds, builds fresh stacks.

The scatter

LootTable.fill does not simply place what it rolled. LootTable.getAvailableSlots collects the container’s empty slot numbers and shuffles them; LootTable.shuffleAndSplitItems then pulls the multi-count stacks out of the result list and repeatedly splits one — taking a random amount between one and half its count — until the number of pieces roughly matches the slot count. That is why one rolled stack of arrows arrives as several partial ones in unrelated slots — how many depends on how much of the chest is free. If the pieces outnumber the free slots, the remainder is logged as a warning and silently discarded.

Above that sits LootTable.createStackSplitter, which every public LootTable.getRandomItems and LootTable.fill wraps its output in: it drops items the level’s feature flags disable, and cuts anything at or over its maximum stack size into stack-sized pieces. NestedLootTable deliberately calls LootTable.getRandomItemsRaw instead, so a nested table’s results are split once by the outer table rather than twice. Nesting the same table twice is fine, from two pools or two rolls of one: the recursion guard is a stack that pops on the way out rather than a list of everything seen (contexts and predicates), so only a table genuinely inside itself would trip it, and such a table fails the whole reload: validation reports the cycle as fatal.

Where the randomness comes from

A LootContext picks its random source by trying three things in order — an explicitly supplied source or seed, then the server’s saved sequence for a named random sequence, then the plain Level.getRandom (contexts and predicates owns the mechanism). For a chest the load-bearing part is the condition on the first of the three: a seed is installed only when it is non-zero, the zero LootTable.RANDOMIZE_SEED names.

So a seed of zero means unseeded, is indistinguishable from having no seed at all, and is never written to NBT — which is why a chest given a loot table by command rolls whatever its table’s named sequence is up to (every vanilla table names one; a table that names none falls to the level’s random), where a structure chest, carrying a seed, rolls the same contents whoever opens it and whenever. Neither rolls twice: the key is gone after the first unpack either way. A table that declares a named random sequence draws from a stored, saved one instead of the level random, which is what keeps the same table in the same world reproducible across a restart even when no chest ever carried a seed.

And which thread does the rolling

One, everywhere. Loading is the only part of any of this that is not on the Server thread — one RegistryLoadTask per reloadable registry on the worker pool, with the layer frozen before anything is validated (the resource system). Rolling is on the Server thread without exception, and the guarantee is a type rather than a thread check: a LootParams demands a ServerLevel to be built at all (contexts and predicates). The strongest form of it is a fact about the whole tree: no client class references the loot package at all.

A loot table that travels inside an item

Everything above is a table anchored to a position. One kind is not. DataComponents.CONTAINER_LOOT carries a SeededContainerLoot — a key and a seed living in a stack’s component patch — which a container broken with no player to roll it (an explosion, a piston) writes into the stack it drops when its own loot table copies the component, as the shulker boxes’ do (RandomizableContainerBlockEntity.collectImplicitComponents; creative pick-block writes it too), and which the loot function SetContainerLootTable writes directly, as against SetContainerContents, which writes the stacks themselves. So a shulker box given a loot table and blown up unopened comes out already owing a roll, and carries that debt around in your inventory.

The component is persistent with no network codec of its own, which under the rule on data components means one is derived for it, so it does reach the client. What it cannot carry is the contents: the client has no loot registry at all, and SeededContainerLoot.addToTooltip does not try — it prints the unknown-contents line and nothing else, on either side. That tooltip is the only place in the game where a player is shown, in so many words, that the items do not exist yet.

For a 1.21-era reader. LootPoolSingletonContainer is gone: UniformContainerBase holds weight and quality, with SingleEntryContainerBase and ExpandableContainerBase beneath it. Each list of functions is one optional modifier, several functions making one SequenceFunction, so LootItemFunctions.compose is gone too, and each list of conditions is one optional condition.

Where to look

RandomizableContainer.unpackLootTable is one short method and holds the whole of why the chest was empty, including the order of the clear and the roll. Read RandomizableContainerBlockEntity straight after it for the five overrides that decide what counts as reading.

Then the roll, outside in: LootTable.fill, LootPool.addRandomItems, and UniformContainerBase.EntryBase.getWeight for the two lines where luck touches anything. ComposableEntryContainer is the boolean algebra, and AlternativesEntry beside TagEntry is the shortest way to see that a composite and a leaf are the same interface.

LootItemConditionalFunction is all forty-two functions, and its final LootItemConditionalFunction.apply is why a failing condition is a no-op rather than a veto. LootTable.createStackSplitter and LootTable.shuffleAndSplitItems are the scatter.

Three callers on the other side of the door are worth opening to see the range of what rolls a table: BlockBehaviour.BlockStateBase.getDrops for every block broken (block breaking), LivingEntity.dropFromLootTable for every mob killed (damage and death), and EquipmentUser.equip, the one caller that compares the looked-up table against LootTable.EMPTY by identity to decide whether to bother. EnchantWithLevelsFunction, a function a table runs rather than a caller, is the cost-based function on the loot side of enchanting. Contexts and predicates has the thirty-one sets they build against, and the count of which of them roll a table.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

VIII · The player

Verified against Minecraft 26.3 · Part VIII · The entity a human plays as: what it is made of, when it runs, and the five things it does that the rest of the world does differently.

Everything in Parts IV to VII happens to the world. This part is about the one object in it that argues back. You meet it as friction — the snap back after a laggy jump, the swing that does more damage if you wait, the food bar that empties from sprinting and not from walking, the effect timer that keeps counting while the connection is down — and every one of those is the same thing showing through. A player is an entity like any other, with the same base class, the same tick and the same synched data, and then almost every rule is bent for it: it is ticked twice instead of once, its inventory reports seven slots it does not store, its melee combat has three separate entry points of which the famous one is the least interesting, and the server simulates its movement in full every tick and then throws the answer away. The server is not allowed to be right about the player — nor about anything a player steers, which borrows the player’s authority — and this part is the cost of that.

The shape of the part

Part VIII is a trunk and four branches. Two pages say what a player is and when it runs; everything after them is one thing a player does, and they are independent of each other — except the spear, which is the sword swing’s sequel and should not be watched before it. The inventory has no lecture of its own and does not need one: Part VII stops at the slot, and the container those slots sit in is a section of player anatomy, because what is interesting about it is that seven of its forty-three slots are somewhere else. So the shape is a trunk of two pages, four branches that do not depend on one another, and one sequel — which is what the figure numbers.

flowchart TD
    PA["1 · Player anatomy — eight classes, forty-three slots"]
    TT["2 · The two-phase tick — when it runs, and what is thrown away"]
    IM["3 · Input to movement — walking, and being believed"]
    SS["4 · The sword swing — one integer, and a number rebuilt"]
    SP["5 · The spear — two more melee paths"]
    HE["6 · Hunger and experience — two bars the server owns"]
    SE["7 · Status effects — a list of things happening to you"]
    PA --> TT
    TT --> IM
    TT --> SS
    TT --> HE
    TT --> SE
    SS -- "its sequel" --> SP

The trunk and its four branches, numbered to the watch order: an arrow means the page below assumes the page above, and the four leaving the two-phase tick are independent of one another, so the labelled edge is the only order this part insists on beyond the trunk.

Before you start

Part VI is the hard prerequisite, and two of its pages in particular. Entity anatomy, because a player is a LivingEntity with three rungs added on the server and four on the client, and this part never re-teaches the base; and authority, because every page here rests on it — a Player is client-authoritative on both sides, and the server’s own answer for your movement is thrown away in favour of the number you sent it. If you watch one page from another part first, watch that one. Three more of Part VI’s are assumed rather than re-taught: attributes, because the reach and base numbers of an ordinary swing are attributes; synched entity data; and movement and collision, the pipeline input to movement feeds and never repeats.

Then the server tick and the level tick, because half this part’s timing claims are about which phase something ran in — including the fact that a player’s own physics run after every level has finished. Players and sessions owns how a ServerPlayer comes to exist at all, and how the session it belongs to ends. From Part VII, using an item, because the spear’s charge is an item you use and the meal in hunger and experience is that same machinery from the eater’s end.

Watch in this order

  1. Player anatomy — the vocabulary page: eight classes, two game-mode objects, forty-three slots. There is a rung between LivingEntity and Player — Avatar — with no instance state at all, shared with a posable dummy; and the main-hand item has no storage of its own, being the selected hotbar slot seen through the equipment container a horse also has.
  2. The two-phase tick — one player, one tick, twice. The connection records where you are, runs the whole physics pipeline, and then puts you back: the server keeps the velocity and throws the position away.
  3. Input to movement — W is pressed. A held movement key pressed and released between two ticks never happened, flooding the server with move packets makes the anti-cheat stricter, and the packet that reports your key presses cannot move you but can move a minecart.
  4. The sword swing — left-click on a pig. The attack packet carries one integer and the server rebuilds the rest, charging the hit against a cooldown twice in two different shapes — quadratically on the base damage, linearly on the enchantment bonus — and multiplying the mace’s fall bonus by the critical hit.
  5. The spear — the part’s most surprising lecture. Two components on one item: a stab whose packet has no target in it, and a charge whose damage comes from closing speed and which ignores the attack cooldown entirely.
  6. Hunger and experience — two bars the server owns outright. Walking costs exactly zero exhaustion; a full bar with saturation left heals up to eight times faster than one at eighteen; and the experience packet watches the total, so every mutation that moves only your level has to poison the last-sent value by hand.
  7. Status effects — the part’s closer, and its cleanest statement of the server/client split: the client runs none of an effect’s behaviour — it counts the effect down, draws it, and reads it for its own movement. The server corrects that countdown by testing whether the remaining duration divides by six hundred, so the periodic correction never reaches an infinite effect, whose duration is −1.

Watched as lectures, one and two are the pair to keep together, and four and five are the other pair. Six and seven can be watched in either order, or skipped and returned to.

Where the part stops

Part VIII is the smallest part of the book with a system in it (only Part I is smaller, and Part I is two pages about the program the rest run inside and what the book leaves out) — 32 files and 8,460 lines in world/entity/player, world/food, ServerPlayer and client/player — and the coverage question has an almost trivial answer here: 1% of the part’s lines are named on no page in the book. A player is a small object surrounded by large ones, so the interesting borders are not the ones inside these packages but the ones just outside them.

Upward, the part starts at Avatar rather than at Entity: Avatar sits in Part VI’s package and is explained here, beside the Mannequin — the posable dummy that shares the rung, and the reason the rung exists — and what a player inherits above it belongs to Part VI. Outward, it stops where the player stops being a player. How a hit is resolved once it lands is damage and death in Part VI; what your client is told about everyone else is what the client is told in Part IX; the ledger behind the block you already saw break is prediction and acknowledgement in Part X; drawing a player, its skin and its model parts is Part XI’s; and the chat session a ServerPlayer carries — the public half of message signing, the key itself never leaving the client — belongs to chat and signing. The two game-mode objects, ServerPlayerGameMode and MultiPlayerGameMode, are the sharpest border of all: this part says what they hold, and what they do with a block is Part V’s two click pages.

Three things a reader will look for here have no lecture. Two are declined: Hotbar, and the HotbarManager just outside these packages that keeps the nine saved creative hotbars, belong to the creative screen’s saved hotbars, which this book does not cover; and sleep is half-explained on purpose, with player anatomy naming the fields and the refusals a bed answers with while the everyone is asleep half — the night skip and the weather reset — belongs to the level tick. The third is simply missing, and is the one gap this part admits rather than declines: nobody explains the walk between those two halves — what one player lying down does over the hundred ticks of Player.SLEEP_DURATION that follow, and which of the two owners is driving during them.

Reference this part uses

Attributes, because reach, attack damage, attack speed, sweeping ratio and knockback are all attributes — and attack damage and knockback, two of those that decide what a hit is worth, are not synced to the client at all. Packets for the movement, attack and health packets by name. Data components for the components that make an item a weapon. Damage outside LivingEntity for what a swing meets when the target is not a living entity. Then game rules, level data and rules and diagram lanes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Player anatomy

Verified against Minecraft 26.3 · Part VIII · You open your own inventory and look at what you are made of: six classes deep on your own screen, forty-three slots wide, and the item in your hand has no slot of its own at all.

You are a LivingEntity that a human is steering down a connection. Almost everything on this page follows from that sentence: the class ladder exists to separate what any living thing does from what a thing with an inventory and a game mode does from what a thing with a connection does. Six rungs of it reach from Entity down to the object on your own screen, and five from Entity down to the one the server holds. But two of the things a reader goes looking for are not where they look. One rung of that ladder holds no instance state at all, and the main-hand item has no storage of its own — the hotbar slot you are looking at and the item LivingEntity.getMainHandItem returns are the same bytes, aliased through the equipment container a horse also has.

The cast

classwhat it decidesthread
Avatarthe player-shaped hitbox, the eye height and the two cosmetic synched values — and no instance stateboth
Playereverything a reader means by the player: inventory, abilities, experience, sleep, reachboth
ServerPlayerthe connection, the advancements, the statistics, and the server’s last sent fieldsServer
AbstractClientPlayerthe rung both client players share: the tab-list entry, the skin, the animation stateRender
LocalPlayer / RemotePlayerthe two that sit on it: the one a human steers, and every other player on your screen — interpolated, never derivedRender
Inventorythirty-six stacks, and a window onto EntityEquipment for the restboth
ServerPlayerGameMode / MultiPlayerGameModewhat the current GameType allows, one object per sideServer / Render
Mannequinthe other Avatar: a posable dummy that gets the whole skin pipelineboth

The ladder, and the class in the middle

classDiagram
    class Entity
    <<abstract>> Entity
    class LivingEntity
    <<abstract>> LivingEntity
    class Avatar {
        <<abstract>>
        no instance fields at all
    }
    class Player
    <<abstract>> Player
    class AbstractClientPlayer
    <<abstract>> AbstractClientPlayer
    Entity <|-- LivingEntity
    LivingEntity <|-- Avatar
    Avatar <|-- Player
    Avatar <|-- Mannequin
    Player <|-- ServerPlayer
    Player <|-- AbstractClientPlayer
    AbstractClientPlayer <|-- LocalPlayer
    AbstractClientPlayer <|-- RemotePlayer
    Mannequin <|-- ClientMannequin

Every abstract rung is marked, so the five unmarked classes are the only ones the game instantiates outside its test framework — and the branch beside Player is the reason Avatar exists at all.

Entity and LivingEntity belong to Part VI. The rung below them, Avatar (world/entity), is short and has no instance fields at all — every name below it is a static constant or a static EntityDataAccessor, a key onto the synched table Entity owns, so owning a synched value here means holding the key and not the storage. It owns the player-shaped dimensions (Avatar.POSES, Avatar.STANDING_DIMENSIONS, Avatar.CROUCH_BB_HEIGHT, Avatar.SWIMMING_BB_WIDTH, Avatar.SWIMMING_BB_HEIGHT), the 1.62 eye height (Avatar.DEFAULT_EYE_HEIGHT), the two cosmetic synched values (Avatar.DATA_PLAYER_MAIN_HAND, Avatar.DATA_PLAYER_MODE_CUSTOMISATION) read back through Avatar.getMainArm and Avatar.isModelPartShown, and one abstract method, Avatar.getProfile, returning a ResolvableProfile. That, a setter and a few defaults aside, is the whole class.

It exists because two things in the game are shaped like a person and only one of them is a player. Avatar ships in the server jar, and what it holds is what Player and Mannequin both need and nothing else does: the body, the eye height, the arm you favour and the model parts you have turned off. Everything a player has on top of that — the inventory, the abilities, the hunger, the attribute set — is Player’s, which is why Player.createAttributes is on Player and a mannequin gets the plain living set (attributes). The renderer follows the same line rather than drawing it: AvatarRenderer is generic over an Avatar that is also a ClientAvatarEntity, and exactly two classes satisfy that, AbstractClientPlayer and ClientMannequin. That pairing is worth knowing, because the mannequin repeats Player’s server/client split on its own branch, and in one class instead of two: Mannequin (world/entity/decoration) holds a mutable static factory, Mannequin.constructor, and ClientMannequin.registerOverrides replaces it during client startup, so a mannequin spawned into a ClientLevel is really a ClientMannequin with the full PlayerModel and skin pipeline behind it. The mannequin is therefore a sibling of Player, not of ArmorStand; only the package is shared with the armour stand. It is posable (Mannequin.VALID_POSES), profiled (Mannequin.DATA_PROFILE), describable (Mannequin.DATA_DESCRIPTION) and optionally immovable (Mannequin.DATA_IMMOVABLE).

Player itself is abstract for one method above all: Player.gameMode, returning a nullable GameType. A long tail of its other methods are empty hooks that exist so the two sides can disagree — Player.onUpdateAbilities, Player.awardStat, Player.triggerRecipeCrafted, Player.crit, Player.magicCrit, Player.sendSystemMessage, Player.doCloseContainer, Player.openTextEdit, Player.sendMerchantOffers, Player.handleCreativeModeItemDrop. On Player they do nothing; ServerPlayer, which has somewhere to send a packet, overrides all but the last, and LocalPlayer overrides six, the last among them.

What Player owns

whatthe fieldswho explains it
storagePlayer.inventory, Player.enderChestInventory (a PlayerEnderChestContainer)below
the open windowPlayer.inventoryMenu (final) and Player.containerMenu, which is Player.inventoryMenu when nothing is opencontainers and menus
what the mode allowsPlayer.abilitiesbelow
the food barPlayer.foodDatahunger and experience
experiencePlayer.experienceLevel, Player.experienceProgress, Player.totalExperience, Player.enchantmentSeed, Player.lastLevelUpTime, Player.takeXpDelayhunger and experience
sleepPlayer.sleepCounter, Player.startSleepInBed / Player.stopSleepInBed, the Player.BedSleepingProblem refusals, Player.SLEEP_DURATION (100) and Player.WAKE_UP_DURATION (10)ServerLevel owns the everyone is asleep half
the two attack clocksLivingEntity.attackStrengthTicker and LivingEntity.itemSwapTicker, declared two rungs up but read, reset and incremented only herethe sword swing
cooldownsPlayer.cooldowns, built by Player.createItemCooldowns, which only ServerPlayer overridesusing an item
four synched valuesPlayer.DATA_PLAYER_ABSORPTION_ID, Player.DATA_SCORE_ID, Player.DATA_SHOULDER_PARROT_LEFT, Player.DATA_SHOULDER_PARROT_RIGHTsynched entity data
addressingPlayer.ENDER_SLOT_OFFSET (200), Player.HELD_ITEM_SLOT (499), Player.CRAFTING_SLOT_OFFSET (500), decoded by Player.getSlot — the numbering a command’s slot argument speaksbelow
the odds and endsPlayer.gameProfile, Player.lastDeathLocation, Player.fishing, Player.reducedDebugInfo, Player.lastItemInMainHand, Player.hurtDir, Player.jumpTriggerTime, Player.wasUnderwater—

None of those four synched values is the hand, which is Avatar’s; and a player’s skin is not synched data at all: it arrives out of band, from the tab-list entry. A mannequin’s does travel as synched data, in Mannequin.DATA_PROFILE.

The skin itself is a small closed family in the same package, and it is worth naming because a player’s skin is not on the entity at all and the toggles are one byte there. PlayerSkin is a record of three textures — body, cape and elytra — plus a PlayerModelType (PlayerModelType.SLIM or PlayerModelType.WIDE, the two arm widths) and a secure flag saying whether the textures came signed. It lives on the tab-list entry, not on the player, which is why a skin can change without an entity packet. PlayerModelPart is the other half: the seven toggles in the skin-customisation screen, each a bit in the byte Avatar.DATA_PLAYER_MODE_CUSTOMISATION carries, read back through Avatar.isModelPartShown. Drawing any of it is Part XI’s — the skin and the seven toggles land in an AvatarRenderState at extract time.

Reach is two attributes, not one. Player.blockInteractionRange and Player.entityInteractionRange read Attributes.BLOCK_INTERACTION_RANGE and Attributes.ENTITY_INTERACTION_RANGE (attributes), whose defaults — 4.5 and 3.0 — live on the attributes themselves. Player.DEFAULT_BLOCK_INTERACTION_RANGE and Player.DEFAULT_ENTITY_INTERACTION_RANGE name the same two numbers. The checks the server makes are Player.isWithinBlockInteractionRange and Player.isWithinEntityInteractionRange. Note which class supplies which: Player.createAttributes adds attack damage, attack speed, luck, a player’s own movement speed, the block range, Attributes.BLOCK_BREAK_SPEED, Attributes.SUBMERGED_MINING_SPEED, Attributes.SNEAKING_SPEED, Attributes.MINING_EFFICIENCY, Attributes.SWEEPING_DAMAGE_RATIO and the waypoint attributes, while the entity range comes from LivingEntity.createLivingAttributes.

Two smaller seams: Player.permissions returns PermissionSet.NO_PERMISSIONS and both sides override it — it is what Player.canUseGameMasterBlocks consults alongside Abilities.instabuild for Permissions.COMMANDS_GAMEMASTER — and Player implements ContainerUser, which is how a chest counts who still has it open, over a box Player.getContainerInteractionRange sizes.

Forty-three slots, and one of them is an alias

Inventory implements Container in a particular shape: one Inventory.items list of thirty-six stacks (Inventory.INVENTORY_SIZE) plus a reference to the player’s EntityEquipment. Slots at or above thirty-six are not stored here at all. Inventory.EQUIPMENT_SLOT_MAPPING routes them into the equipment object — the four armour indices, Inventory.SLOT_OFFHAND (40), Inventory.SLOT_BODY_ARMOR (41) and Inventory.SLOT_SADDLE (42) — so Inventory.getContainerSize is forty-three, and a player carries the same body-armour and saddle slots a horse does.

Player.createEquipment returns a PlayerEquipment, which overrides the map so that EquipmentSlot.MAINHAND resolves to Inventory.getSelectedItem. That is the alias: the held item is not stored twice, and the main hand is the selected hotbar slot seen through the equipment interface.

The rest of the class is the vocabulary the whole game uses to put things in a player: Inventory.add, Inventory.getFreeSlot, Inventory.getSlotWithRemainingSpace, Inventory.placeItemBackInInventory, Inventory.findSlotMatchingItem, Inventory.contains, Inventory.removeItem, Inventory.clearOrCountMatchingItems, Inventory.dropAll, Inventory.getSuitableHotbarSlot, Inventory.addAndPickItem and Inventory.pickSlot (pick-block), and Inventory.fillStackedContents, which hands the thirty-six ordinary slots to a StackedItemContents, which keeps the stacks a recipe may draw on and tallies them in StackedContents — the matcher that lives in this package and belongs to recipes. Inventory.save and Inventory.load cover the thirty-six — the equipment half is persisted by LivingEntity — and Inventory.setSelectedSlot throws rather than accept a non-hotbar index.

Why the forty-three need two ticking calls

The split is visible once a tick. Inventory.tick runs over the thirty-six ordinary slots, called from Player.aiStep; EntityEquipment.tick covers the other seven, called from LivingEntity.aiStep two rungs up. Each walks only its own storage, though the two reach into each other — Inventory routes the last seven indices into the equipment, and PlayerEquipment answers the main hand from the inventory. A third walk then crosses the whole forty-three at once — the second half of the player’s tick offers every stack to ServerPlayer.synchronizeSpecialItemUpdates, which is how a filled map gets its update packet (the two-phase tick). Two callers for one tick hook is not an accident of history; it is the storage split showing through.

The other numbering, the one a command speaks

Those forty-three are not the numbering a command speaks. Player.getSlot decodes a second, sparser addressing over the same storage and past it: an index inside the thirty-six is an ordinary slot, Player.ENDER_SLOT_OFFSET plus n reaches the ender chest, Player.CRAFTING_SLOT_OFFSET plus n the four crafting-grid slots, Player.HELD_ITEM_SLOT is the stack on the cursor rather than any stored slot, and anything else falls through to the equipment slots two rungs up. That is the map behind a slot argument in a command.

Abilities, GameType, and the one method that connects them

Abilities is five public booleans and two floats: Abilities.invulnerable, Abilities.flying, Abilities.mayfly, Abilities.instabuild, Abilities.mayBuild, plus Abilities.getFlyingSpeed and Abilities.getWalkingSpeed. It does not serialise itself by hand — it packs into the record Abilities.Packed and Abilities.Packed.CODEC does the work, through Abilities.pack and Abilities.apply.

GameType is the four-constant enum (GameType.SURVIVAL, GameType.CREATIVE, GameType.ADVENTURE, GameType.SPECTATOR) with GameType.DEFAULT_MODE, a GameType.CODEC and a GameType.STREAM_CODEC. The method that matters is GameType.updatePlayerAbilities: the single place in the game that decides which abilities a mode grants. Both sides call it — the server from ServerPlayerGameMode, the client from MultiPlayerGameMode on login, on respawn and on a mode-change event. GameType.isBlockPlacingRestricted is what sets Abilities.mayBuild, and GameType.isSurvival is true for GameType.ADVENTURE too.

The save file does not use the accessors’ names for any of this: Abilities.Packed.CODEC names the keys, and it writes flySpeed where the accessor is Abilities.getFlyingSpeed. While reading that class, note the misspelled constant beside it — Abilities.DEFAULY_FLYING.

That is also where a data pack’s reach over the player ends, before it begins. The attribute defaults Player.createAttributes supplies are code, and the starting GameType comes from the world’s settings and the server’s properties; the ladder, the slot count and what each mode grants are code too, which makes the player one of the few systems in the game a data pack cannot redefine.

The two game-mode objects

ServerPlayerGameMode (server/level)MultiPlayerGameMode (client/multiplayer)
owns the modeServerPlayerGameMode.getGameModeForPlayerMultiPlayerGameMode.getPlayerMode
changes itServerPlayerGameMode.changeGameModeForPlayerMultiPlayerGameMode.setLocalMode
breaking stateServerPlayerGameMode.isDestroyingBlock, ServerPlayerGameMode.destroyProgressStart, ServerPlayerGameMode.hasDelayedDestroyMultiPlayerGameMode.isDestroying, MultiPlayerGameMode.destroyProgress, MultiPlayerGameMode.destroyDelay
the block hooksServerPlayerGameMode.handleBlockBreakAction, ServerPlayerGameMode.useItemOn, ServerPlayerGameMode.useItemMultiPlayerGameMode.startDestroyBlock, MultiPlayerGameMode.continueDestroyBlock, MultiPlayerGameMode.useItemOn, MultiPlayerGameMode.useItem
attacking— (ServerGamePacketListenerImpl handles it)MultiPlayerGameMode.attack, MultiPlayerGameMode.interact
containers—MultiPlayerGameMode.handleContainerInput

ServerPlayerGameMode has exactly one subclass, DemoMode, which MinecraftServer hands a player instead when the server is in demo mode. It watches the level’s own gameTime for the demo’s days, keeping counters of its own only for the intro screen and to space out the reminders: it fires the tutorial prompts on the first morning, a message on each of the DemoMode.DEMO_DAYS, and past DemoMode.TOTAL_PLAY_TICKS it overrides the block-break and use hooks to answer with a reminder instead of doing anything. It is the only place in the game where the kind of game-mode object, rather than the GameType, decides what you may do.

Neither object is held by Player itself, and only one of them is held by a player at all: Minecraft.gameMode holds the client one — LocalPlayer has no such field — while ServerPlayer.gameMode is a field on the server player. Block interaction and block breaking own the block halves of both columns, and prediction and acknowledgement owns the ledger they share — which, note, MultiPlayerGameMode does not hold either: MultiPlayerGameMode.startPrediction reaches for ClientLevel.getBlockStatePredictionHandler per call.

The three sides of one player

Three concrete classes carry a player, and the fourth name in this section is not a fourth side: AbstractClientPlayer is the rung the two client ones share, and it holds what both of them need.

ServerPlayer is everything that needs a server. ServerPlayer.connection is the ServerGamePacketListenerImpl everything else reaches the client through; ServerPlayer.advancements, ServerPlayer.stats and ServerPlayer.recipeBook (a ServerRecipeBook) are the three ledgers a session accumulates; ServerPlayer.chunkTrackingView and ServerPlayer.lastSectionPos record what the client has been sent (what the client is told); and ServerPlayer.chatSession, a RemoteChatSession, holds the public half of message signing, since the key pair never leaves the client (chat and signing). Around those sit the fields that exist because a server has to remember something the world cannot re-derive: ServerPlayer.respawnConfig for where you come back, ServerPlayer.camera for what you are looking through, ServerPlayer.textFilter for what a chat message is allowed to be, ServerPlayer.wardenSpawnTracker for how loud you have been, ServerPlayer.enderPearls for the pearls in flight, and ServerPlayer.containerSynchronizer for the open window’s own outbound channel.

What is worth more than the roll-call is the shape of two rows of fields on it, because both exist for the same reason and answer to different readers. ServerPlayer.lastSentHealth, ServerPlayer.lastSentFood, ServerPlayer.lastFoodSaturationZero and ServerPlayer.lastSentExp turn a difference into a packet — the first three into one, experience into another; a second row, from ServerPlayer.lastRecordedHealthAndAbsorption through ServerPlayer.lastRecordedArmor and on, turns a difference into a scoreboard criterion update instead. Nothing on the server watches a value change: it compares the value against the copy it last acted on. A third pair remembers what the client said rather than what the server found — ServerPlayer.lastClientInput (an Input) and ServerPlayer.lastKnownClientMovement, both explained by input to movement. Players and sessions owns this object’s lifecycle; it is constructed during the configuration phase by PrepareSpawnTask, before the play listener exists.

AbstractClientPlayer adds what a client needs to draw a player and to name one. Its AbstractClientPlayer.playerInfo is the tab-list entry, fetched lazily from the connection — and that entry is enough of a directory that the client registers LocalPlayerResolver in front of the ordinary profile lookup, so a profile the client needs for a skin, a mannequin’s or a head’s, resolves out of the tab list before anything asks a server. It also carries the per-frame animation state AvatarRenderer reads (AbstractClientPlayer.clientAvatarState), AbstractClientPlayer.getSkin and the field-of-view modifier.

LocalPlayer is the one the human steers, and everything it adds is about sending: LocalPlayer.connection (a ClientPacketListener), LocalPlayer.input (a ClientInput at construction, swapped for a KeyboardInput by the connection on login and respawn), LocalPlayer.lastSentInput and the last-sent position block, and LocalPlayer.dropSpamThrottler, which is the client’s own rate limit on throwing things away. The rest is what only a first-person view needs: LocalPlayer.recipeBook (a ClientRecipeBook), LocalPlayer.permissions, LocalPlayer.startedUsingItem, the auto-jump pair, and LocalPlayer.yBob and LocalPlayer.xBob, which lag the view so the first-person hand sways behind it. RemotePlayer is every other player on the client, and adds almost nothing: it sets Entity.noPhysics when it is built (each Player.tick then sets it to whether the player is a spectator), interpolates through RemotePlayer.lerpDeltaMovement, and has an empty RemotePlayer.updatePlayerPose — another player’s pose is told to you, not derived.

Which of the three is allowed to decide anything is Part VI’s authority, stated once there: a Player is client-authoritative on both sides, and the server runs the physics anyway. The two-phase tick is what that costs.

What crosses between them

Very little of a player is entity-synched, so the packets that carry one are their own small set. ClientboundLoginPacket and ClientboundRespawnPacket both carry a CommonPlayerSpawnInfo built by ServerPlayer.createCommonSpawnInfo — which is also where the client’s local game mode comes from. A mode change afterwards arrives as ClientboundGameEventPacket.CHANGE_GAME_MODE or ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, and the abilities as ClientboundPlayerAbilitiesPacket. The hotbar selection travels both ways, as ClientboundSetHeldSlotPacket and ServerboundSetCarriedItemPacket, and the inventory itself through the open menu’s sync — with ClientboundSetPlayerInventoryPacket, built by Inventory.createInventoryUpdatePacket, for each slot Inventory.placeItemBackInInventory fills.

What a player is on disk

Player.addAdditionalSaveData and Player.readAdditionalSaveData are where a player becomes a file: the inventory as a sparse slot/stack list, the selected slot, the sleep timer, the four experience fields including the enchanting seed, the food data, the score, the abilities through Abilities.Packed, the ender chest, and the last death location. ServerPlayer adds the game-type history through ServerPlayer.storeGameTypes, the thrown ender pearls through ServerPlayer.saveEnderPearls, the vehicle through ServerPlayer.saveParentVehicle, and ServerPlayer.SavedPosition — which is read before the entity exists, by the configuration-phase spawn task.

Questions players ask

Why does the creative flag not come from my abilities? Player.isCreative and Player.isSpectator do not read Abilities at all; they compare Player.gameMode against GameType constants. The flags themselves are reached through accessors on Player that consult Abilities for you, and the widest by far is Player.hasInfiniteMaterials, which reads Abilities.instabuild and has about twice the call sites of the next, Player.canUseGameMasterBlocks, which reads the same flag — with Player.preventsBlockDrops, Player.mayBuild and Player.isPushedByFluid well behind. The most permissive flag in the game is also the most widely consulted one.

Whose game mode arrives late — and why is it yours? On the client every player’s mode comes from the tab list: AbstractClientPlayer.gameMode resolves through AbstractClientPlayer.getPlayerInfo, and returns null when there is no entry. For another player that gap cannot be observed — ClientPacketListener refuses to spawn a RemotePlayer whose PlayerInfo has not arrived, and a PlayerInfo starts at GameType.DEFAULT_MODE, so their mode is survival until an update packet says otherwise, never null. The one player who really does have a null tab-list entry is you: LocalPlayer is built during login, before your own entry is sent. What covers the gap is a second, independent source — MultiPlayerGameMode.localPlayerMode, set from the spawn info on login and respawn and from the game-event packet — and that is the one that drives block breaking and Abilities.mayBuild, the one ability no packet carries.

Why does building permission survive a packet that says otherwise? Abilities.mayBuild never goes on the wire, and nothing recomputes it on receipt. ClientboundPlayerAbilitiesPacket carries four flag bits and two floats, none of them the build permission; the client’s copy is written only by MultiPlayerGameMode, on login, respawn and a mode change, so an abilities packet with no mode change leaves it as it was. The other direction is smaller still: ServerboundPlayerAbilitiesPacket carries only the flying bit.

Where to look

Open Player first and read it as the class the two sides argue over: Player.getSlot is the one method that shows the command numbering, Player.createAttributes the one that shows what a player has that a mannequin does not. Then Avatar above it, which takes two minutes and explains the Mannequin beside it. Inventory and PlayerEquipment are the pair to read together, in that order, because the second is three short methods and is the whole of the alias. ServerPlayer and LocalPlayer are the two big faces: read them for their last sent fields rather than end to end. For the modes, GameType.updatePlayerAbilities is the single decision point and ServerPlayerGameMode / MultiPlayerGameMode are the two objects that call it. Two doors this page only points at: PrepareSpawnTask, where a ServerPlayer is built before the play listener exists, and AvatarRenderer with its ClientAvatarEntity bound, which is where the skin and the seven toggles go.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The two-phase tick

Verified against Minecraft 26.3 · Part VIII · One server tick of one player — which happens twice, from two different callers, and the second half throws its own answer away.

Every other entity on the server is ticked once, by the level it stands in. A player is ticked twice: once from the level’s entity loop, and once from its own connection, after every level in the game has finished. Keep the inheritance chain in view for the whole page, because the halves are told apart by how far up it they reach: a ServerPlayer is a Player, which is an Avatar, which is a LivingEntity, which is an Entity, and every one of them but Avatar declares its own tick. The first half never reaches Player.tick: what the level runs besides ServerPlayer.tick is Entity.commonTick, the bookkeeping it gives every entity first, and for a rider the chain of rideTick overrides that ends in the Entity.rideTick calling it — Player.rideTick among them, which dismounts a sneaking rider instead. The one block the two halves have in common is the container-validity check — and the second half is stranger still. The connection records where the player is, runs the entire physics pipeline, and then puts the player back where it found them. The server simulates your movement in full, every tick, and deletes the result: what it keeps is the velocity, and that is the number the anti-cheat allows for when it checks your reported motion.

The cast

classwhat it decidesthread
ServerLevelphase one: ticks the player in entity order, inside the level tickServer
ServerPlayerboth halves — ServerPlayer.tick and ServerPlayer.doTick overlap in one block onlyServer
ServerGamePacketListenerImplphase two: the record–simulate–snap-back bracketServer
PlayerPlayer.tick and Player.aiStep — on the server, reached from phase two only; on the client, from LocalPlayer.tick (and Player.tick from RemotePlayer.tick)both
Inventorythe thirty-six ordinary slots’ per-tick item hookboth
FoodDatahunger, regeneration and starvation, ticked after the physicsServer
AbstractContainerMenuthe open window, diffed against what the client was toldServer
LocalPlayerthe client’s own single tick, gated on the level having loadedRender

Phase one: what the world does to this player

ServerPlayer.tick is called by the level’s entity loop — through ServerLevel.tickNonPassenger when the player is walking, and through ServerLevel.tickPassenger and Entity.rideTick when mounted, each time after Entity.commonTick has counted down Entity.invulnerableTime, saved last tick’s position and advanced the tick count — and players are ticked there whether or not their chunk is entity-ticking (the level tick). It runs late in ServerLevel.tick: after the block ticks and the chunk source, before the block entities.

It does not call Player.tick. What it does instead is the outside world’s business with the player: ServerPlayerGameMode.tick for block-breaking progress and the delayed destroy (block breaking), the countdown of LivingEntity.damageCooldownTime, the hurt cooldown, AbstractContainerMenu.broadcastChanges on the open menu followed by closing it if it is no longer valid (containers and menus), snapping the player to the camera entity when one is set, the per-tick advancement criteria and a flush of the dirty ones, the warden spawn tracker, and ServerPlayer.updatePlayerAttributes. It is not quite connection-free: its very first statement is the connection’s client-load timeout (players and sessions).

Phase two: what this player would do if it simulated itself

ServerPlayer.doTick is called by ServerGamePacketListenerImpl.tickPlayer, from the connection tick, after every level has ticked. This is the half that calls up into Player.tick and LivingEntity.tick, so the player’s physics are simulated here. It then ticks FoodData.tick, the play-time statistics, ServerPlayer.synchronizeSpecialItemUpdates over all forty-three slots (player anatomy), and every has this changed since I last sent it comparison that produces ClientboundSetHealthPacket and ClientboundSetExperiencePacket (hunger and experience). This is also where the shared block turns up: ServerPlayer.doTick re-runs the same AbstractContainerMenu.stillValid check phase one ran, and closes the menu if it now fails — the only work both halves do.

The first group of those is gated, and the gate is a single condition: a spectator that is touching an unloaded chunk skips it. Behind that gate sit Player.tick, the container check, FoodData.tick and the play-time statistics; the forty-three-slot sweep and every last-sent comparison run regardless. So a spectator drifting out of the loaded world stops being simulated and keeps being reported on.

Player.aiStep, reached from inside that, is where the two item-tick calls happen and in which order: Inventory.tick over the thirty-six ordinary slots, before EntityEquipment.tick covers the other seven from LivingEntity.aiStep — why there are two of them is the forty-three slots (player anatomy). It is also the item and orb pickup sweep, gated on being alive and not a spectator; what the sweep does with an orb once it touches one is hunger and experience.

The division is clean enough to use as a rule while reading the rest of this part. If the thing you are looking for is the world acting on the player — the menu’s change broadcast, the breaking timer, the spectator camera, the advancement criteria — it is in phase one. If it is the player acting — physics, hunger, effects, item ticking, the health, food and experience packets — it is in phase two.

Both halves, in the order they run

One object, one tick, two callers and two bands — and the one call that appears in both of them is the container check.

sequenceDiagram
    participant SL as ServerLevel
    participant SP as ServerPlayer
    participant SPGM as ServerPlayer<br/>GameMode
    participant ACM as Abstract<br/>ContainerMenu
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant FD as FoodData

    rect rgba(0, 0, 0, 0.04)
        Note over SL,FD: phase 1 — the entity loop, inside the level tick
        SL->>SP: commonTick, then tick — never up to Player.tick
        SP->>SPGM: tick — the breaking timer and the delayed destroy
        SP->>ACM: broadcastChanges, then stillValid
        SP->>SP: updatePlayerAttributes — the reach and waypoint modifiers
    end
    rect rgba(0, 0, 0, 0.04)
        Note over SL,FD: phase 2 — the connection tick, after every level
        SGPL->>SGPL: resetPosition — this position becomes firstGood
        SGPL->>SP: doTick, inside the bracket
        SP->>SP: Player.tick, then LivingEntity.tick — the physics
        SP->>ACM: stillValid — the only work both halves do
        SP->>FD: tick — hunger, regeneration, starvation
        SP->>SGPL: ClientboundSetHealthPacket, if a watched field differs
        SGPL->>SP: absSnapTo — back to firstGood, keeping the rotation
    end

The two bands are the two callers, and ServerPlayer is one lane in both because it is one object: the physics the page is about happen inside the second band and their position is undone by its last arrow, and AbstractContainerMenu is the only other lane both bands touch.

The bracket, and what survives it

ServerGamePacketListenerImpl.tickPlayer is a bracket around one call. ServerGamePacketListenerImpl.resetPosition records the player’s current position into the firstGood… and lastGood… fields — good meaning this is the last place the server was willing to agree you were — then ServerPlayer.doTick runs, and then the player is snapped back to the recorded position with Entity.absSnapTo, keeping only the rotation. What runs inside it is the whole of LivingEntity.aiStep, LivingEntity.travel and Entity.move. The rest of the method is the anti-cheat that rides along in the same bracket: the floating too long kick, and the same record-and-check done again for the vehicle the player is steering. On foot the authoritative position moves in ServerGamePacketListenerImpl.handleMovePlayer or in a teleport, never here. Riding is one exception, and it is phase one that makes it one: Entity.rideTick ticks the passenger and then has the vehicle reposition it, so a rider’s real position is written by the level’s loop before the bracket ever opens, and the snap-back at the end of the bracket puts it back there. A spectator looking through another entity is the other, for the same reason: phase one snaps them to it.

What survives the snap-back is Entity.getDeltaMovement — the velocity the anti-cheat allows for when it measures the client’s reported displacement (input to movement) — plus everything non-positional the tick did: drowning, burning, effects, hunger, the last-sent diffs. Both halves run every tick whether or not a packet arrived, and packets are drained before either of them. What phase two tells the client about its own player leaves at once: Connection.tick flushes the channel right after it has run the listener that called ServerPlayer.doTick (the server tick).

Not all of a player’s state is written inside the two halves: the handlers drained before the tick write it too, on the server thread — the position above is one — and the chat and command handlers write one field of it off that thread altogether. Almost every game handler defers to the owning thread before it touches anything (the server tick owns the rule and counts the exceptions); the chat handlers are among the ten that do not. ServerGamePacketListenerImpl.tryHandleChat reads ServerPlayer.getChatVisibility and calls ServerPlayer.resetLastActionTime on the Netty thread, before it hands the rest over (chat and signing) — one field on a ServerPlayer read and another written outside both halves and outside the server thread.

The pairing that makes the bracket necessary is Part VI’s authority: Player.isClientAuthoritative is an unconditional yes on both sides, which denies a ServerPlayer local-instance authority, while Player’s move-simulation type and its Entity.isEffectiveAi override make Entity.canSimulateMovement and Entity.isEffectiveAi true on the server anyway. So the pipeline runs and its answer is not believed.

The client ticks its player once

LocalPlayer.tick runs from ClientLevel’s entity tick on the Render thread, with its entire body gated on the connection reporting that the level has loaded. Minecraft.gameMode is ticked separately, and earlier in Minecraft.tick than the entity tick. ClientInput.tick is called from inside LocalPlayer.aiStep, so input is sampled inside the tick, not pushed from the key callback — though the method doing the sampling is KeyboardInput.tick; ClientInput.tick itself is empty.

Questions players ask

If the server ticks my player from the connection, does a silent client stop being ticked? No. ServerPlayer.doTick runs every tick regardless of traffic, and so does ServerPlayer.tick. Only two things hold phase two back, and neither is silence: MinecraftServer.isPaused — which only an integrated server ever reports true — short-circuits the whole bracket before ServerPlayer.doTick is called at all, and the spectator gate above skips its first half. What stops a silent client moving is neither: it is the snap-back, which undoes every position the simulation produced.

Why does fall damage come from the packet handler? Because the branch inside Entity.move is the one gate there that reads local-instance authority alone, which a ServerPlayer fails (authority). Entity.doCheckFallDamage on the movement-packet path does it instead, with the client’s own reported delta — which is the two-phase split showing up as a damage number.

Is a mounted player different? In phase one, where Entity.rideTick ticks the player and then has the vehicle place it; and in the bracket, which skips the player’s floating check and, for a player steering, runs the vehicle’s record-and-check instead. The movement packets a passenger sends are judged by an almost entirely separate piece of code too — see input to movement.

Where to look

Read the two halves side by side: ServerPlayer.tick and ServerPlayer.doTick are both short, and the second is the one with the spectator gate at the top of it. Then ServerGamePacketListenerImpl.tickPlayer for the bracket itself — the record, the call, the Entity.absSnapTo, and the two floating checks that ride along after it. Player.aiStep is where the item ticks and the pickup sweep happen, and LocalPlayer.tick is the client’s whole one-pass answer to all of it, worth reading immediately afterwards for the contrast. If you want the callers rather than the callees, ServerLevel.tickNonPassenger is phase one’s.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Input to movement

Verified against Minecraft 26.3 · Part VIII · W is pressed: the key becomes a boolean, the boolean becomes a velocity, the velocity becomes a packet, and the server decides whether to believe it.

You press W. Nothing happens for up to a twentieth of a second, because the key was not pushed anywhere — it was written into a boolean that the next tick will read. That tick turns seven booleans into a velocity, the velocity into a position, and the position into a packet; the server then decides whether to believe the packet. It usually does. The player, and whatever it steers, is what the server does not control, and everything here exists to reconcile that.

The surprising part is what deciding costs you. Flooding the server with move packets makes the check stricter, not looser, a held key pressed and released between two ticks never happened at all, and the packet that reports your key presses cannot move you — though it can move a minecart. The server has an expected velocity to compare yours against because it simulates your player every tick and throws the answer away (the two-phase tick).

The cast

classwhat it decidesthread
KeyboardHandlerthat a key went down, when nothing is in the way of itRender
KeyMappingwhat that key is bound to, and whether it is heldRender
KeyboardInputseven booleans and a normalised vector, once per tickRender
LocalPlayerthe movement itself, and what is worth sendingRender
ServerGamePacketListenerImplwhether to believe it, and where the player really isServer
ServerPlayerwhat the client last said: the input, and the known movementServer

Who is allowed to decide any of this is Part VI’s authority, and this page assumes it in one sentence: the server simulates your player and is not allowed to believe the result. Everything below is what that costs, and the cost is paid twice — once on the way out, where the client decides, and once on the way in, where the server checks.

What each side holds

On the client

  • KeyMapping — one object per bindable action, holding KeyMapping.isDown and KeyMapping.clickCount. Only the first of those matters here: the movement keys are polled with KeyMapping.isDown and never drained with KeyMapping.consumeClick, so a key held for ten ticks reads down ten times. What a mapping is, how a press reaches one, what a toggle does to it and why a sneak toggle comes back when the inventory closes, and a held sneak only if its key is still down (and not on macOS) all belong to input and keybinds.
  • Options — the movement bindings: Options.keyUp, Options.keyDown, Options.keyLeft, Options.keyRight, Options.keyJump, Options.keyShift, Options.keySprint. Plus Options.autoJump and Options.sprintWindow (the double-tap window in ticks, default seven, zero to disable).
  • Input (world/entity/player) — a shared record of seven booleans (forward, backward, left, right, jump, shift, sprint) with Input.EMPTY and an Input.STREAM_CODEC that packs all seven into one byte; the bit values are named Input.FLAG_FORWARD … Input.FLAG_SPRINT.
  • ClientInput — ClientInput.keyPresses (an Input) plus ClientInput.moveVector (a Vec2, where Vec2.x is the left impulse and Vec2.y the forward one). ClientInput.makeJump is how auto-jump fakes a press. ClientInput.tick is empty; the subclass that actually reads the keyboard is KeyboardInput, whose KeyboardInput.tick builds a fresh Input from the seven KeyMapping.isDown values, maps each pair to −1, 0 or +1 with KeyboardInput.calculateImpulse, and normalises the resulting vector.
  • LocalPlayer — one block of fields whose entire job is what did I last tell the server. Five of them shadow the position and rotation (LocalPlayer.xLast, LocalPlayer.yLast, LocalPlayer.zLast, LocalPlayer.yRotLast, LocalPlayer.xRotLast), two shadow the ground and collision flags (LocalPlayer.lastOnGround, LocalPlayer.lastHorizontalCollision), one shadows the key set (LocalPlayer.lastSentInput), one the sprint state (LocalPlayer.wasSprinting), and one counts up to a re-send that happens whether anything changed or not (LocalPlayer.positionReminder, against LocalPlayer.POSITION_REMINDER_INTERVAL, twenty). The rest of the block is local state nobody is told about: LocalPlayer.sprintTriggerTime for the double-tap, LocalPlayer.autoJumpEnabled and LocalPlayer.autoJumpTime for auto-jump, and LocalPlayer.crouching.

LocalPlayer.input starts as a bare ClientInput and is replaced with a KeyboardInput by ClientPacketListener on login and on respawn — which is why a respawned player’s input object is a different one.

On the server

ServerGamePacketListenerImpl holds the whole judgement in four groups of fields, and the groups are the argument. Two positions, not one: ServerGamePacketListenerImpl.firstGoodX and its siblings are where the connection tick found the player at the top of the bracket, and ServerGamePacketListenerImpl.lastGoodX and its siblings are the last position it accepted from you. Three teleport fields — ServerGamePacketListenerImpl.awaitingTeleport, ServerGamePacketListenerImpl.awaitingTeleportTime and ServerGamePacketListenerImpl.awaitingPositionFromClient — hold the handshake open while a rubber-band is in flight. Two counters, ServerGamePacketListenerImpl.receivedMovePacketCount against ServerGamePacketListenerImpl.knownMovePacketCount, are how the speed budget learns how many packets arrived since the last tick, with ServerGamePacketListenerImpl.receivedMovementThisTick and ServerGamePacketListenerImpl.receivedPositionThisTick the flags that last one client tick. And a floating pair, ServerGamePacketListenerImpl.clientIsFloating and ServerGamePacketListenerImpl.aboveGroundTickCount. Two of those four groups, the positions and the floating pair, have a vehicle twin beside them, with ServerGamePacketListenerImpl.lastVehicle saying which vehicle — ServerGamePacketListenerImpl.vehicleFirstGoodX, ServerGamePacketListenerImpl.vehicleLastGoodX, ServerGamePacketListenerImpl.clientVehicleIsFloating — which is the first sign that a vehicle is judged by its own cut-down copy of this code.

On ServerPlayer itself there are only two: the observed per-tick displacement, ServerPlayer.lastKnownClientMovement, read back through ServerPlayer.getKnownMovement and ServerPlayer.getKnownSpeed, and the raw key state, ServerPlayer.lastClientInput.

The thresholds below are given as numbers. Where one has a name a reader can grep for, it is a compile-time constant, which a decompile shows as its number at every use: ServerGamePacketListenerImpl.MAXIMUM_FLYING_TICKS is the floating budget’s 80 ticks, and ServerGamePacketListenerImpl.CLIENT_LOADED_TIMEOUT_TIME the client-load timeout’s 60.

Sampled once a tick, judged once a tick

Keys are sampled inside the tick, not pushed from the callback. The movement half of KeyboardHandler.keyPress sets KeyMapping.isDown and bumps KeyMapping.clickCount, and does even that only when no screen is open; everything else the method does — the screen’s own key handling, the debug keys, the pause — reaches a KeyMapping only to clear one or to hold the debug modifier down. A release is recorded screen or no screen, unless an open screen consumes it, and a consumed release would leave a mapping stuck down were it not already released when the screen opened, which belongs to input and keybinds, along with when the callback runs. The read happens once per game tick, deep inside LocalPlayer.aiStep, which calls ClientInput.tick — the empty base method whose KeyboardInput override does the work.

Mouse look is the exception, and it is the one input on this page not sampled once a tick. MouseHandler.handleAccumulatedMovement runs per frame, in Minecraft.runTick after the tick loop, and — when the window is active and the mouse grabbed — MouseHandler.turnPlayer calls Entity.turn directly. Rotation is therefore finer-grained than position, which is why a packet can carry a rotation the tick never saw a keypress for. The sensitivity curve inside MouseHandler.turnPlayer and the rest of the gates on it belong to input and keybinds.

On the server the ordering is the whole story:

  1. MinecraftServer.processPacketsAndTick drains PacketProcessor before MinecraftServer.tickServer. Every movement packet for the tick is applied first, ahead of any level ticking.
  2. The levels tick.
  3. MinecraftServer.tickChildren reaches its connection phase, and ServerGamePacketListenerImpl.tick runs ServerGamePacketListenerImpl.tickPlayer — the simulate-and-discard step, and the place the floating check is enforced. A paused server short-circuits before it.

W, held down: what the client decides and sends

The press and the reading of it are in different ticks, which is the first thing the figure shows.

sequenceDiagram
    participant KH as KeyboardHandler
    participant KM as KeyMapping
    participant KI as KeyboardInput
    participant LP as LocalPlayer
    participant Wire as the network

    KH->>KM: set, then click — isDown goes true, and nothing else yet
    rect rgba(0, 0, 0, 0.04)
        Note over KH,Wire: the next client tick — LocalPlayer.tick, then LocalPlayer.sendChanges
        LP->>KI: tick, from inside aiStep — the override that does the work
        KI->>KM: isDown, once for each of seven mappings
        LP->>LP: applyInput — moveVector becomes xxa and zza, jump becomes jumping
        LP->>LP: travel — travelInAir, then Entity.move, unguarded
        LP->>Wire: ServerboundPlayerInputPacket, only if the key set changed
        LP->>Wire: ServerboundMovePlayerPacket.<br/>PosRot
    end

Nothing happens at the callback but a flag: the band is one client tick, and every key the tick reads is read through KeyMapping, which is why a press and a release of a held key inside one band never happened at all — and it is LocalPlayer.sendPosition that decides which of the move variants leaves at the foot of it.

The client half

KeyboardInput.tick builds an Input from the seven keys and a normalised Vec2. LocalPlayer.applyInput — overriding a LivingEntity.applyInput that does nothing but decay them — turns that into the LivingEntity.xxa and LivingEntity.zza movement fields, after passing it through LocalPlayer.modifyInput: a flat 0.98 scaling, then the item-use slowdown (from LocalPlayer.itemUseSpeedMultiplier), Attributes.SNEAKING_SPEED when moving slowly, and LocalPlayer.modifyInputSpeedForSquareMovement, the diagonal correction. From there it is ordinary movement and collision: Player.travel — a real override, handling passengers, the swimming look nudge and the creative-flight damping — then LivingEntity.travel → LivingEntity.travelInAir → LivingEntity.handleRelativeFrictionAndCalculateMovement → Entity.moveRelative → Entity.move.

Sprint is decided in LocalPlayer.aiStep before that, and it is a rising edge, not a release. LocalPlayer.aiStep snapshots the forward impulse before ticking the input, so the value it later tests is the previous tick’s; LocalPlayer.canStartSprinting requires the current tick’s. The pair means the double-tap window — Options.sprintWindow, seven ticks by default — is armed on the first press and consumed on the second. Letting go is not what arms it, and letting go is not what cancels it either: sneaking, using an item that forbids sprinting or walking backwards clear LocalPlayer.sprintTriggerTime outright, and LocalPlayer.shouldStopRunSprinting ends a sprint already running. Auto-jump is LocalPlayer.updateAutoJump (called from LocalPlayer.move) setting LocalPlayer.autoJumpTime, which makes the next tick call ClientInput.makeJump.

What goes on the wire

Both packets leave from LocalPlayer.sendChanges, which Minecraft.tick calls once the level has ticked its entities and block entities, and for a player on foot LocalPlayer.sendPosition picks the variant: ServerboundMovePlayerPacket.PosRot when both changed, ServerboundMovePlayerPacket.Pos or .Rot for one, ServerboundMovePlayerPacket.StatusOnly when only the ground or collision flag changed, and nothing at all otherwise — except that a position is re-sent every twenty ticks regardless, via LocalPlayer.positionReminder. “Changed” is not the same test for the two halves: rotation compares exactly, while position must have moved by more than 2×10⁻⁴ blocks. And all of it but the sprint command sits behind LocalPlayer.isControlledCamera, so while spectating another entity a client sends no move packets at all, not even the reminder — which has one side effect nobody would look for here, because LocalPlayer.sendPosition is also where Options.autoJump is read back into LocalPlayer.autoJumpEnabled, so the setting quietly stops tracking for a passenger or a non-camera player. The two booleans ride in one byte (ServerboundMovePlayerPacket.FLAG_ON_GROUND, ServerboundMovePlayerPacket.FLAG_HORIZONTAL_COLLISION). ServerboundPlayerInputPacket is sent only when the key set changes, and ServerboundClientTickEndPacket — a zero-byte singleton — closes every client tick that has a level and is not paused.

Elytra, which is its own round trip

LocalPlayer.aiStep asks Player.tryToStartFallFlying and, if it says yes, sends ServerboundPlayerCommandPacket.Action.START_FALL_FLYING — the server runs the same method on receipt. The server may disagree and call LivingEntity.stopFallFlying, and the flight itself is LivingEntity.updateFallFlying and LivingEntity.travelFallFlying.

What the server does with the packet it gets

Everything above happens before the packet exists. What follows is one handler, ServerGamePacketListenerImpl.handleMovePlayer, and two judgements inside the private ServerGamePacketListenerImpl.handlePlayerPositionChange it calls — the method a teleport’s acknowledgement is run through too — and then, because a judgement can go against you, a handshake for putting you back.

The two checks, in the order they run

sequenceDiagram
    participant Wire as the network
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SP as ServerPlayer

    rect rgba(0, 0, 0, 0.04)
        Note over Wire,SP: the packet drain, before any level ticks
        Wire->>SGPL: ServerboundMovePlayerPacket.<br/>PosRot
        SGPL->>SGPL: containsInvalidValues, then the two clamps
        alt moved too quickly
            SGPL->>Wire: ClientboundPlayerPositionPacket, then return — nothing below runs
        else within the budget
            SGPL->>SP: move, with MoverType.PLAYER
            opt a residual over 0.0625, or a new collider
                SGPL->>Wire: ClientboundPlayerPositionPacket, awaiting an ack
            end
        end
    end
    rect rgba(0, 0, 0, 0.04)
        Note over Wire,SP: the connection phase, after every level has ticked
        SGPL->>SP: doTick, then absSnapTo — simulate the tick and discard it
    end

The two bands are two phases of one server tick, and the rubber-band is the only arrow here that is conditional twice over: it needs the move applied first, and then either arm of the disjunction below.

ServerGamePacketListenerImpl.handleMovePlayer begins with ServerGamePacketListenerImpl.containsInvalidValues, which disconnects on NaN coordinates and non-finite rotations — an infinite coordinate survives it and is clamped instead, to ±3×10⁷ horizontally by ServerGamePacketListenerImpl.clampHorizontal and ±2×10⁷ vertically by ServerGamePacketListenerImpl.clampVertical — and it disconnects a client that sends a second position before the packet that closes its tick. It then discards the position entirely while a teleport is outstanding, and ServerGamePacketListenerImpl.handlePlayerPositionChange takes the rest: for a passenger it applies rotation only — snapping the position back with Entity.absSnapTo and re-registering the chunk position, which is not quite “returns early” — and it short-circuits a sleeping player, teleporting them back if they claim to have moved more than a block. Then two checks:

  • moved too quickly: the squared distance from firstGood… minus Entity.getDeltaMovement().lengthSqr() against a budget of 100 per packet, or 300 while fall-flying, scaled by how many move packets arrived since the last tick. Both sides are squared, so 100 is a hundred blocks squared — about ten blocks a tick. The whole check, and the packet counter it uses, is gated on TickRateManager.runsNormally, so a frozen world does no speed checking at all, though a stepped tick does. It is also skipped for the singleplayer host, during a dimension change, and when GameRules.PLAYER_MOVEMENT_CHECK is off (GameRules.ELYTRA_MOVEMENT_CHECK covers the elytra case). Failure teleports the player back and returns.
  • The move is then applied — Entity.move with MoverType.PLAYER — and moved wrongly measures what is left over: a residual above 0.0625 while not changing dimension, sleeping, creative, spectating or inside LivingEntity.isInPostImpulseGraceTime (the mace and wind-charge exemption, which the Lunge enchantment’s impulse opens too and which counts itself down unless a pearl, chorus fruit, fall damage or another explosion ends it first). The rubber-band that follows is a disjunction: either that failure with a demonstrably clear old box, or ServerGamePacketListenerImpl.isEntityCollidingWithAnythingNew reporting the player ended up inside a collider it was not already inside — which fires whether or not the residual check failed. Both arms are additionally suppressed for a no-physics or sleeping player. The vertical half of that residual is dead code: the guard that zeroes it is a disjunction true for every finite double, so the 0.0625 test is horizontal-only in practice, in the vehicle handler as well as this one.

Accepting means Entity.absSnapTo, ServerChunkCache.move, Entity.setOnGroundWithMovement, Entity.doCheckFallDamage, ServerGamePacketListenerImpl.handlePlayerKnownMovement and ServerPlayer.checkMovementStatistics — the walked-distance statistics are computed from the client’s reported delta, never from a simulation. The server also infers the jump, before the move is applied and whether or not it is then accepted: a packet that reports leaving the ground while moving upward, from a player the server has on the ground, calls LivingEntity.jumpFromGround on the player’s behalf.

Where the velocity everything downstream reads comes from

The reported delta is stored by ServerPlayer.setKnownMovement, and ServerGamePacketListenerImpl.handleClientTickEnd zeroes it if no move was accepted during that client tick. That is what everything downstream reads when it wants the player’s velocity — whether a swing sweeps, what a spear’s charge does, the speed a fired projectile inherits, leash physics — and it is why a client that stops sending is treated as stationary rather than as still coasting.

The teleport handshake, and what it does not undo

ServerGamePacketListenerImpl.teleport bumps ServerGamePacketListenerImpl.awaitingTeleport, moves the player with Entity.teleportSetPosition, records ServerGamePacketListenerImpl.awaitingPositionFromClient and sends ClientboundPlayerPositionPacket (a PositionMoveRotation plus a set of Relative flags saying which fields are deltas). ServerGamePacketListenerImpl.updateAwaitingTeleport re-sends after more than twenty ticks without an acknowledgement, when the next move packet arrives, and until one does, every incoming move packet contributes rotation only. That is also the answer to what happens to the movement you made while the rubber-band was in flight: nothing replays it. The client snaps, the packets it had already sent are discarded as positions, and the ticks you walked between the rejection and the snap are simply gone — unlike a block prediction, which survives the same teleport on purpose. The client replies with ServerboundAcceptTeleportationPacket, which carries the position it snapped to and which the server runs through the same checks as a move, then calls BlockStatePredictionHandler.onTeleport, which does not drop its outstanding block predictions: it records the sequence the teleport arrived at, so that a receipt at or below that number settles those predictions without the position snap that would otherwise shove you back (prediction and acknowledgement). On the receiving end ClientPacketListener.handleMovePlayer applies the position only when the player is not a passenger, and never interpolates: it passes the interpolate flag as a literal false, so your own player is always snapped, and the 4096-blocks-squared jump test inside it decides anything only when a caller asks to interpolate, which this one never does. ClientboundPlayerRotationPacket is the rotation-only sibling.

Floating, and everything exempted from it

The third judgement is not about a packet at all. It runs in the connection tick beside the bracket, and of the three it is the one that ends sessions.

Floating has a flat definition — nothing but air around you and for just over half a block below, while you are not dropping — and a budget that is anything but flat. ServerGamePacketListenerImpl.getMaximumFlyingTicks returns an effectively unbounded budget below a gravity of 10⁻⁵, and otherwise stretches the eighty-tick budget as gravity falls; so the kick scales with gravity, and only upward. ServerGamePacketListenerImpl.clientIsFloating is then suppressed outright by six things: spectator mode, Abilities.mayfly, the server’s own allow-flight setting, MobEffects.LEVITATION, fall-flying and riptide — and the kick is not counted at all while you sleep, ride or die. That list is why creative flight never trips it, and it is the list the rest of the book points here for.

A floating vehicle runs a second copy of the whole check — ServerGamePacketListenerImpl.clientVehicleIsFloating against ServerGamePacketListenerImpl.aboveGroundVehicleTickCount, on its own budget and only for the controlling passenger — so a rider and the thing they are riding are judged separately, and either can end the session (players and sessions).

What it calls, and what crosses the wire

  • Called by: Minecraft.tick (client: the input through ClientLevel’s entity tick, the move packets through LocalPlayer.sendChanges); PacketProcessor and MinecraftServer.tickChildren (server).
  • Calls into: LivingEntity.travel and Entity.move (movement and collision); ServerChunkCache.move, which is what makes chunks load as you walk (tickets and loading).
  • Crosses the network as: ServerboundMovePlayerPacket and its four variants, ServerboundPlayerInputPacket, ServerboundPlayerCommandPacket (whose ServerboundPlayerCommandPacket.Action is seven values — start and stop sprinting, start and stop riding-jump, ServerboundPlayerCommandPacket.Action.STOP_SLEEPING, ServerboundPlayerCommandPacket.Action.OPEN_INVENTORY, ServerboundPlayerCommandPacket.Action.START_FALL_FLYING; there is no sneak action — sneaking reaches the server through ServerboundPlayerInputPacket, which calls Entity.setShiftKeyDown; and the packet carries an entity id the server never validates against the sender), ServerboundMoveVehiclePacket, ServerboundAcceptTeleportationPacket, ServerboundClientTickEndPacket; and back, ClientboundPlayerPositionPacket, ClientboundPlayerRotationPacket, ClientboundMoveVehiclePacket.
  • Data-driven by: almost nothing — GameRules.PLAYER_MOVEMENT_CHECK and GameRules.ELYTRA_MOVEMENT_CHECK (level data and rules), plus the movement attributes.

Questions players ask

If ServerboundPlayerInputPacket never moves me, what is it for? Three things a player notices. ServerPlayer.setLastClientInput feeds ServerPlayer.getLastClientMoveIntent, and both NewMinecartBehavior and OldMinecartBehavior read it to nudge a stalled cart along the rider’s intended direction — so the packet that cannot move a player can move a minecart — and the same stored input is what a data pack’s player predicate tests (PlayerPredicate). And the handler sets the sneak flag directly, which is why there is no sneak action on ServerboundPlayerCommandPacket. And a key change counts as activity, resetting the idle timer (ServerPlayer.resetLastActionTime). Boats are steered client-side (LocalPlayer.rideTick → AbstractBoat.setInput) and the result ships as ServerboundMoveVehiclePacket.

Does sending move packets faster help me cheat? It does the opposite. The per-packet budget normally scales with how many packets arrived since the last tick, but above five the code clamps the count to one — so a flood gets a one-packet budget for a many-packet displacement. And a second position inside one client tick, before the ServerboundClientTickEndPacket that closes it, is a disconnect; beyond that, only chat, commands and item drops have a TickThrottler, and a server that sets a rate limit kicks any flood of packets (the connection).

Why is a passenger barely checked? A passenger’s own move packet contributes rotation and a chunk re-registration and nothing else, and the vehicle’s packet is judged by a cut-down copy of the same code: a flat budget of 100, no elytra case, no game rule, and no horizontal-collision flag.

Why did my quick tap do nothing? Movement polls KeyMapping.isDown once a tick, so a press and release that both fall between two ticks never happened — unless the key is sneak or sprint set to toggle, where the press alone flips the state. The keys that use KeyMapping.consumeClick behave the other way: three taps inside one tick can fire three times.

Why does brushing a wall sometimes cancel my sprint and sometimes not? LocalPlayer.isHorizontalCollisionMinor is a client-only override that measures the angle against LocalPlayer.MINOR_COLLISION_ANGLE_THRESHOLD_RADIAN, about eight degrees; a graze shallower than that is forgiven. The server has no equivalent.

Where to look

The client half reads shortest first: KeyboardInput.tick is the whole of the sampling, LocalPlayer.aiStep the tick it happens inside, and LocalPlayer.sendPosition the method that decides what, if anything, leaves. MouseHandler.handleAccumulatedMovement is the per-frame exception beside them, and KeyboardHandler.keyPress the callback that does less than its name suggests.

The server half is essentially one class. Read ServerGamePacketListenerImpl.handleMovePlayer and the ServerGamePacketListenerImpl.handlePlayerPositionChange it calls end to end — between them they are the speed check, the move, the residual check and the rubber-band — then ServerGamePacketListenerImpl.handleMoveVehicle beside it to see the same logic with the elytra case and the game rule taken out. On the wire, ServerboundMovePlayerPacket with its four variants and ClientboundPlayerPositionPacket with its PositionMoveRotation and Relative flags are worth opening for their shape alone. Two doors this page only points at: PacketProcessor, which is what drains a move packet before any level ticks, and TickThrottler, which is what movement notably does not have.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The sword swing

Verified against Minecraft 26.3 · Part VIII · Left-click on a pig: the client picks a target and sends one integer, and the server rebuilds every part of the hit from scratch.

You put the crosshair on a pig and click. The client has already decided what you are looking at — earlier in this same tick — checks a handful of reasons not to swing, and sends a packet with almost nothing in it: ServerboundAttackPacket is a record of one int, the entity id. No hand, no sneak flag, no hit position, no damage. Everything else the server re-derives: the weapon from your main hand, the geometry from the target’s bounding box measured against your eye, and the damage from an attribute, a cooldown curve applied twice in two different shapes, and a multiplication order in which the mace’s fall bonus lands before the critical hit and is therefore multiplied by it.

The cast

classwhat it decidesthread
Minecraftwhat you are looking at, and whether the click swings at allRender
LocalPlayerthe raycast, and which range its winner is judged againstRender
MultiPlayerGameModesends the attack, and predicts almost nothingRender
ServerGamePacketListenerImplresolves the id, re-checks the range and the itemServer
PlayerPlayer.attack: one method, and the order inside it is load-bearingboth (only the server’s answer counts)
LivingEntitythe swing animation state, and the two attack clocksboth
AttackRangea weapon’s own reach, with a minimum as well as a maximum—

Picking: what is under the crosshair

Minecraft.pick runs before Minecraft.handleKeybinds in the client tick (the client loop), and the keybind drain is what turns Options.keyAttack into Minecraft.startAttack — so the hit result a click uses was computed earlier in the same tick. Minecraft.pick also runs per frame, for the crosshair and the block outline, but that value is not what the attack sees.

It asks the local player, through LocalPlayer.raycastHitResult, with the camera entity as the ray’s origin. A player carries two reaches, and the whole of this section turns on their being different numbers: Attributes.BLOCK_INTERACTION_RANGE (4.5) and Attributes.ENTITY_INTERACTION_RANGE (3.0) (player anatomy).

If the active item (the one being used, if any, else the main hand) carries an AttackRange (DataComponents.ATTACK_RANGE), that component’s own search runs first, and the classic algorithm runs only when that search comes back empty: a block clip out to the greater of the two ranges, an entity sweep with ProjectileUtil.getEntityHitResult over the bounding box expanded along the view direction and inflated by one, each candidate inflated by Entity.getPickRadius (zero for everything but a pickable projectile) — and the entity wins only if it is strictly nearer than the block. Then LocalPlayer.filterHitResult measures the winner against its own one of the two reaches, an entity against 3.0 and a block against 4.5. That is the divergence outside creative: a wall four blocks away is in reach and a mob four blocks away is not — and a mob at four blocks in front of a wall hides the wall, because the mob wins the ray and is then turned into a miss.

AttackRange is worth a second look, because it is a reach floor as well as a ceiling: a minimum and a maximum, separate creative values, a hitbox margin and a mob factor. AttackRange.isInRange is the test, AttackRange.defaultFor falls back to Attributes.ENTITY_INTERACTION_RANGE, and AttackRange.effectiveMinRange / AttackRange.effectiveMaxRange apply the mob factor — only for non-players.

Deciding: most branches do not swing

Minecraft.startAttack is the branch point. It returns early — no swing, no packet — while Minecraft.missTime is running, when there is no hit result at all (setting a ten-tick miss time), while LocalPlayer.isHandsBusy, for a disabled item, and when Player.cannotAttackWithItem refuses with a tolerance of zero; spectators take a branch of their own. Two branches do swing: the piercing short-circuit to MultiPlayerGameMode.piercingAttack when the item carries DataComponents.PIERCING_WEAPON — the spear — and the tail of the hit-result switch: entity to MultiPlayerGameMode.attack, block to MultiPlayerGameMode.startDestroyBlock (block breaking), a miss (nothing in reach, or an air block) to Player.resetAttackStrengthTicker and the ten-tick miss time. Even the entity branch is conditional — a weapon with its own AttackRange that the hit falls outside of swings but sends no attack packet at all. The ten-tick miss time only exists outside creative, and opening any screen parks the timer at a very large number in every mode.

On the server, ServerGamePacketListenerImpl.handleAttack requires the client to have loaded and the player not to be a spectator, resolves the id with ServerLevel.getEntityOrPart, checks the world border, applies Player.isWithinAttackRange with a 3.0-block server buffer — applied to both ends, so a weapon’s minimum range effectively vanishes server-side — rejects a piercing weapon (that path arrives elsewhere), checks the item is enabled, re-checks Player.cannotAttackWithItem with a tolerance of five ticks, more lenient than the client’s zero, and calls Player.attack. Attacking something absurd — an ItemEntity, an ExperienceOrb, an unattackable AbstractArrow, yourself — is a disconnect, not a rejection; failing the range check is a silent drop.

The packet is drained from PacketProcessor at the top of the tick, before MinecraftServer.tickServer and therefore before any level ticks. That ordering matters: Player.attack runs before the victim’s damage cooldown comes down for the tick (damage and death owns LivingEntity.damageCooldownTime and who decrements it when). Each of the resulting feedback packets is written and flushed on its own: the connection suspends flushing across MinecraftServer.tickChildren (and across a player’s join), and the attack was handled before that bracket opened.

One click, one integer, one round trip

Two packets go out, and the one that comes back for the hit, the damage event, has no damage number in it.

sequenceDiagram
    box transparent the client
    participant LP as LocalPlayer
    participant MPGM as MultiPlayer<br/>GameMode
    participant CPL as ClientPacketListener
    end
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Player as Player
    participant Entity as Entity
    end

    LP->>LP: raycastHitResult — AttackRange first, then the classic pick
    LP->>MPGM: attack, from Minecraft.startAttack
    MPGM->>SGPL: ServerboundAttackPacket — one varint, the entity id
    LP->>SGPL: ServerboundPunchPacket — no fields, from Minecraft.startAttack
    SGPL->>SGPL: ServerLevel.<br/>getEntityOrPart
    SGPL->>Player: isWithinAttackRange — the reach, plus a 3.0 buffer
    SGPL->>Player: attack — the damage rebuilt from nothing but the id
    Player->>Entity: hurtOrSimulate — its answer gates the tail
    Entity->>CPL: ClientboundDamageEventPacket — a damage type and three ids
    Player->>Player: causeExtraKnockback, doSweepAttack, itemAttackInteraction

Everything the client sent is on the two arrows that cross to the right, and between them they carry one integer: every number in the box on the right was rebuilt there, and the arrow coming back carries none of them.

The two clocks a swing is charged against

Before the arithmetic, the thing the arithmetic reads. A player carries two attack clocks, and both are declared two rungs up, on LivingEntity, while being read, reset and incremented only from Player: LivingEntity.attackStrengthTicker and LivingEntity.itemSwapTicker.

The first is the one you watch. Player.getCurrentItemAttackStrengthDelay is twenty divided by Attributes.ATTACK_SPEED, and the scale the damage uses is how far through that delay the ticker has got. Player.resetAttackStrengthTicker clears both clocks; Player.resetOnlyAttackStrengthTicker clears one. Player.tick resets both when the main-hand item type changes — swap weapons and you start again.

The second exists only to drive the held-item swap animation. Player.onAttack clears the attack ticker and leaves the swap ticker alone, but it is not the only reset a swing gets: both sides reset the attack ticker twice per swing, once inside Player.attack and once after it with Player.resetAttackStrengthTicker, which clears both clocks — the client in MultiPlayerGameMode.attack, the server on the punch packet that follows, because ServerGamePacketListenerImpl.handlePunch resets the ticker too.

The damage: one number, two curves, one order

Player.attack is a single method, and everything interesting about melee combat is the order in which it touches one float.

flowchart TD
    S["s = Player.getAttackStrengthScale, at a partial tick of 0.5"]
    BASE["base = Attributes.ATTACK_DAMAGE"]
    BOOST["boost = Player.getEnchantedDamage minus base, times s"]
    BQ["base × (0.2 + s² × 0.8) — Player.baseDamageScaleFactor"]
    RESET["Player.onAttack — the ticker resets, the scale already read"]
    GATE{"base or boost<br/>above zero?"}
    NONE["nothing below runs"]
    KB["sprint knockback, if s is above 0.9 and you are sprinting"]
    ITEM["base += Item.getAttackDamageBonus — the mace's fall bonus"]
    CRIT["base × 1.5, if full strength and Player.canCriticalAttack"]
    TOTAL["total = base + boost"]
    HURT["Entity.hurtOrSimulate — was anything damaged?"]
    S --> BOOST
    S --> BQ
    BASE --> BOOST
    BASE --> BQ
    BQ --> RESET
    RESET --> GATE
    GATE -- "neither" --> NONE
    GATE -- "either" --> KB
    KB --> ITEM
    ITEM --> CRIT
    CRIT --> TOTAL
    BOOST --> TOTAL
    TOTAL --> HURT

One float, followed down: the short branch is the enchantment bonus, which is scaled once and then left alone, and the long one is the base damage, which is scaled, gated, added to and multiplied before the two meet again at the bottom.

One node in it needs its meaning before the rest of the page spends it. Entity.hurtOrSimulate is the wrapper that branches on the side — Entity.hurtServer on the server, Entity.hurtClient on the client (damage and death owns the wrapper itself) — and what it returns is not the obvious thing: the boolean means was anything damaged, not did the hit land. That is what gates the knockback, the sweep, the durability and the hit particles below.

Read the picture for the two things it makes obvious. The cooldown is applied twice, differently — Player.baseDamageScaleFactor is the quadratic ramp on the base damage, and a plain multiplication is the linear one on the enchantment bonus, both from the same scale read with the same 0.5 partial tick. And the item bonus is inside the crit, because Item.getAttackDamageBonus is added before the ×1.5 rather than after it. One substitution the figure leaves out: while an auto-spin attack is in flight the base is LivingEntity.autoSpinAttackDmg, the riptide number, instead of the attribute.

The gates along the way are as particular as the arithmetic, and the first two are put to the target rather than to the attacker. Player.cannotAttack asks it two things: Entity.isAttackable, which is true by default and false for a dropped item, an experience orb, a falling block, a thrown eye of ender, a firework rocket and any arrow outside EntityTypeTags.REDIRECTABLE_PROJECTILE, and Entity.skipAttackInteraction, the hook by which a thing claims the swing for itself — an Interaction entity uses it to record who hit it, and a BlockAttachedEntity to re-enter through Entity.hurtOrSimulate with zero damage. A no from the first or a yes from the second ends the swing (damage outside LivingEntity has the per-class table). Then Player.deflectProjectile, which is why a ghast fireball can be batted back: anything in EntityTypeTags.REDIRECTABLE_PROJECTILE is turned around here rather than damaged, and the attack ends — all but a player’s wind charge in its first five ticks, which WindCharge will not let be deflected.

Then three tests that are not gates but switches, each deciding what kind of hit this is. Sprint knockback needs the scale above 0.9, plays a sound, and adds a flat 0.5 to the knockback later; it is the step the item bonus is added immediately after, which is why the item-bonus node in the figure above sits where it does. Player.canCriticalAttack needs falling, not on the ground, not climbing, not in water, not mobility-restricted, not a passenger, not sprinting, a LivingEntity target — and full strength as well. Player.isSweepAttack needs full strength, not a crit, not sprint knockback, on the ground, moving slower than 2.5× the walking speed, and something in ItemTags.SWORDS.

If the hit landed, the tail runs in order: Player.causeExtraKnockback — which is also where the attacker’s own motion is damped and sprinting cancelled, using LivingEntity.getKnockback computed from Attributes.ATTACK_KNOCKBACK through the enchantments and halved — then Player.doSweepAttack, Player.attackVisualEffects, LivingEntity.setLastHurtMob, Player.itemAttackInteraction, Player.damageStatsAndHearts, and Player.causeFoodExhaustion of 0.1 (hunger and experience). If it did not land, a no-damage sound. Either way LivingEntity.postPiercingAttack runs at the end — the same hook a stab ends on, which is why the name says piercing on a method that closes an ordinary swing (the spear).

Player.itemAttackInteraction is itself three steps in a particular order: ItemStack.hurtEnemy (the item’s own hook and the use statistic, not durability), then EnchantmentHelper.doPostAttackEffectsWithItemSource (enchantments), then ItemStack.postHurtEnemy, which is where Weapon’s per-attack durability cost is applied. Weapon (DataComponents.WEAPON) is a pair: that cost, and Weapon.disableBlockingForSeconds, the axe’s shield-breaking rule. LivingEntity.getSecondsToDisableBlocking reads it back, but only when the weapon in the attacker’s hand is also their active item — so an axe swung while the other hand is using something disables no shield.

Player.doSweepAttack damages every living entity in a box around the primary target inflated by (1, 0.25, 1), for candidates within three blocks of the attacker, excluding the attacker, the primary target, allies and marker armour stands. Each takes 1.0 + Attributes.SWEEPING_DAMAGE_RATIO × base, run through Player.getEnchantedDamage and then scaled by the attack-strength scale, plus a flat 0.4 knockback. Its sweep sound is unguarded; the damage and the ParticleTypes.SWEEP_ATTACK particles sit behind the server check.

Armour, the hurt cooldown, DataComponents.BLOCKS_ATTACKS and knockback resistance decide how much of that damage arrives, and all four belong to damage and death.

Questions players ask

Why does mashing do less damage? Because the base damage is quadratic in the charge and the enchantment bonus is linear in it. At half charge the base is 0.2 + 0.25 × 0.8 — that is s² with s = 0.5 — so 40% of full, while the enchantment bonus is at the full 50%. Mashing costs you more of the sword than of the enchantments on it.

Why does my sword make no sound until the server answers? Because the attack sounds are the counter-example to the client’s own prediction rule: Player.playServerSideSound excludes nobody, so every hit sound the attacker hears arrives as a ClientboundSoundPacket, one round trip late (what makes a sound).

Does the client predict any of this against a mob? Almost none. Entity.hurtClient returns false and neither LivingEntity nor Mob overrides it, so on the client Entity.hurtOrSimulate reports that the hit did not land and the entire block after it is skipped: no predicted knockback, no sweep, no visual effects, no durability, no exhaustion. The exceptions are the nine classes that do override it, and none of them is a mob — a RemotePlayer, a vehicle, a hanging thing, an orb (damage outside LivingEntity has the roll-call). Against four of them — another player, a vehicle, an end crystal, a shulker bullet — the whole block runs locally; a dropped item and an orb cannot be attacked at all, and the block-attached ones either claim the swing through Entity.skipAttackInteraction or refuse it in Entity.hurtClient. Player.getEnchantedDamage does nothing on Player either; it returns its argument unchanged and only ServerPlayer overrides it. With Attributes.ATTACK_DAMAGE not being client-syncable (attributes), the client’s damage figure is never authoritative wherever that block does run. That the client may not decide a hit at all is the rule of authority, applied to combat.

Can a weapon be too close to swing? No: it swings, and what an AttackRange minimum withholds on the client is the attack packet. The server’s attack path would enforce at most the floor less three blocks — the 3.0-block leniency is subtracted from the minimum as well as added to the maximum, which erases any floor under three — but the one built-in weapon with a floor, the spear, never takes that path, and its stab’s own search starts at the floor.

How does my client know how badly the pig was hurt? It learns the new health, not the hit. ClientboundDamageEventPacket carries no amount at all — a damage-type holder, three entity ids and an optional source position — and the victim’s red flash and damage cooldown are reconstructed from it, with the hurt sound too when the victim is you; a pig’s hurt sound arrives as a sound packet of its own (damage and death owns the packet). The health itself comes from synched entity data.

Are sweep and knockback enchantment effects? They are attributes, and the two enchantments reach them differently: Sweeping Edge raises Attributes.SWEEPING_DAMAGE_RATIO with an attribute modifier, while Knockback leaves Attributes.ATTACK_KNOCKBACK alone and adds to the number read from it, through an effect on the weapon. Attributes.SWEEPING_DAMAGE_RATIO defaults to zero, so a vanilla sweep does 1.0 — scaled by the attack-strength ratio, so slightly less than 1.0 anywhere in the sweep’s legal window below full charge. And Attributes.ATTACK_KNOCKBACK defaults to zero, so for an unenchanted sword the entire attacker-side knockback is the sprint bonus of 0.5.

What makes my own arm move? Your own client does, before the server hears about it. Every branch of Minecraft.startAttack that swings at all calls LivingEntity.swing, which on the client only sets the animation state, and the tail of the hit-result switch then sends ServerboundPunchPacket, a packet with no fields at all. That is why a miss on an air block still animates, and why the animation never waits for a round trip. The server’s own LivingEntity.swing then broadcasts ClientboundSwingAnimationPacket to your trackers — the players who can see you — and not back to you, because you played it a round trip ago.

Why does my swing look different with a different weapon? Because swing duration is a data component — ItemStack.getAttackAnimation returns a SwingAnimation — not a constant six ticks; MobEffects.MINING_FATIGUE stretches it and haste shortens it. The animation state itself is one LivingEntity.SwingState, which holds the current LivingEntity.SwingDescription (hand, animation and length in ticks), a tick count and the progress from zero to one, and ServerGamePacketListenerImpl.handlePunch is where the server receives yours. Crit particles are the exception to the never-echoed rule: they go to the attacker’s trackers and to the attacker too, while naming the victim.

Is this the only way to hit something in melee? No. Two other paths end in damage and neither goes through Player.attack: a PiercingWeapon short-circuits before the hit-result switch, and a KineticWeapon is reached from item use rather than attack. Both are the spear.

For a 1.21-era reader. ServerboundSwingPacket and LocalPlayer.swing are gone: after the local LivingEntity.swing, Minecraft.startAttack sends ServerboundPunchPacket itself, and the server passes the swing on as ClientboundSwingAnimationPacket rather than as a ClientboundAnimatePacket action. The fields swinging, swingingArm, swingTime and attackAnim are now one LivingEntity.SwingState, and the hurt cooldown that was Entity.invulnerableTime is LivingEntity.damageCooldownTime; the old name is now a separate timer that a hit does not set.

Where to look

Two methods are the whole lecture, and they are worth reading in this order. Minecraft.startAttack is the client’s branch point: almost every reason not to swing is in it (the rest are around it: Minecraft.tick skips the keybind drain while a screen or an overlay is up, and Minecraft.handleKeybinds swallows the click while you are using an item), and so are the two branches that do. Player.attack is the server’s, and everything surprising about melee combat is the order of the lines in it — read it once for the shape and a second time for where Player.baseDamageScaleFactor and Item.getAttackDamageBonus land relative to the ×1.5.

Around them: ServerGamePacketListenerImpl.handleAttack for the nine checks between the packet and the method, AttackRange for the reach floor the server subtracts away, and Weapon for the two-field component that charges the durability. Player.doSweepAttack and Player.itemAttackInteraction are the two halves of the tail worth opening on their own. On the wire, ServerboundAttackPacket takes ten seconds to read and ClientboundDamageEventPacket is worth the same for what it leaves out. Two doors this page only points at: ProjectileUtil, which is where the entity sweep happens, and SwingAnimation, the component behind the arm.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The spear

Verified against Minecraft 26.3 · Part VIII · Two ways to hit something with the same item: jab it, and the client sends no target at all; charge it and run, and the damage comes from how fast the gap is closing.

A spear is one item with two weapons in it. Left-click and you stab: the client sends a packet with no entity id in it, and the server does its own raycast and hits everything along the ray. Hold right-click and you charge: the spear becomes an item you are using, like a bow, except that what it does each tick is look for entities in front of you and hurt them by an amount that grows with the closing speed. Neither path enters Player.attack, the method the sword swing is about, and the second has a property no other melee attack in the game has. Every ordinary swing is scaled by how far the attack-strength clock has recharged — quadratically on the base damage, linearly on the enchantment bonus, so a sword swung at half charge does 40% of its base damage (the two clocks). A charging spear ignores that scaling entirely, because the code that applies the two curves is skipped for the item you are currently using, and a charge is a use.

The cast

classwhat it decidesthread
PiercingWeaponthe stab — and, through its filter, who either weapon is allowed to hit along a rayServer (its attack sound plays on both sides)
KineticWeaponthe charge: three speed conditions, and the damage from closing speedServer
Item.Properties.spearthe seven spears, and the combat components that make one—
Minecraft / MultiPlayerGameModethe client’s short-circuit, and the packet with no targetRender
ServerGamePacketListenerImplServerboundPlayerActionPacket.Action.STAB, and the piercing rejection in the ordinary attack handlerServer
LivingEntity.stabAttackthe general tail — damage, two knockbacks, dismount — which only a mob’s charge runsServer
Player.stabAttacka player’s whole tail instead, durability included, with the cooldown curves — sometimesServer
SpearUseGoal / SpearAttackhow a zombie or a piglin runs the chargeServer

What an item needs to be a spear

Item.Properties.spear is one builder call per material, and the seven spears — Items.WOODEN_SPEAR through Items.NETHERITE_SPEAR — differ almost only in the numbers it is given; the wooden one gets its own sounds and burns as fuel, and the netherite one is fire-resistant. What it attaches is the interesting part, because it is both weapons at once plus the reach to use them:

componentwhat the spear gets
DataComponents.PIERCING_WEAPONknockback yes, dismount no, an attack sound and a hit sound
DataComponents.KINETIC_WEAPONa contact cooldown of ten ticks, a delay, three conditions, and a damage multiplier
DataComponents.ATTACK_RANGEAttackRange.minReach of 2.0 and AttackRange.maxReach of 4.5 — 2.0 and 6.5 in creative — with a hitbox margin and a mob factor
DataComponents.MINIMUM_ATTACK_CHARGE1.0: no stab before the attack clock is full
DataComponents.ATTACK_ANIMATIONSwingAnimationType.STAB, with a per-material duration
DataComponents.DAMAGE_TYPEDamageTypes.SPEAR, as a delayed holder component
DataComponents.USE_EFFECTSthe whole component overridden: sprint allowed, vibrations off, speed multiplier one — the one definition in the game, shared by the seven spears, that does not slow you down while you hold it out
DataComponents.WEAPONa durability cost of one per attack
attribute modifiersAttributes.ATTACK_DAMAGE from the material, and an Attributes.ATTACK_SPEED derived from the swing duration

Four of those rows are what the rest of this page is about — the two weapon components, the reach they share, and the UseEffects override. The other five are what stops a spear behaving oddly in the hand: no stab before the attack clock is full, a stab animation rather than a slash, its own damage type in the death message, a durability cost of one per attack, and the attack damage and speed its attribute modifiers set.

The ordinary tool components come from the material and are not in the table: Item.Properties.spear also calls Item.Properties.durability, Item.Properties.repairable and Item.Properties.enchantable, so a spear takes damage, mends on an anvil and accepts enchantments the way any tool does (items and stacks and enchanting).

That UseEffects override is why a spear feels unlike every other held-down item — LocalPlayer.itemUseSpeedMultiplier and LocalPlayer.isSlowDueToUsingItem are the readers it turns off, and using an item explains what the component does for everything else.

Two entries, one exit

flowchart TD
    CLICK["left-click: Minecraft.startAttack"]
    HAS{"main hand:<br/>PIERCING_WEAPON?"}
    NORMAL["the ordinary path: MultiPlayerGameMode.attack, then Player.attack"]
    REFUSE["and ServerGamePacketListenerImpl.handleAttack refuses a piercing weapon"]
    PA["MultiPlayerGameMode.piercingAttack — the sound, and the ticker locally"]
    PKT["ServerboundPlayerActionPacket.Action.STAB — no entity id, a dummy position"]
    SGPL["ServerGamePacketListenerImpl.handlePlayerAction — not a spectator, and Player.cannotAttackWithItem with a 5-tick tolerance"]
    PW["PiercingWeapon.attack — the server's own raycast"]
    USE["right-click: Item.use sees KINETIC_WEAPON, then LivingEntity.startUsingItem"]
    TICK["ItemStack.onUseTick, every use tick of the 72000"]
    KW["KineticWeapon.damageEntities — server side only: the ticks used, the look vector, the closing speed"]
    RAY["ProjectileUtil.getHitEntitiesAlong — every entity on the ray, filtered by PiercingWeapon.canHitEntity"]
    STAB["Player.stabAttack — or a mob's LivingEntity.stabAttack"]
    CLICK --> HAS
    HAS -- "no" --> NORMAL
    NORMAL --> REFUSE
    HAS -- "yes" --> PA
    PA --> PKT
    PKT --> SGPL
    SGPL --> PW
    USE --> TICK
    TICK --> KW
    PW --> RAY
    KW --> RAY
    RAY --> STAB

Two entries — a click on the left, a held right-click on the right — meeting at one raycast, and the only place the two columns touch is that meeting: the left column’s refusal node is the reason a client cannot route a stab down the ordinary path.

Two things in that picture are worth stopping on. The client tells the server nothing about the target on the stab path: the packet is a ServerboundPlayerActionPacket carrying ServerboundPlayerActionPacket.Action.STAB, whose block position and direction are dummies, and every question about what was hit is answered by the server’s own raycast. And the ordinary attack handler refuses a piercing weapon — ServerGamePacketListenerImpl.handleAttack checks for DataComponents.PIERCING_WEAPON and drops out — so the two paths cannot be confused for one another even by a client that tries.

The stab

PiercingWeapon.attack takes the attacker’s Attributes.ATTACK_DAMAGE, the weapon in the given slot and LivingEntity.getAttackRangeWith, and walks ProjectileUtil.getHitEntitiesAlong with the block-collider clip context — so a wall stops the ray, but a crowd does not. Every entity along it is stabbed — in the order the search of the box around the ray returned them, which is not sorted by distance — each through the attacker’s Player.stabAttack with the same damage figure.

PiercingWeapon.canHitEntity is the filter, and it is a projectile-shaped test rather than a melee one: the target must not be Entity.isInvulnerableToPiercingWeapon, must be alive, and must satisfy Entity.canBeHitByProjectile. Player against player defers to Player.canHarmPlayer, and an entity riding the same vehicle as the attacker is not hit. An Interaction — the invisible click-target entity a data pack places to catch hits — short-circuits the rest of the filter to hittable once it is alive and not immune, same vehicle included. And Player.stabAttack still puts the melee gate, Player.cannotAttack, to every target the filter lets through.

Afterwards the attacker gets LivingEntity.onAttack — which on a Player resets the attack-strength ticker — and LivingEntity.postPiercingAttack, the hook that runs EnchantmentHelper.doPostPiercingAttackEffects; only then does the weapon play PiercingWeapon.makeHitSound if anything was hit and PiercingWeapon.makeSound regardless, and swing the arm. The client half did the swing and LivingEntity.onAttack itself as it sent the packet, and called LivingEntity.postPiercingAttack too, which does nothing off a ServerLevel.

The charge

A spear’s kinetic weapon is built on speed: its damage comes from how fast the gap is closing, its dismount and knockback need you to be moving, and the UseEffects override above exists so that you can sprint while charging. The two halves are one design.

A kinetic weapon is also used, not swung. Item.use sees DataComponents.KINETIC_WEAPON, calls LivingEntity.startUsingItem and plays the sound; Item.getUseDuration returns 72000 for it, the same effectively-endless duration a bow gets, so in play the charge runs until you release it, the spear leaves your hand, or you die or change dimension (using an item). Starting also allocates LivingEntity.recentKineticEnemies, a server-side map of who the charge has reached and when, which LivingEntity.stopUsingItem throws away.

Each use tick, ItemStack.onUseTick diverts to KineticWeapon.damageEntities — and skips the item’s own Item.onUseTick when it does. What that method computes is a speed argument, not a swing:

  • How long you have been charging. Ticks used must be at least KineticWeapon.delayTicks; everything below is measured from there.
  • How fast you are going, along your look vector. KineticWeapon.getMotion reads Entity.getKnownSpeed — the reported movement from input to movement — scaled to blocks per second, taking the root vehicle’s motion for a non-player passenger (input to movement).
  • How fast the gap is closing. The target’s own projected speed is subtracted, floored at zero, and that relative speed is what the damage is built from.
  • Whether you already reached them. LivingEntity.wasRecentlyStabbed against KineticWeapon.contactCooldownTicks — ten for a spear — is why running through a crowd does not hit the same mob every tick.

Who is in front of you is answered exactly as it is for the stab: ProjectileUtil.getHitEntitiesAlong walks the weapon’s AttackRange with the block-collider clip context and PiercingWeapon.canHitEntity filters what it finds. The two attacks differ in what they compute, never in what they can reach.

Three independent KineticWeapon.Conditions then decide what the hit is, and the point is that each gates its own effect rather than the hit as a whole: KineticWeapon.dismountConditions, KineticWeapon.knockbackConditions and KineticWeapon.damageConditions. Each is a maximum duration and two speed bars, one on the attacker’s own projected speed and one on the closing speed; a spear’s first two set the first bar and its damage condition the second. If any of the three passes the attack happens at all; then the dismount only dismounts if its own condition passed, the knockbacks only fire if theirs did, and the damage is only dealt if the damage condition passed. A charge can knock a target off a horse and deal it no damage.

A spear’s three come from the builder with different windows, and for all seven materials they nest the same way: damage has the longest window and the lowest threshold (on the closing speed), dismount the shortest window and much the highest. A wooden spear dismounts for five seconds, knocks back for ten and damages for fifteen — so a charge that has run too long can still hurt when it can no longer unseat anyone. When the damage does land it is the attacker’s base Attributes.ATTACK_DAMAGE plus the floor of relative speed × KineticWeapon.damageMultiplier — the base value, so the modifiers a sword swing would pick up are not in it.

A landed charge broadcasts an entity event, and it is the part of the telling that is about the charge, alongside the ordinary damage sync, knockback and, for a player, durability that every hit sends: LivingEntity.onKineticHit plays a local hit sound, never within ten ticks of the last — the number KineticWeapon.HIT_FEEDBACK_TICKS names — and LivingEntity.getTicksSinceLastKineticHitFeedback feeds the animation. A ServerPlayer also trips CriteriaTriggers.SPEAR_MOBS_TRIGGER with the number of living entities the charge has reached.

The tail, and the cooldown that is not applied

Both paths end in a method called stabAttack, which exists twice. LivingEntity.stabAttack is the general one, and a mob’s charge is the only thing that runs it: it returns false off a ServerLevel, runs the damage through EnchantmentHelper.modifyDamage, calls Entity.hurtServer — into the same pipeline a sword swing ends in (damage and death) — applies two knockbacks, a flat one and LivingEntity.getKnockback, and dismounts the target, each of the three behind its own flag. Two things then run on different conditions again: ItemStack.hurtEnemy for any living target whether or not the damage landed, and the post-attack enchantment effects (enchantments) only if it did. The attack sound plays once anything at all happened.

Player.stabAttack overrides it whole, never calling it, and the override is where the spear becomes strange. It computes the enchantment boost the way Player.attack does, and then applies the two cooldown curves — the linear one to the boost, the quadratic Player.baseDamageScaleFactor to the base — only if the player is not currently using an item in that slot. A stab qualifies, so a stab is charged like a sword swing. A kinetic charge does not: while you are holding the spear out, both curves are skipped and every tick’s hit lands at full base damage. The rest of the override is the familiar tail — Player.deflectProjectile can still end it, the knockbacks are the same two, Player.itemAttackInteraction applies the durability cost, and Player.causeFoodExhaustion charges the same 0.1 a sword does (hunger and experience).

What a mob does with the same item

A mob runs only the charge — the stab reaches PiercingWeapon.attack only from a player’s packet — and the split between the two ways it can run it is the ordinary one (goals and brains). SpearUseGoal drives the charge for a goal-based mob; SpearApproach, SpearAttack and SpearRetreat do it for a brain-based one. Zombies, zombified piglins and piglins are the users in the tree, and Piglin treats a kinetic weapon like a crossbow when deciding what it is holding. Both paths read KineticWeapon.computeDamageUseDuration — the delay plus the damage condition’s window — to know how long to hold the charge for.

The thresholds are far easier for them than for you. Every speed bar in the three conditions is multiplied by an action factor of 0.2 for anything that is not a player, against 1.0 for you: a mob charging at a fifth of your speed clears the same bar.

What a data pack can and cannot change here

The shape is data-driven; the values are not. PiercingWeapon and KineticWeapon are ordinary data components with codecs and stream codecs, so a data pack can describe a weapon of either kind without code — but every built-in spear’s numbers are hard-coded (the shared ones in Item.Properties.spear, the per-material ones where Items registers each spear and in its ToolMaterial) rather than read from JSON, which is why the seven differ almost only in arithmetic.

One field in KineticWeapon is not a combat number at all. KineticWeapon.forwardMovement — 0.38 for a spear — is read only by SpearAnimations, and only by its third-person methods: a rendering offset living in the middle of a combat component, shipped to every client that receives the item.

Where to look

Item.Properties.spear is one statement and is the whole definition of the weapon — read it first, with the numbers for one material in hand. Then the two components it hangs on: PiercingWeapon, whose PiercingWeapon.canHitEntity filter serves both attacks, and KineticWeapon, whose KineticWeapon.damageEntities is the charge end to end and whose nested KineticWeapon.Condition is three fields and one test.

The exit is worth reading as a pair: LivingEntity.stabAttack for what a mob’s charge does to a target, then Player.stabAttack for the override a player runs instead, which puts the cooldown curves back — and the one condition that skips them. On the way in, MultiPlayerGameMode.piercingAttack and ServerboundPlayerActionPacket show how little the client sends, and Minecraft.startAttack is where the short-circuit sits. Two doors this page only points at: SpearUseGoal for the mob side, and SpearAnimations, which turns out to be the only reader of a field in the combat component.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Hunger and experience

Verified against Minecraft 26.3 · Part VIII · Two bars above the hotbar that the server owns outright: one you empty by sprinting, one you fill by mining, and neither of them is quite the number the interface draws.

The food bar and the experience bar look like the same kind of thing: a number the server keeps and sends you. They are, and they are also the two counters a player argues with most — the meal that does not seem to fill you, the levels that vanish into an anvil. Both are worth following for the same reason: neither is quite what the interface suggests. There is no method called eat on Player or LivingEntity — eating is a walk over the components of a stack that happens to end in FoodData.eat — and the experience packet is change-detected on the total alone, so every mutation that changes only your level has to poison the last-sent value or the bar will not move.

The cast

classwhat it decidesthread
FoodDatathe food bar, saturation and exhaustion; both sides hold one, but only the server ticks itboth (FoodData.tick takes a ServerPlayer)
FoodPropertieshow much a given item is worth, as a data componentboth
Consumablehow long eating takes, what it sounds like, and what else it appliesboth
Playerthe four experience fields, the enchanting seed among them, and the level curveboth
ExperienceOrba value and a multiplicity, wandering toward youboth
ServerPlayerthe change detection that turns any of this into a packetServer

Both halves hang off ServerPlayer.doTick, the connection-driven half of the two-phase tick; the level’s entity tick touches essentially none of it. The order inside that half matters: item use is resolved, then Player.aiStep runs ServerPlayer.tickRegeneration — which is the Peaceful refill, its whole body gated on that difficulty and the natural-regeneration rule — and the orb pickup, then FoodData.tick, and then the change-detection block that emits the packets. So a meal eaten this tick and a Hunger effect’s exhaustion from this tick are both visible to FoodData.tick in the same tick, and the resulting health and food reach the client in that tick too.

The food bar is four numbers and a file of constants

FoodData (world/food) is a value bag with no back-reference to the player: FoodData.foodLevel (20), FoodData.saturationLevel (5.0), FoodData.exhaustionLevel and FoodData.tickTimer. Its surface is the two FoodData.eat overloads (one taking a FoodProperties, one taking a nutrition and a saturation modifier), plus FoodData.addExhaustion (which caps at 40), FoodData.needsFood, FoodData.hasEnoughFood, FoodData.setFoodLevel, FoodData.setSaturation, the two accessors, and FoodData.tick — whose signature is FoodData.tick(ServerPlayer), server-only by type. It saves as four loose keys in the player tag, not a sub-compound.

FoodConstants names most of the thresholds in the system — FoodConstants.MAX_FOOD, FoodConstants.HEAL_LEVEL, FoodConstants.HEALTH_TICK_COUNT, FoodConstants.HEALTH_TICK_COUNT_SATURATED, FoodConstants.EXHAUSTION_DROP, FoodConstants.EXHAUSTION_HEAL, FoodConstants.EXHAUSTION_SPRINT, FoodConstants.EXHAUSTION_MINE, FoodConstants.EXHAUSTION_ATTACK, FoodConstants.SPRINT_LEVEL, FoodConstants.SATURATION_FLOOR and the saturation-quality ladder from FoodConstants.FOOD_SATURATION_POOR to FoodConstants.FOOD_SATURATION_SUPERNATURAL — and each is a compile-time constant, which javac writes in wherever it is used, so a decompile of FoodData shows the numbers themselves and this page gives them as numbers. The file’s one method, FoodConstants.saturationByModifier, turns a food’s saturation modifier into saturation. A few numbers have no name there at all, the exhaustion cap of 40, the starvation floors and the Peaceful refill’s periods among them.

Before the tick can spend exhaustion, something has to produce it, and the economy is smaller than players assume. Walking costs nothing: ServerPlayer.checkMovementStatistics charges the walking and the crouching branch zero per block, the zero FoodConstants.EXHAUSTION_WALK and FoodConstants.EXHAUSTION_CROUCH name. What does cost you is sprinting, jumping, swimming (and walking in or under water, at the same rate), mining, attacking, the Hunger effect, the ApplyExhaustion enchantment effect, and being hurt — the last of which is data-driven, since DamageSource.getFoodExhaustion reads the damage type. Player.causeFoodExhaustion charges 0.1 for a melee hit that lands, which is the figure the sword swing and the spear both end on. And in creative or spectator the whole economy is disabled in one line: Player.causeFoodExhaustion returns immediately for invulnerable abilities.

What FoodData.tick then does with that budget is one exhaustion rule and a three-way chain, tested in the order below, first match only — and the figure is that order, with the conditions themselves in the table under it.

flowchart TD
    EX{"exhaustion<br/>above 4.0?"}
    EX -- "yes" --> DRAIN["spend 4.0 — saturation first, then the bar"]
    EX -- "no" --> C1
    DRAIN --> C1{"1 · heal fast?"}
    C1 -- "yes" --> FAST["every 10 ticks, charged back as exhaustion"]
    C1 -- "no" --> C2{"2 · heal slowly?"}
    C2 -- "yes" --> SLOW["every 80 ticks, for 6.0 of exhaustion"]
    C2 -- "no" --> C3{"3 · starve?"}
    C3 -- "yes" --> STARVE["every 80 ticks, above the difficulty's floor"]
    C3 -- "no" --> RESET["none of the three — the timer resets"]

A chain, not a fan: each numbered diamond after the first is only reached because the one above it said no, which is the whole of why the three are exclusive.

tested in this orderthe conditionand then
1 · heal fastthe game rule on, saturation left, hurt, and the bar at 20every 10 ticks: heals a sixth of the saturation it spends, and charges that much back as exhaustion
2 · heal slowlythe game rule on, hurt, and 18 food or moreevery 80 ticks: half a heart, for 6.0 of exhaustion
3 · starvefood at zero — and not gated on the game ruleevery 80 ticks: half a heart of DamageTypes.STARVE, above the difficulty’s floor
4 · none of them—FoodData.tickTimer goes back to zero

The order is what makes the three exclusive, and it is why a full bar with saturation left heals you up to eight times faster than a bar at eighteen: both conditions hold at twenty food, and the fast one is tested first.

The game rule is GameRules.NATURAL_HEALTH_REGENERATION, which lives in world/level/gamerules with typed GameRule lookups (level data and rules). The starvation hit only lands if health is above five hearts, or above half a heart on Normal, or unconditionally on Hard: five hearts is the floor on Easy and Peaceful, half a heart on Normal, and death on Hard. DamageTypes.STARVE is declared with zero exhaustion, so starving does not feed itself.

Eating is a component walk

FoodProperties (DataComponents.FOOD) is three things: nutrition, saturation and can always eat. That is the whole of what a meal is worth. Everything about how long it takes, what it looks like and what else it applies is a different component on the same stack — Consumable, and using an item owns it.

The two meet through an interface, and that is the answer to the missing eat method. FoodProperties reaches the player by implementing ConsumableListener, and Consumable.onConsume walks every component of that type on the stack. It is not the only implementation — PotionContents, SuspiciousStewEffects and OminousBottleAmplifier implement it too, and PotionContents is how drinking applies an effect, which is where this page and status effects meet in one method. Two routes reach FoodData.eat without any of that: CakeBlock.eat, and the saturation effect, both using the overload that takes a nutrition and a saturation modifier.

The walk itself is five hand-offs on the server and one line out to the client, and the packet leaves before any of the five have run.

sequenceDiagram
    box transparent the server
    participant SP as ServerPlayer
    participant IStack as ItemStack
    participant Cons as Consumable
    participant FP as FoodProperties
    participant FD as FoodData
    end
    box transparent the client
    participant CPL as ClientPacketListener
    end

    SP->>SP: updateUsingItem — the zero check is server side only
    SP->>CPL: ClientboundEntityEventPacket(9), before the walk starts
    SP->>IStack: finishUsingItem — the item decides what finishing means
    IStack->>Cons: onConsume — walks every ConsumableListener on the stack
    Cons->>FP: onConsume — the food component is one such listener
    FP->>FD: eat — nutrition and pre-multiplied saturation, clamped
    Cons->>Cons: onConsumeEffects — server only, then consume(1)
    CPL->>CPL: the same walk again, locally — the food half unguarded
    SP->>FD: tick — exhaustion drain, then regen or starvation
    SP->>CPL: ClientboundSetHealthPacket — the prediction is overwritten

The second arrow leaves before the server has eaten anything, and it and the health packet at the end are the figure’s only arrows to the client (the meal’s sounds go as broadcasts): the five arrows after it on the left are the walk, and the client’s one self-call is that walk run again, its food half with no side guard.

Consumable.canConsume consults Player.canEat only when the stack has DataComponents.FOOD and the user is a player — potions and milk are ungated — and Player.canEat itself passes for invulnerable abilities, can always eat, or FoodData.needsFood. An item whose Consumable.consumeTicks is zero is consumed instantly with no animation. After the food lands, ItemStack.finishUsingItem applies DataComponents.USE_REMAINDER and DataComponents.USE_COOLDOWN (using an item).

The decision to finish is server-only, but the client replays the meal on an entity event and runs FoodProperties.onConsume and its FoodData.eat locally, with no side guard on it — which is why the hunger bar’s jump is predicted while a chorus fruit’s teleport is not (using an item). The prediction barely outlives the packet that caused it when the meal moved the bar: the server then sends ClientboundSetHealthPacket from the same tick, and ClientPacketListener.handleSetHealth overwrites food and saturation outright, and routes health through LocalPlayer.hurtTo, which works out the delta first so the damage flash still plays. The client also reads its food data for three decisions of its own: whether you may start eating at all (Player.canEat), sprinting, which is gated on having more than six food or being able to fly, and the HUD’s food-bar jitter, which reads saturation.

What crosses the wire for all of this is four packets of its own, beside the sounds it broadcasts — ClientboundEntityEventPacket for the finished meal, ClientboundSetHealthPacket (health, food and saturation together, to that player only), ClientboundSetExperiencePacket (progress, level and total) and ClientboundTakeItemEntityPacket for the orb pickup animation — and what is data-driven is DataComponents.FOOD, DataComponents.CONSUMABLE, DataComponents.USE_EFFECTS, the consume effects a Consumable lists (typed by Registries.CONSUME_EFFECT_TYPE), the damage types’ exhaustion and the three game rules this page names.

Eating slowdown is a third component again — UseEffects, on every item, which is why a meal and a drawn bow cost you exactly the same speed (using an item). It has nothing to do with the food: it is read while any item is held out, and a spear is the one definition that turns it off (the spear).

The other bar, and the number it is really watching

Player.experienceLevel, Player.experienceProgress, Player.totalExperience and Player.enchantmentSeed, plus Player.takeXpDelay and Player.lastLevelUpTime — which exists only to throttle the level-up sound — are the whole of it. The arithmetic is Player.giveExperiencePoints, Player.giveExperienceLevels and Player.getXpNeededForNextLevel — the three-segment curve with corners at levels 15 and 30.

Where orbs come from is worth naming, because two game rules gate it: LivingEntity.dropExperience requires the experience not to have been consumed already, and either an always-dropper or a recent player kill with GameRules.MOB_DROPS on and the entity’s own LivingEntity.shouldDropExperience agreeing; a player’s own death drop is Player.getBaseExperienceReward, seven per level capped at 100, unless GameRules.KEEP_INVENTORY — or unless you are a spectator, which drops nothing.

The packet that draws the bar watches one of those four fields, and it is not the one on the bar. ServerPlayer.lastSentExp is compared against Player.totalExperience, and ClientboundSetExperiencePacket goes out only when the two differ. Every mutation that changes the level without changing the total — ServerPlayer.setExperienceLevels, Player.giveExperienceLevels, enchanting, respawn — therefore has to poison the last-sent value by hand before the bar will move. Nothing in the game changes a level and lets the change detection notice: ServerPlayer’s version of each of those methods remembers to lie to it, and a new one that forgot would produce a client whose level is stale until the next orb.

ExperienceOrb is an Entity with ExperienceOrb.DATA_VALUE synched and ExperienceOrb.count, ExperienceOrb.age, ExperienceOrb.health and ExperienceOrb.followingPlayer unsynched — though ExperienceOrb.age and ExperienceOrb.followingPlayer are still mutated by the client’s own tick, which runs the follow behaviour locally. ExperienceOrb.health is not: once the orb is made or loaded, only ExperienceOrb.hurtServer writes it. ExperienceOrb.awardWithDirection splits an amount into denominations via ExperienceOrb.getExperienceValue (a fixed ladder from 2477 down to 1) and calls ExperienceOrb.tryMergeToExisting for each; ExperienceOrb.award is a one-line delegate to it. Merging is by count, not value: an orb carries one value and a multiplicity. The merge candidate search picks a random group number below ExperienceOrb.ORB_GROUPS_PER_AREA and only merges into orbs whose id is congruent to it — which keeps same-valued orbs in separate groups that never merge across, rather than reducing the scan.

Questions players ask

Why does a meal not seem to fill me? Because the number you cannot see is clamped to the one you can. FoodData.eat adds nutrition to the bar and saturation behind it, and then clamps saturation to the new food level — so the emptier you are when you eat, the more of the meal’s saturation is thrown away. A golden carrot carries 14.4 of it — nutrition 6 at a modifier of 1.2, doubled by FoodConstants.saturationByModifier — and eaten on an empty bar it leaves you at six food and six saturation, with more than half of what it was worth discarded. The reserve that funds fast healing is exactly what an emergency meal loses. At the other end the meal does not happen at all: Player.canEat refuses at a full bar unless the food is can always eat or your abilities are invulnerable.

Can my saturation sit above my food bar? Only on Peaceful with natural regeneration on, and only by less than a point at the top: ServerPlayer.tickRegeneration adds a whole point of saturation whenever it is below 20, while FoodData.eat clamps saturation to the food level. Only one of the two respects the clamp.

Why is my client’s saturation out of date? Because it is sent but not change-detected. ClientboundSetHealthPacket carries a full float, yet the server only notices whether saturation became zero — so your client’s saturation does not update until health or food moves or it crosses zero, and the one thing the HUD reads from it, whether it is zero, is the one thing sent on time.

Why is the Standard Galactic gibberish stable until I enchant? Because Player.onEnchantmentPerformed subtracts the level cost and re-rolls Player.enchantmentSeed, while AnvilMenu — which also spends levels, through Player.giveExperienceLevels — does not. A seed that loads back as zero is re-rolled on read. Enchanting owns what the seed is for. The levels the anvil eats and the seed the table reads are fields on the same object, and only one of the two spenders touches both.

Why does an orb repair my pickaxe before it reaches my bar? Because ExperienceOrb.playerTouch runs ExperienceOrb.repairPlayerItems first — mending, through EnchantmentEffectComponents.REPAIR_WITH_XP — and only the remainder becomes experience; that method then calls itself with the leftover. One orb entity can also be picked up many times: it carries a count, decremented per absorption behind a two-tick Player.takeXpDelay, and a player touches one orb per tick, chosen at random from those it is touching — so at most one is absorbed every two ticks. The pickup sweep buckets orbs separately from items for exactly that purpose.

Where to look

FoodData is short and is the food half entire — read it first, then FoodConstants beside it for the names of most of the numbers a decompile shows written into FoodData. Foods is where the built-in FoodProperties values live, if you want to know what a carrot is worth.

For the walk that reaches it, open Consumable.onConsume and follow it into ConsumableListener: FoodProperties is one implementation of four, and seeing the other three is what makes the missing eat method obvious. The experience half is one method, Player.giveExperiencePoints, plus ExperienceOrb for the denomination ladder and the merge. Then the two packets the whole page is about: ClientboundSetHealthPacket, which carries a number it does not change-detect, and ClientboundSetExperiencePacket, which change-detects a number it is not drawing.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Status effects

Verified against Minecraft 26.3 · Part VIII · Poison II is already on you: the server hurts you on a rhythm, and your client never runs a single one of the effect’s hooks — it only counts.

Poison II lands. Your health starts dropping in steps, the swirls appear, the icon appears in the corner, and if the connection stutters the timer beside your inventory keeps counting anyway. That last part is the whole page. The client runs none of a MobEffect’s behaviour. It counts durations down, advances a blend factor and spawns particles from a list the server synched. It does read the effects it holds — jump boost in LivingEntity.getJumpBoostPower, slow falling in LivingEntity.getEffectiveGravity, levitation inside LivingEntity.travelInAir (movement and collision) — because that is shared movement code your own player runs unguarded, your client simulating its own player in earnest (authority). But every attribute modifier, every pulse of damage, every regeneration tick happens on the server behind an explicit server-side guard, and apart from the packets an event sends — a new or changed effect, a hidden one surfacing, a respawn or a change of dimension — the client’s copy of the duration is corrected only when the server happens to notice, which it does by testing whether the remaining duration divides by six hundred. For a finite effect that is once every six hundred ticks. An infinite effect’s duration is −1, which divides by nothing, so that test never re-sends it.

The cast

classwhat it decidesthread
MobEffectwhat the effect does, and on what rhythm — the singleton, shared by every holderServer (the client reads its colour, blend durations, name and category, and for a tooltip its attribute templates)
MobEffectInstanceduration, amplifier, flags, and the masked effect underneathboth
MobEffectsthe forty built-in holders—
LivingEntityLivingEntity.activeEffects, the tick, and the three server-guarded hooksboth
AttributeInstancewhere an effect’s modifier landsServer (a syncable attribute’s base and modifiers are sent to the client)
ServerPlayerwho gets told, and how oftenServer
MobEffectUtilthe questions the rest of the game asks about effectsboth

What an effect is

MobEffect is the behaviour singleton: a category, a colour, a particle factory, blend durations and a map of MobEffect.AttributeTemplates. Its hooks are the interesting part, because one of them is false by default.

hookwhen it runs
MobEffect.shouldApplyEffectTickThisTickevery tick, to ask whether this is a pulse — false by default
MobEffect.applyEffectTickon a pulse; returning false ends the effect
MobEffect.applyInstantaneousEffectnever from the tick — only from the splash potion, the lingering cloud and the drink
MobEffect.onEffectAddedwhen it lands on an entity that did not already have it
MobEffect.onEffectStartedon every LivingEntity.addEffect that passes LivingEntity.canBeAffected, whether or not it changed anything
MobEffect.onMobHurtwhen the holder takes damage
MobEffect.onMobRemovedwhen the holder is killed or discarded

The default false is why each effect has its own rhythm, and poison’s, regeneration’s and wither’s are each one number right-shifted by the amplifier — halved per level, floored at one tick. Poison’s is 25, so Poison I pulses every 25 ticks and the Poison II of this page’s scenario every 12; regeneration’s is 50, wither’s 40, and hunger’s override skips the arithmetic and pulses every tick. Attribute modifiers go on as AttributeInstance.addPermanentModifier with an amount linear in amplifier + 1, computed by MobEffect.AttributeTemplate.create (attributes).

MobEffectInstance is the per-entity half: duration, amplifier, the ambient, visible and show-icon flags, a private blend state, and MobEffectInstance.hiddenEffect.

That last one is a stack, and it is where a weaker effect goes when a stronger but shorter one lands on top of it. MobEffectInstance.update builds the chain, so Strength I with nine minutes left survives underneath a Strength II that has thirty seconds, and surfaces again through MobEffectInstance.downgradeToHiddenEffect when the stronger one expires. Both of the instance’s codecs are recursive, so the save file and the wire format are each capable of carrying the whole chain.

The wire does not. ClientboundUpdateMobEffectPacket never uses that stream codec at all: it writes an entity id, a MobEffect holder, an amplifier, a duration and a flags byte by hand. So the client rebuilds an instance with nothing under it, and learns about the masked one only when the surfacing on the server triggers a fresh packet — which is why a weaker effect can appear from nowhere in your list the instant a stronger one runs out.

MobEffectInstance.INFINITE_DURATION is −1, and MobEffectInstance.compareTo is what orders both lists of effects a player sees — ambient last, infinite next, then by remaining duration, then by colour, except that two ambient effects, or two with more than 32,147 ticks left, skip the infinite and duration steps. The two surfaces sort it in opposite directions — the HUD reversed, the inventory list not — and the HUD also lays its icons out from the right, so the two end up in the same order: left to right along the top of the screen, top to bottom beside your inventory.

The blend state beside those flags is a pure render quantity: it ticks only on the client, is never saved and never sent, and only MobEffects.NAUSEA and MobEffects.DARKNESS declare blend durations to use it. The wire carries one bit for it, set only when an effect is first added — an update clears the bit, and a client that did not have the effect skips the blend, while one that did carries its blend over (MobEffectInstance.copyBlendState). That is why Nausea swims in when you eat a pufferfish and simply carries on when you eat a second.

LivingEntity.canBeAffected is the veto, and it consults three entity tags as well as the effect itself.

MobEffectCategory is the third small vocabulary word, and it is three constants — MobEffectCategory.BENEFICIAL, MobEffectCategory.HARMFUL and MobEffectCategory.NEUTRAL — carrying nothing but a ChatFormatting. Two things read it. A potion’s tooltip takes each line’s colour from MobEffectCategory.getTooltipFormatting, which is why neutral prints blue like a benefit; and the HUD asks MobEffect.isBeneficial, which is true of MobEffectCategory.BENEFICIAL alone, to decide which of its two rows an icon goes in — so a neutral effect sits on the bottom row with the harmful ones while its tooltip is coloured with the good ones.

Those three are the vocabulary. The registry that uses them, MobEffects, is forty entries, and every one is a Holder<MobEffect>, not a bare MobEffect — including MobEffects.BREATH_OF_THE_NAUTILUS. Some point at attributes a reader would not expect: MobEffects.JUMP_BOOST modifies Attributes.SAFE_FALL_DISTANCE, and MobEffects.INVISIBILITY modifies Attributes.WAYPOINT_TRANSMIT_RANGE.

Behind all of it is a fourth family the page’s own hook rests on: world/effect holds a small MobEffect subclass for each effect that needs behaviour — PoisonMobEffect, RegenerationMobEffect, WitherMobEffect, HungerMobEffect, AbsorptionMobEffect and eight more, one of them serving both instant health and instant damage — with InstantaneousMobEffect the base of those two and saturation, which act on every tick an instance of them lasts. Everything else in MobEffects is a plain MobEffect with no code, eleven of them carrying an attribute template. The rhythms above are those subclasses overriding two methods each; the family is the shape, and no one member is worth a lecture.

On the entity itself: LivingEntity.activeEffects (a plain unordered map), LivingEntity.effectsDirty, and two synched values — LivingEntity.DATA_EFFECT_PARTICLES, which is a list of ParticleOptions rather than a packed colour, and LivingEntity.DATA_EFFECT_AMBIENCE_ID. The dirty flag has exactly one reader, and it is not this page’s tick: LivingEntity.updateDirtyEffects drains it into the invisibility and glowing flags, the swirl list and the ambience value from inside LivingEntity.updateDataBeforeSync, which the server’s entity-sync pass calls before it looks at what changed (synched entity data). An effect that expired this tick therefore opens its own gate.

That particle list is also nearly all a watcher gets, and it is deliberately hard to see. The default particle factory bakes ambience into the ParticleOptions itself — alpha 38 of 255 instead of opaque — so an ambient effect really does synch a different, fainter particle; and the client spawns one particle from the list on a one-in-n roll whose n is four, fifteen if the entity is invisible, and multiplied by five again when every visible effect on it is ambient, which is what LivingEntity.DATA_EFFECT_AMBIENCE_ID records. An invisible, wholly ambient entity is rolling one in seventy-five; a visible one in the same state, twenty.

MobEffectUtil is the shared question-asking surface: MobEffectUtil.hasDigSpeed, MobEffectUtil.hasWaterBreathing, MobEffectUtil.shouldEffectsRefillAirsupply, MobEffectUtil.addEffectToPlayersAround and the duration formatter the inventory screen uses.

Poison II counting down on two machines

Effects are ticked from LivingEntity.tickEffects, the last call LivingEntity.baseTick makes before it copies this tick’s rotations into last tick’s — which for a player means inside ServerPlayer.doTick, the connection-driven half of the two-phase tick, not the level’s entity tick.

On the server, that tick is three objects and two packets:

sequenceDiagram
    participant SP as ServerPlayer
    participant MEI as MobEffectInstance
    participant ME as MobEffect
    participant Wire as the network

    SP->>MEI: update, when one is already there — a longer, weaker one goes under
    SP->>ME: addAttributeModifiers — LivingEntity.onEffectAdded, or onEffectUpdated on a change
    SP->>Wire: ClientboundUpdateMobEffect<br/>Packet
    rect rgba(0, 0, 0, 0.04)
        Note over SP,Wire: every server tick after that
        SP->>MEI: tickServer, from LivingEntity.tickEffects
        MEI->>ME: shouldApplyEffectTickThisTick — 25 halved per level, for poison
        MEI->>ME: applyEffectTick — the pulse, and false here ends the effect
        SP->>Wire: ClientboundUpdateMobEffect<br/>Packet, when the duration divides by 600
    end

Two arrows leave for the client and the second is the periodic correction: inside the band the duration is the server’s, and unless something changes the client hears about it only on the ticks where it divides by six hundred.

The client’s half of the same scenario is a different set of objects, and the first thing to notice about it is which class is not in it:

sequenceDiagram
    participant Wire as the network
    participant CPL as ClientPacketListener
    participant LE as LivingEntity
    participant MEI as MobEffectInstance

    Wire->>CPL: ClientboundUpdateMobEffect<br/>Packet
    CPL->>LE: forceAddEffect — a fresh instance, with nothing under it
    rect rgba(0, 0, 0, 0.04)
        Note over Wire,MEI: every client tick after that
        LE->>MEI: tickClient — count down and advance the blend
    end

There is no MobEffect lane here, and that absence is the page: the client holds the instance and counts it, and never once asks the singleton what the effect does.

The client branch of LivingEntity.tickEffects never calls MobEffect.applyEffectTick and never touches an attribute. It does not even remove an expired effect: it keeps a zero-duration instance until told otherwise. A client runs an effect’s hooks only when shared code calls LivingEntity.addEffect on it without a guard — a meal it replays (a suspicious stew, an ominous bottle), a turtle helmet worn out of water, a cookie fed to a parrot — and none of the hooks those reach does anything there.

What survives a save, and why the modifiers do

LivingEntity.addAdditionalSaveData writes the active list under active_effects through MobEffectInstance.CODEC, and only when the list is non-empty; LivingEntity.readAdditionalSaveData reads it back, clears LivingEntity.activeEffects and puts each instance straight in. Nothing on that path runs LivingEntity.onEffectAdded, so the hook that would install the attribute modifier never fires on load — and the effect’s Strength still works, because the modifier was written into the AttributeInstance as a permanent one and was itself saved and restored (attributes). The two halves of an effect are persisted separately and reunited by convention rather than by code, which is why the codec that can carry a hidden-effect chain matters at all.

Questions players ask

Why does my duration sometimes jump? Because it was wrong and got corrected. The client’s duration is a local countdown with nothing keeping it honest between corrections, and the periodic one is a divisibility test: LivingEntity.tickEffects calls LivingEntity.onEffectUpdated whenever the remaining duration divides by six hundred. The re-send only ever reaches the affected player or a player riding them — and never at all for an infinite effect, whose −1 divides by nothing.

Why can I not see how long a mob’s effect has left? Because you were never told. A client watching a mob it is not riding receives no MobEffectInstance at all — only what the effects show: the synched particle list and flags, and any syncable attribute an effect has changed — which is why other entities have swirls and no numbers.

Does an effect pulse on its own clock or the world’s? Never the world’s. MobEffectInstance.tickServer counts an infinite-duration effect’s pulses off the entity’s own age and a finite one off its own countdown.

What happens when one effect adds another? The rest of that tick’s effects are silently skipped. LivingEntity.tickEffects catches a concurrent-modification error and drops it, so an effect that adds or removes another quietly aborts the loop it was in.

Where does the effect come from in the first place? Not from this page. Everything above starts at the moment an instance exists on a LivingEntity; the machinery that puts one there — the use timer, the finish, the replay — is using an item; the components that ride on it, PotionContents and SuspiciousStewEffects among them, belong to hunger and experience, and the consume effects, ApplyStatusEffectsConsumeEffect among them, to using an item. What crosses the wire from here on is ClientboundUpdateMobEffectPacket and ClientboundRemoveMobEffectPacket for the effects you hold, LivingEntity.DATA_EFFECT_PARTICLES through synched entity data for everyone else’s swirls, and ClientboundUpdateAttributesPacket for an effect on a syncable attribute such as speed. The effects themselves are code, registered into BuiltInRegistries.MOB_EFFECT by MobEffects with no JSON behind them; what is data-driven is some of the ways they land.

Where to look

LivingEntity.tickEffects is the page in one method: the server branch and the client branch sit side by side in it, and the asymmetry is visible at a glance. Read MobEffectInstance next, for the hidden-effect chain and the two codecs that can carry it, then MobEffect for the seven hooks and the one that is false by default. PoisonMobEffect is a short subclass and is the cheapest way to see what an override overrides; world/effect has a dozen more of the same shape.

MobEffects is worth one pass for the two attribute targets nobody expects, and MobEffectCategory takes a minute and explains why a neutral effect is coloured like a benefit and filed with the harmful ones. When you want to know what the client is allowed to know, ClientboundUpdateMobEffectPacket is five fields written by hand and is the whole answer. Two doors this page only points at: LivingEntity.canBeAffected, the veto and its three entity tags, and MobEffectUtil, which is where the rest of the game asks its questions.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

IX · Networking

Verified against Minecraft 26.3 · Part IX · One socket, four languages, and everything the two halves of the game say to each other across it.

Parts II to IV had a single machine to describe, and Parts V to VIII compared two. This one is about what passes between them. A player meets the seam as a set of symptoms — the Connection lost screen with a reason string on it, the world that arrives in a pause and then a flood, the chest whose contents appear only when it is opened, the chat line that turns red and takes every line after it with it — and each of those can be a decision rather than a fault. Singleplayer runs the same wire, an integrated server on its own threads talking to the client through an in-memory channel, so the split is not a multiplayer feature bolted on the side: it is the shape of the program, and the wire is not a pipe between two halves of one machine but a border between two machines, and the server treats what arrives from a client as a claim rather than a fact.

The shape of the part

Part IX is the wire three times, and two things it carries. The first three lectures are all about the wire and are ordered by what they describe it as: bytes on a socket, then the values those bytes are, then the four languages the socket speaks in turn — a connection changes language as it goes from handshaking to playing, and the last of the three is where that happens. After those three, the part is mostly about what the wire carries rather than the wire. The last two both sit inside the play language: everything the server chooses to tell a client about the world, and the one protocol in the book in which what a player sends is signed, so that the server and the players it reaches can check it.

flowchart TD
    subgraph W["the wire, three times"]
      TC["1 · The connection — bytes to a handler call, and back"]
      PSC["2 · Packets and stream codecs — what crosses the wire"]
      PP["3 · Protocol phases — four languages over one socket"]
    end
    subgraph A["inside the play phase"]
      WCT["4 · What the client is told — the server's choosing"]
      CS["5 · Chat and signing — proving who sent a message"]
    end
    TC -- "two threads" --> PSC
    PSC -- "a codec table per phase" --> PP
    W ~~~ A

The five lectures, numbered to the watch order: the top box is the wire itself, the bottom box is the one language the last two live inside, and an arrow is what the lecture before it settles. Nothing points into the bottom box: its title says where the last two live, and neither needs the other or the lecture above it.

The spine is the first two: read them together, because the second is the second half of the first. The last two could hardly differ more — telling a client where the mobs are and sending it chunks is most of what a connection ever carries, while chat is a handful of packets a minute with a wholly disproportionate amount of cryptography on it.

Before you start

Part I’s anatomy first, for the two-loops figure. The client’s frame loop and the server’s tick loop are different clocks, and the client drains its inbound packets once per frame, not once per tick. That one fact is behind most of what looks like network jitter, and the first and fourth lectures lean on it.

Part III next, and not optionally, because two of this part’s claims are really facts about somebody else’s loop and Part IX states the consequence rather than re-teaching the loop: the server tick owns what happens after every level has ticked, and the level tick owns the phase in which broadcasts go out — before the entity phase, which is why one broadcast carries the block changes made so far this tick but the previous tick’s entity movement.

Then Part II for two objects this part assumes whole: codecs, because a packet codec is the same idea specialised to a byte buffer, and components, because what a chat line displays is one. And Part VI’s authority, which is the premise under lecture four: which side of the wire is allowed to decide where a thing is, and therefore what the other side may be told late.

Nothing here waits on Part X. The client’s half of the story is the deeper version of the first item above, and where its arithmetic would matter these pages state the consequence and link forward to it.

Watch in this order

  1. The connection — you swing at a pig, and some milliseconds later a method runs on the server’s game thread. The lecture with the there-and-back diagram, and then the whole life of the channel that carried it: two threads, two codec layers, one hop.
  2. Packets and stream codecs — the second half of the same lecture. What the thing crossing the wire is, now that you have watched it travel, and why its number is written down nowhere.
  3. Protocol phases — a login, from clicking a server in the list to standing in the world: four languages over one socket, and the surprising thing about when in that sequence a player becomes an object on the server.
  4. What the client is told — a creeper walks into view. Not a trace but a policy: three gates a change passes before it becomes a packet, and the things the server decides never to say.
  5. Chat and signing — the part’s closer, and the one system in the book whose messages are signed by the player who wrote them. What each check catches, and whether it kills the message, the chain, or the connection.

One and two are the pair to keep together; four and five can be watched in either order, and neither strictly needs three.

Where the part stops

Part IX’s packages hold 490 files and 38,711 lines, and the count flatters it, because this part owns the wire, not everything in network/: Component and the classes around it belong to Part II, much of client/multiplayer is the receiving end, which is Part X’s, and the largest block is the packet classes, catalogued in packets rather than narrated one by one.

Three systems in those packages this part names and does not teach. Player reporting is out of scope on what this book skips. The server list screen is one more screen for GUI and screens; only how a typed address becomes a socket belongs here. The boss-bar feed — ClientboundBossEventPacket, one packet with six operations — belongs to its model, scores, teams and stored data, and to the HUD. Past those, 8% of the part’s lines are named on no page in the book.

Outward, what the client does with what it is told is Part X’s, and how a ServerPlayer comes to exist at all is Part III’s. This part stops at the packet that says so.

Reference this part uses

Packets above all — the catalogue this part narrates, and the page to keep open beside every lecture in it. Then the threads, which names the Netty event loop and the two game threads and lists the nine handlers on the client’s play listener that never hop. Behind those: registries for what crosses during configuration, components where a packet carries a stack, level data and rules for the values a server sends on request rather than on change, and diagram lanes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The connection

Verified against Minecraft 26.3 · Part IX · you swing at a pig: one packet up and its answer out to the players watching, from a value on one thread to bytes on a wire to a method call on another — and then the whole life of the channel that carried it.

You swing. ServerboundPunchPacket — an immutable value holding nothing at all, not even which hand — is handed to Connection.send on the client’s Render thread, and some milliseconds later ServerGamePacketListenerImpl.handlePunch runs on the server’s game thread with that value as its argument. What comes back — unless your last swing is still in its first half, when nothing does — is ClientboundSwingAnimationPacket, and it goes to every player tracking you except you: your own arm was already swinging, because the client moved it the moment you clicked. Now close the server list and open a singleplayer world. Every sentence above is still true. The integrated server is another thread in the same process, and the client reaches it through a real Netty channel with a real PacketEncoder and a real PacketDecoder in it: singleplayer serialises every packet to bytes and parses them back again. There is no local shortcut — the same encoder runs, the same decoder runs, and what reaches the server is what the decoder made of the bytes.

That crossing is the first half of this page. The second half is the channel it travelled on, from the first listener installed on a raw socket to the fault that closes it: one connection’s whole life, which is more than one packet’s journey and is where everything surprising about the transport lives.

The cast

classwhat it decidesthread
Connectionthe channel, the current PacketListener, and when a fault kills the linkNetty event loop, plus its tick, its sends and its flushes from a game thread
PacketEncoderone packet becomes the bytes of one frame, or is skippedthe sender’s Netty event loop
PacketDecoderone frame becomes one packet, and a terminal one dismantles the codecthe receiver’s Netty event loop
PacketProcessorthe queue that carries a decoded packet to a game threadfilled from Netty, drained by its owner
PacketListenerwhether this packet should be handled at all — asked twiceboth
TickablePacketListenerthe only way a listener gets time when no packet has arriveda game thread
ServerConnectionListenerthe server’s accept side, and which connections get tickedNetty for the accept, Server for the tick
EventLoopGroupHolderNIO, Epoll, KQueue or in-memory, and the threads that run themany

One packet there, and one back

sequenceDiagram
    box transparent your client
    participant CPL as ClientPacketListener
    participant PEnc as PacketEncoder
    end
    box transparent the server
    participant PDec as PacketDecoder
    participant Conn as Connection
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    end
    box transparent a player watching you
    participant RCPL as ClientPacketListener
    end

    Note over CPL: Render thread — a value, not yet any bytes
    Note over CPL,PEnc: Connection.send, then Connection.sendPacket joins the client's Netty event loop
    CPL->>PEnc: write and flush — no packet queue, Netty buffers
    PEnc->>PDec: one whole frame: a VarInt id, and no fields at all
    Note over PDec,SGPL: the server's Netty event loop
    PDec->>Conn: channelRead0, at the tail of the pipeline
    Conn->>SGPL: shouldHandleMessage, then Packet.handle
    SGPL->>SGPL: ensureRunningOnSameThread<br/>queues the pair and aborts
    rect rgba(0, 0, 0, 0.04)
        Note over PDec,SGPL: Server thread — the PacketProcessor drain, before the tick
        SGPL->>SGPL: shouldHandleMessage again, then the handler from the top
        SGPL->>RCPL: the swing, through each watcher's own Connection, flushed at once
    end
    Note over RCPL: their client's Netty event loop
    RCPL->>RCPL: ensureRunningOnSameThread<br/>queues the pair and aborts
    rect rgba(0, 0, 0, 0.04)
        Note over RCPL: Render thread — the drain, once per frame
        RCPL->>RCPL: the handler from the top, by the next frame
    end

The punch and its answer, one machine to a box: the server’s answer goes to the players watching you and never back to you, and the one arrow into their client folds away its encoding, its decoding and the Connection at each end of the watcher’s link. Watch for each handler body being entered twice.

Six things in it are worth stopping on — the framing, the direct call, the hop, the drain’s own tick phase, the second question the drain asks, and where an error on re-dispatch goes.

Framing is separate from decoding. Varint21FrameDecoder reads at most three length bytes, refuses anything wider and refuses a zero length, and emits exactly one frame or nothing at all. Everything downstream of it may assume it is looking at one whole packet, which is why the codec layer never has to handle a half-arrived value.

Connection.channelRead0 calls Packet.handle directly, on the Netty thread. There is no automatic hop. PacketListener.shouldHandleMessage is consulted first — which is how a listener being torn down ignores what is still arriving — and Connection.receivedPackets counts only the packets that pass it. Three other outcomes live in that method: a packet arriving before any listener is set is an illegal state; a packet whose listener does not implement the packet’s listener interface is a failed class cast and an invalid_packet kick; and a rejected schedule — the server’s PacketProcessor closed because it is shutting down — becomes a server_shutdown kick.

The hop is the handler’s own first line. PacketUtils.ensureRunningOnSameThread asks the PacketProcessor whether this is the right thread; if it is not, it enqueues the listener-and-packet pair and throws the singleton RunningOnDifferentThreadException, a stackless exception that Connection.channelRead0 catches and drops on the floor. The handler body then runs again from the top when the queue is drained, which is why a handler method does nothing observable before that line unless it means to — the client’s transfer handler raises its transferring flag first, so that ClientCommonPacketListenerImpl.shouldHandleMessage already sees it. A handler that touches no game state — the pong bookkeeping, the chunk-batch clock, the keep-alive answer — simply omits it and runs on Netty; the client’s play listener has nine, listed in threads. An unknown custom payload never reaches a game thread either, though its handler has the line: it decodes as a DiscardedPayload, which ClientCommonPacketListenerImpl drops on the Netty thread before the hop it takes for every other payload.

The drain has a tick phase of its own, and the two sides do not schedule it alike. The server drains before the tick proper, so every packet that arrived since last time enters the world at one point (the server tick); the client drains once per frame, not once per tick, because its tick is a sub-step of the frame loop (two loops and a wire between them, and the client loop for the arithmetic). The consequence for a packet is that its handling latency on the client is a frame, not a tick — and that packet handling and the tasks posted to a game thread are two different queues drained at two different moments on both sides.

The drain re-asks the same question. PacketProcessor.ListenerAndPacket calls PacketListener.shouldHandleMessage a second time before dispatching, and logs and drops if the answer has changed. That is the gate that matters: a packet can wait between the two checks, and a disconnect arriving in between must not be able to run its handler.

Errors on re-dispatch go somewhere else entirely. The drain catches exceptions only — a bare out-of-memory error is not caught at all — and routes what it does catch to PacketListener.onPacketError. The interface’s own default raises a reported crash, and no listener a drained packet reaches uses it: every serverbound listener inherits ServerPacketListener’s override, which logs and returns (the server tick owns what the server then does with the exception), and ClientCommonPacketListenerImpl overrides it the other way, writing a disconnection report and hanging up. The one special case is a ReportedException caused by an out-of-memory error: that is rethrown, but not untouched. PacketUtils.makeReportedException delegates both steps to PacketUtils.fillCrashReport, which first decorates the report with an Incoming Packet category naming the type and its terminal and skippable flags, then lets the listener add its own detail.

The pipeline, in both directions

Two directions through one list of handlers. Inbound runs head to tail; outbound runs tail to head, and the last column below is that second reading — six of the nine have an outbound mirror and the other three are inbound-only. Handlers in italics are added later, if at all.

inbound orderhandleradded byits outbound mirror
1"timeout" — a read timeout of thirty secondsthe connect or accept site, before serialization—
2"legacy_query" — LegacyQueryHandlerServerConnectionListener.startTcpServerListener only, and only if the server replies to status; removes itself on the first modern byte—
3"decrypt" — CipherDecoderConnection.setEncryptionKey"encrypt", last out
4"splitter" — Varint21FrameDecoderConnection.configureSerialization"prepender" — Varint21LengthFieldPrepender, its exact inverse
5"decompress" — CompressionDecoderConnection.setupCompression, inserted directly after "splitter""compress"
6an unnamed flow-control handlerConnection.configureSerialization—
7"decoder" or "inbound_config"Connection.configureSerialization"encoder" or "outbound_config"
8"bundler" — PacketBundlePackerConnection.setupInboundProtocol, only for a protocol with a bundle (packets and stream codecs)"unbundler" — PacketBundleUnpacker
9"packet_handler" — the Connection itselfConnection.configurePacketHandleritself, and then "hackfix"

So outbound, from the game outwards, is that column read upwards: "packet_handler", then "hackfix" — an anonymous pass-through that Connection.configurePacketHandler adds immediately before it, whose write method does nothing but call its superclass — then the unbundler, the encoder, the compressor, the prepender, the cipher, and the socket.

Which side gets a live codec at birth is decided by direction. The end that will receive the handshake — the server — is built with a real "decoder" and a dead "outbound_config" placeholder; the end that will send it gets a real "encoder" and a dead "inbound_config". Only the live one is built from Connection.INITIAL_PROTOCOL, which is HandshakeProtocols.SERVERBOUND — one of the nine per-phase codec tables packets and stream codecs builds. The placeholder is an UnconfiguredPipelineHandler.Inbound or UnconfiguredPipelineHandler.Outbound holding no protocol at all, which is the entire point of it.

HandlerNames holds most of those names as constants, the local pipeline’s latency among them, and none for hackfix.

The threads underneath it

EventLoopGroupHolder owns the event-loop groups: four instances behind two accessors, EventLoopGroupHolder.local for the in-memory channel and EventLoopGroupHolder.remote, which tries KQueue and then Epoll only if the native-transport flag is set and otherwise goes straight to NIO. That flag is the client’s option and the server’s use-native-transport property, so switching it off really does change transport rather than hint at it. The class lives in server/network and the client uses it too: ConnectScreen, RealmsConnect and the multiplayer screen’s ServerSelectionList — whose saved-server rows are the ServerData that ServerList keeps on disk — ask for a group, and the list hands the one it got to ServerStatusPinger, which never asks for its own.

What the client dials is not what the player typed

One package, client/multiplayer/resolver, stands between the two and nothing else in the book uses it: ServerAddress.parseString splits host from port, ServerNameResolver resolves it, ServerRedirectHandler looks up the _minecraft._tcp SRV record that lets a server answer on a port nobody types, AddressCheck asks the account service whether the address is blocked, and the answer is a ResolvedServerAddress. LegacyServerPinger is the client half of the "legacy_query" row above, for listing a pre-1.7 server.

Singleplayer runs the same pipeline

Connection.configureInMemoryPipeline builds the local variant, and the differences are smaller than almost anyone assumes.

  • "splitter" and "prepender" become LocalFrameDecoder and LocalFrameEncoder, which do nothing but HiddenByteBuf.unpack and HiddenByteBuf.pack — no length prefix, because the buffer never becomes a byte stream.
  • No read timeout on either side. What that costs the singleplayer host is the keep-alive section’s, below.
  • No legacy query handler, and never a cipher or a compression handler — though by two different mechanisms. Compression is refused at the installation sites, which both test Connection.isMemoryConnection. Encryption is not: neither Connection.setEncryptionKey nor either side’s key handler asks. What prevents it sits one gate further up, in ServerLoginPacketListenerImpl, where the decision to ask for encryption requires authentication and a non-memory connection, so ClientboundHelloPacket is never sent and the ciphers are never reached.
  • Everything else is identical, with one debug exception. PacketEncoder and PacketDecoder are still there, still running the same stream codecs, and singleplayer pays the full serialisation cost; the only handler that exists only on the local pipeline is ServerConnectionListener.LatencySimulator, installed when SharedConstants.DEBUG_FAKE_LATENCY_MS is positive.

The integrated server binds its channel through EventLoopGroupHolder.local and ServerConnectionListener.startMemoryChannel hands back an address that Connection.connectToLocalServer dials.

Sending, and why nothing queues on the way out

Outbound is the mirror image and shorter. Connection.send reaches Connection.sendPacket, which checks whether the calling thread is already the channel’s event loop and, if not, schedules the write onto it — so a packet sent from the game thread crosses the same boundary an inbound packet does, just without a queue of its own. Connection.doSendPacket then chooses between a write and a write-and-flush, and between a real future and Netty’s void promise; Connection.flushChannel performs the same hop for a bare flush.

PacketSendListener is the callback that rides along. PacketSendListener.thenRun runs something once the packet is really on the wire — which is how compression and the client’s encryption are installed after the packet that announced them, and how a disconnect waits for its own kick message to leave. PacketSendListener.exceptionallySend sends a fallback packet when the write fails. Both run on the event loop, which is why “disconnect after sending” is not a game-thread operation.

Why a tick’s packets leave in two writes, not fifty

The server does not flush per packet: ServerCommonPacketListenerImpl.send turns a send into a write with no flush while ServerCommonPacketListenerImpl.suspendFlushing has set the flag. The one thing about that bracket which is a fact about the channel rather than about the tick is that the flag is tested together with the thread check, so it is honoured only for a caller on the connection’s owning game thread and anything sent from another thread flushes on its own regardless. Which two moments empty the buffer, and in what order, belongs to the server tick — the bracket opens at the top of MinecraftServer.tickChildren, which is why the packet drain that runs before it is outside the bracket entirely.

Connection.tick, the tick a game thread gives the connection

Connection.tick is also the only place a listener gets time on a game thread without a packet having arrived. It drains Connection.pendingActions (the one queue Connection owns, holding closures rather than packets, and mattering only in the window before the channel exists); ticks the listener if it is a TickablePacketListener; calls Connection.handleDisconnection if the channel has died; flushes; every twentieth tick runs Connection.tickSecond, which rolls the packet-rate averages; and last, samples bandwidth. Note the order: the flush is in the middle, and the disconnect check happens before it.

The early phases have no hop at all, and a tick is what advances them. None of ServerHandshakePacketListenerImpl, ServerStatusPacketListenerImpl, ServerLoginPacketListenerImpl or the client’s ClientHandshakePacketListenerImpl contains a single PacketUtils.ensureRunningOnSameThread call, so in those phases every protocol swap a handler makes and the encryption setup happen on the event loop. What runs on a game thread is this method: ServerLoginPacketListenerImpl is a TickablePacketListener, and its tick is what performs the ban and whitelist checks, switches compression on, and sends the terminal packet that ends the phase. Login is a three-thread state machine, and protocol phases walks it.

Its callers differ by side. The server has one, MinecraftServer.tickConnection, which walks ServerConnectionListener.tick. The client has six, because it has more than one connection and more than one thing that waits on one: MultiPlayerGameMode.tick ticks the play connection; ConnectScreen, RealmsConnect and ServerReconfigScreen tick the one they are waiting on; Minecraft.tick ticks Minecraft.pendingConnection, the singleplayer world’s until its level arrives; and ServerStatusPinger ticks the connections it opened to ping the servers in the list. Note the clock: all six run at tick rate, not at the frame rate the drain above runs on.

A throw out of Connection.tick, and why the channel decides what it costs

ServerConnectionListener.tick catches an exception out of Connection.tick (an error passes through) and kicks that client with “Internal server error” — unless Connection.isMemoryConnection, in which case the same catch rethrows it as a fresh reported crash named “Ticking memory connection” and takes the integrated server down. One catch, two branches, and the branch is the channel rather than the fault: the same bug that costs a multiplayer client its seat costs a singleplayer player their world.

A phase change is a message written down the pipeline

Connection does not know which phase it is in. There is no protocol field and no getter; the answer is distributed between the two codec handlers currently in the pipeline and the listener object, and every place Connection names a ConnectionProtocol is a comparison rather than a reading — Connection.validateListener checks a new listener against the protocol it is being installed for, Connection.setupOutboundProtocol asks only whether this is login, and the handshake entry point asks only whether the listener is the initial one. That is what makes the next paragraph possible.

The codecs are swapped by writing through the pipeline, not by editing it from outside. Connection.setupInboundProtocol validates that the new listener’s direction and phase match the ProtocolInfo, assigns Connection.packetListener, and then builds an UnconfiguredPipelineHandler.InboundConfigurationTask — a closure that will replace the current handler with a new PacketDecoder and turn auto-read back on, optionally adding "bundler" after it. Auto-read is Netty’s own switch for whether a channel keeps pulling bytes off the socket without being asked; turning it off is how the game stops delivery in the middle of a batch while it swaps codecs, and it is the mechanism the rest of this section turns on. That task is written down the channel, where UnconfiguredPipelineHandler.Inbound recognises it and runs it with the right context, and Connection.syncAfterConfigurationChange blocks the caller until it completes. Connection.setupOutboundProtocol is the mirror image, and also records whether the new outbound protocol is the login one, for the benefit of the disconnect path.

What a terminal packet does to the codecs

Returning to the unconfigured state is automatic, and it is asymmetric. ProtocolSwapHandler.handleInboundTerminalPacket fires when a packet whose Packet.isTerminal is true passes through PacketDecoder: it turns auto-read off, inserts a fresh UnconfiguredPipelineHandler.Inbound under the name "inbound_config", and removes the decoder. ProtocolSwapHandler.handleOutboundTerminalPacket does the equivalent on the encoder side — but there is no incoming flow to stop, so it leaves auto-read alone and simply puts an UnconfiguredPipelineHandler.Outbound in the encoder’s place. The bundler and unbundler remove themselves on the same signal, and PacketBundlePacker treats a terminal packet arriving inside a bundle as a decode error rather than a swap.

So a phase change reads: terminal packet, codecs self-destruct and inbound reads stop, the terminal packet’s handler installs the new protocol (on the event loop in the early phases and in the server’s return to configuration, on a game thread otherwise), a configuration task travels the pipeline in order with the byte stream, reads resume. The unnamed flow-control handler between "splitter" and the decoder is what makes turning auto-read off stop delivery mid-batch. Which phases exist, and what ends each of them, is protocol phases.

The first listener, and who is allowed to install it

The server’s first listener is installed by Connection.setListenerForServerboundHandshake, which refuses if one already exists and refuses on a connection that is not receiving serverbound traffic in the handshake protocol — so it is server-side by construction. The client has no equivalent: its first listener arrives with the pair of protocols that Connection.initiateServerboundConnection installs around ClientIntentionPacket, and Connection.initiateServerboundStatusConnection is the same code with a different intent.

Compression and encryption arrive behind the packet that announces them

Compression is Connection.setupCompression, which inserts CompressionDecoder after "splitter" and CompressionEncoder after "prepender", or re-thresholds them if they already exist; a negative threshold removes both. The server turns it on during login, sending ClientboundLoginCompressionPacket with a send-listener that installs the handlers only after that packet is on the wire, and the client installs its own side when it handles the packet. Both sides skip it entirely on a memory connection. The asymmetry worth knowing is that the server validates that a compressed frame really was above the threshold and under the decompressed ceiling, and the client checks neither; the ceilings are in packets and stream codecs.

Encryption is Connection.setEncryptionKey, which inserts CipherDecoder before "splitter" and CipherEncoder before "prepender" — so decryption is the first thing that happens to inbound bytes and encryption the last thing before outbound bytes leave. Neither is ever removed. The server installs its ciphers synchronously the moment it handles the key packet; the client attaches its own to the send of that packet, so the key packet itself goes out in the clear and everything after it does not.

How a connection dies

Connection.exceptionCaught is the funnel, and it has four outcomes.

  • A SkipPacketException is logged and swallowed: the codec layer has already decided this one packet may be dropped, and the connection survives. It is the one marker that does not end the connection — an empty interface, implemented by SkipPacketDecoderException and SkipPacketEncoderException, one per direction.
  • A timeout — the thirty-second read timeout expiring — disconnects with disconnect.timeout, the “Timed out” a player sees.
  • Any other fault, the first time: the listener is asked for a DisconnectionDetails through PacketListener.createDisconnectionInfo; if this end is the one sending clientbound traffic it tries to tell the peer why, with either ClientboundLoginDisconnectPacket or ClientboundDisconnectPacket depending on Connection.sendLoginDisconnect, and disconnects once that packet has gone. Connection.setReadOnly follows on both branches — on the sending one as soon as the write is handed over, without waiting for it to complete.
  • Any other fault, the second time — a fault while handling a fault, which Connection.handlingFault detects — skips the message to the peer and disconnects immediately.

Connection.setReadOnly is how a pending disconnect ignores the rest of the stream: it turns auto-read off and leaves the peer’s remaining packets unread. Connection.handleDisconnection is the other end of the story. It runs only once the channel is really closed (from the tick that finds the connection dead, or straight after a disconnect), reports to the listener — or to Connection.disconnectListener, the client’s connect-attempt fallback, if the connection never got a real one — and is guarded by Connection.disconnectionHandled so that it reports at most once.

Keep-alive is the real timeout on a live connection, and it belongs to the common listener rather than to the play one, so it runs in configuration too. ServerCommonPacketListenerImpl.keepConnectionAlive sends a challenge every ServerCommonPacketListenerImpl.LATENCY_CHECK_INTERVAL milliseconds — fifteen seconds — and disconnects with ServerCommonPacketListenerImpl.TIMEOUT_DISCONNECTION_MESSAGE if the previous one was never answered. An answer carrying the wrong id is disconnected with the same message, so a stale reply disconnects immediately rather than being ignored. The round trip a correct answer measures is not used raw: it is smoothed three parts old to one part new, which is why a tab list lags a genuine latency change by several pings. Keep-alive stops once the listener has closed itself behind a terminal packet; from that moment ServerCommonPacketListenerImpl.checkIfClosed gives the protocol swap another fifteen seconds and then times the connection out. The thirty-second read timeout exists only on socket connections, so the singleplayer host has neither clock running against them: no read timeout on the in-memory pipeline, and the exemption ServerCommonPacketListenerImpl.isSingleplayerOwner grants here, from the one kick of the game listener’s three that could reach the host (players and sessions).

Questions players ask

Does the server drop packets when it is behind? Not for being late. Connection keeps no outbound packet queue at all — once the channel exists, everything written goes into Netty’s own buffer, and nothing in the game asks whether that buffer is full — and inbound, the PacketProcessor’s queue is unbounded and each drain empties it. What a slow server costs you is latency, not messages, until the keep-alive gives up.

Why does kicking someone sometimes stall the caller? Never when /kick does it: the kick and ban commands defer Connection.disconnect — the call that blocks on the channel close — to an event-loop callback, and the BlockableEventLoop.executeBlocking after it runs Connection.handleDisconnection in place on the Server thread, which returns at once while the channel is open. What stalls is elsewhere: a login refusal from the login’s tick calls Connection.disconnect itself, and the Server thread waits for the socket to shut; a kick made on the Netty thread, such as a wrong keep-alive id, waits in BlockableEventLoop.executeBlocking for the Server thread.

Is a flood of packets rate-limited? Only if the server was configured for it. RateKickingConnection overrides Connection.tickSecond to kick a client whose average received-packet rate exceeds RateKickingConnection.rateLimitPacketsPerSecond, and the socket accept site builds one only when the server’s rate limit is above zero — which it is not by default; the in-memory one never does. An ordinary socket gets a plain Connection.

Why is the network graph in the debug screen client-side only? Connection.bandwidthDebugMonitor is inbound-only and socket-only, set from ConnectScreen and the Realms connect path and nowhere else, so no server ever has one. MonitoredLocalFrameDecoder exists for the singleplayer case and is never installed: the local pipeline is always built with a null monitor.

That is the transport: one wire, two ends, a thread hop at each of them, and a picture that does not change between singleplayer and a public server. What crosses — what a packet class has to declare, how its fields become bytes, how the far side knows which class to build, and what stops a hostile sender from allocating a gigabyte — is the other half of this lecture, packets and stream codecs.

Where to look

Connection is the page, and it is worth reading in three sittings rather than one. First Connection.channelRead0 and Connection.exceptionCaught — the two ends of a packet’s life at this class, arrival and fault. Then Connection.configureSerialization and Connection.setupInboundProtocol beside each other, which is the pipeline built once and then rebuilt by writing through itself. Then Connection.tick, six duties in one method and the one whose job is the tick.

Around it, in the order this page meets them: PacketUtils.ensureRunningOnSameThread for the hop, one test and the reason every handler is written the way it is; PacketProcessor for what the hop enqueues and who drains it; PacketDecoder and PacketEncoder for the two places a codec runs; and UnconfiguredPipelineHandler with ProtocolSwapHandler, the placeholders and the helpers the codecs call to become them, which between them are the phase change. ServerConnectionListener is the accept side and the one catch that treats a memory connection differently.

Four doors this page only points at: Varint21FrameDecoder, which defines what a frame is; PacketSendListener, for everything the game does after a packet is really on the wire; RateKickingConnection, the subclass almost nobody runs; and EventLoopGroupHolder, where the choice of transport is made.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Packets and stream codecs

Verified against Minecraft 26.3 · Part IX · What the thing crossing the wire is: a value with no bytes, a number nobody chose, and the limits that stop a hostile sender.

The connection hands you a frame: a length, then a run of bytes that a handler on a Netty thread is about to turn into a method call on the other side. This page is what is inside that frame. A chat line leaving the server is a ClientboundSystemChatPacket — a record of a Component and a boolean — and it has no write method, no byte layout of its own, and no number. The number that goes on the wire in front of it is written down nowhere in the game: not on the packet, not on its PacketType, not in any table a human maintains. A packet’s id is the position of one line in a chain of registration calls. Insert or remove one of those lines and every packet after it renumbers — and because two dozen packet types are registered into several phases, the same packet type is a different number in each phase it appears in.

The cast

classwhat it decidesthread
Packetthat a message is a value with a type, one handler method and two flags — and nothing at all about bytesany
PacketTypewhich message this is: a direction and a name, and no number—
StreamCodecone value’s bytes — an encoder and a decoder over a ByteBuf, composed out of its fields’ codecsNetty
ByteBufCodecsthe primitive vocabulary the packet codecs are built from, and where the read limits sitNetty
IdDispatchCodecone codec for a whole phase: a var-int id, then delegate to the entry that id namesNetty
ProtocolInfowhat a configured connection holds — the phase, the direction, that one codec, and the bundlerNetty
ProtocolInfoBuilderthe registration chain, and therefore every packet number in the gameclass-load, or the configuration-to-play swap
RegistryFriendlyByteBufthat a registry id on the wire means something, by carrying the RegistryAccess it is relative toNetty

The catalogue of which packets exist is generated, not written: reference/packets.md lists all 235 packet types across the eight *PacketTypes classes that declare them.

A packet is a value, a name and a direction

Packet is an interface with four methods and one static helper.

memberwhat it says
Packet.typethe PacketType, always a constant from one of the eight *PacketTypes classes
Packet.handlehands this packet to one named method on the phase’s listener interface — see below
Packet.isSkippabledefault false. True for five packets, so a failure to encode one is dropped rather than fatal
Packet.isTerminaldefault false. True for the seven packets that end a protocol phase
Packet.codeca static convenience: StreamCodec.ofMember under a friendlier name

Exactly one family of packets refuses to be handled at all: BundleDelimiterPacket makes Packet.handle final and throws, because a delimiter is consumed by the pipeline and must never reach a listener. It is abstract and has one subclass, ClientboundBundleDelimiterPacket, which is the one that is actually registered — see a bundle is two empty markers, below. The skippable set is five packets, and what they share is not chat: each carries a payload composed at run time from data rather than assembled from fixed fields — a Component in four of them (ClientboundSystemChatPacket, ClientboundPlayerChatPacket, ClientboundDisguisedChatPacket and ClientboundPlayerCombatKillPacket, the death message) and an arbitrary CompoundTag in the fifth, ClientboundTagQueryPacket, which answers the debug key that copies a targeted block or entity with its data. Those are the payloads likeliest to fail to encode, and the five whose failure to encode a connection survives. The terminal set is exactly the seven transition packets drawn on protocol phases and no others; what the flag does to the pipeline is the business of the connection. Beware the namesake: ServerboundResourcePackPacket.Action.isTerminal asks whether a resource-pack response is a final answer, and that packet is not terminal.

The interface Packet.handle targets is per phase and per direction, and those interfaces are the quietest large family in the part: ClientGamePacketListener and ServerGamePacketListener are the two big ones — one method per packet, which is why the play pair declares nearly two hundred methods — with ClientCommonPacketListener and ServerCommonPacketListener above them and a pair each for status, login, configuration, cookie and ping. Handshake has only a serverbound one, and every phase’s interface is rooted in ClientboundPacketListener or ServerboundPacketListener. Beyond ServerPacketListener’s error policy, each phase’s interface naming its protocol and the two roots their direction, nothing in them decides anything: they are the vocabulary a phase’s *Impl class promises to implement, and a packet arriving at a listener that implements the wrong one is the failed class cast and invalid_packet kick the connection describes (protocol phases tabulates which *Impl serves which phase).

PacketType is a record of two things — a PacketFlow and an Identifier. A direction and a name; no number, no version, no size. PacketFlow is the two-constant enum PacketFlow.SERVERBOUND / PacketFlow.CLIENTBOUND, with PacketFlow.getOpposite and PacketFlow.id.

Three shapes of packet class coexist in the tree, and two of them carry the argument. The modern one is a record whose STREAM_CODEC is a StreamCodec.composite naming each component’s codec and accessor — ClientboundSystemChatPacket is one. The older one is a plain class with a private buffer constructor and a private write method, joined into a codec by Packet.codec; ClientboundKeepAlivePacket and ClientboundSetHealthPacket are these, and StreamMemberEncoder exists chiefly so that second form can bind a member reference as its encoder half — CustomPacketPayload, DisplayInfo and TrackedWaypoint bind one too. The third shape is the one with no fields to serialise at all: fifteen packet classes that declare a StreamCodec.unit codec, every one but ServerboundPlayerLoadedPacket a singleton (the bundle delimiter is handed one by ProtocolInfoBuilder.withBundlePacket), of which ServerboundFinishConfigurationPacket is the one this part’s login trace turns on.

From two fields to a numbered blob

flowchart TD
    subgraph V["knows chat, not ids"]
      direction TB
      F1["content, a Component"]
      F2["overlay, a boolean"]
      SC1["ComponentSerialization.<br/>TRUSTED_STREAM_CODEC"]
      SC2["ByteBufCodecs.BOOL"]
      COMP["ClientboundSystemChatPacket.STREAM_CODEC, a StreamCodec.composite of both"]
      F1 --> SC1
      F2 --> SC2
      SC1 --> COMP
      SC2 --> COMP
    end
    COMP --> ENTRY["one ProtocolInfoBuilder.addPacket call, that codec against a PacketType"]
    subgraph I["knows ids, not chat"]
      direction TB
      WRAP["StreamCodec.mapStream, at bind: a fresh RegistryFriendlyByteBuf per call"]
      DISP["IdDispatchCodec, one for the clientbound play phase"]
      WRAP --> DISP
    end
    ENTRY --> I
    DISP --> OUT["on the wire: a VarInt id, then the fields"]

How one packet class is assembled, cut at the line the section is about: above it nothing knows an id, below it nothing knows chat. This is the order things are built in — at send time the IdDispatchCodec runs outermost, writing the id and then handing the value back up to the composite codec.

Read it downwards and you have the page. Nothing above the ProtocolInfoBuilder.addPacket line knows anything about ids, and nothing below it knows anything about chat.

One codec serves an entire phase. ProtocolInfo.codec is a single StreamCodec — an IdDispatchCodec — and not a table the encoder walks: encoding looks the PacketType up in a map, and a type never registered in this protocol is an encoder error naming the unknown packet, which is how a configuration-phase packet sent during play fails. Decoding reads the id, bounds-checks it and delegates — and then must have consumed the frame exactly, or PacketDecoder raises an error naming how many bytes were left over. The check sits where the frame’s length is still known. All of that runs on the Netty event loop, inside PacketEncoder and PacketDecoder, never on a game thread; the framing, the compression, the ciphers and the later hop to the game thread belong to the connection.

The codec layer is small, and composition is all of it

net/minecraft/network/codec is a handful of files and does all of the composing; the byte-level encodings themselves live outside it, and are named below. StreamCodec is one interface extending StreamEncoder and StreamDecoder, so StreamEncoder.encode takes a buffer and a value and StreamDecoder.decode takes a buffer and returns one. It is deliberately not a Codec: a packet is written once, read once and must be small, so it gets hand-laid bytes rather than a document in some format — the distinction belongs to codecs, NBT and JSON.

The constructors and combinators are StreamCodec.of, StreamCodec.ofMember, StreamCodec.unit, StreamCodec.map, StreamCodec.mapStream, StreamCodec.apply, StreamCodec.dispatch, StreamCodec.recursive and StreamCodec.cast — plus StreamCodec.composite in fourteen arities, one through fourteen pairs of codec and getter followed by a constructor. Fields encode and decode strictly in argument order, and that ordering is the format specification: there is no other statement anywhere of what a packet’s bytes look like. StreamCodec.CodecOperation lets StreamCodec.apply read left to right, and StreamCodec.dispatch is how registry-dispatched values travel — ConsumeEffect.STREAM_CODEC, SlotDisplay.STREAM_CODEC and RecipeDisplay.STREAM_CODEC are built with it.

ByteBufCodecs is the primitive library underneath: SCREAMING_CASE constants for the fixed things, lowerCamel factories for the parameterised ones.

whatthe names
numbersByteBufCodecs.BOOL, ByteBufCodecs.BYTE, ByteBufCodecs.SHORT, ByteBufCodecs.UNSIGNED_SHORT, ByteBufCodecs.INT, ByteBufCodecs.VAR_INT, ByteBufCodecs.LONG, ByteBufCodecs.VAR_LONG, ByteBufCodecs.FLOAT, ByteBufCodecs.DOUBLE
bytes and textByteBufCodecs.BYTE_ARRAY, ByteBufCodecs.LONG_ARRAY, ByteBufCodecs.STRING_UTF8, ByteBufCodecs.byteArray, ByteBufCodecs.stringUtf8, ByteBufCodecs.PLAYER_NAME
tags and JSONByteBufCodecs.TAG, ByteBufCodecs.COMPOUND_TAG, ByteBufCodecs.TRUSTED_TAG, ByteBufCodecs.TRUSTED_COMPOUND_TAG, ByteBufCodecs.lenientJson
shapes and identitiesByteBufCodecs.VECTOR3F, ByteBufCodecs.QUATERNIONF, ByteBufCodecs.RGB_COLOR, ByteBufCodecs.CONTAINER_ID, ByteBufCodecs.GAME_PROFILE, ByteBufCodecs.GAME_PROFILE_PROPERTIES
structureByteBufCodecs.optional, ByteBufCodecs.collection, ByteBufCodecs.list, ByteBufCodecs.map, ByteBufCodecs.either, ByteBufCodecs.lengthPrefixed
registriesByteBufCodecs.idMapper, ByteBufCodecs.registry, ByteBufCodecs.holder, ByteBufCodecs.holderRegistry, ByteBufCodecs.holderSet

Two are worth naming for what they encode rather than for what they are. ByteBufCodecs.ROTATION_BYTE is one byte meaning 1/256 of a full turn — a little over a degree, with Mth.packDegrees and Mth.unpackDegrees doing the arithmetic — and ByteBufCodecs.OPTIONAL_VAR_INT spends zero for absent and value-plus-one otherwise.

The bridge to the disk-and-JSON codecs of Part II is ByteBufCodecs.fromCodec and its relatives, which run an ordinary Codec into a carrier format and put the result on the wire. The combinator underneath takes the ops as an argument and is format-agnostic, and the six NBT entry points either reach it with NbtOps or, the three that read registries, do its work themselves over a registry view of NbtOps, so a Codec on the wire almost always means an NBT tag. Beneath it, ByteBufCodecs.tagCodec takes an NbtAccounter supplier — which is exactly what trusted turns out to mean, below.

Almost always, because the combinator is public and exactly two packets call it with JsonOps and carry JSON strings instead: the server-list response, ClientboundStatusResponsePacket, and the login kick, ClientboundLoginDisconnectPacket. Both are read by a client that may speak another version — the server list’s ping, and a client the server has just refused — and both write against no registries at all. They are also the two a player is likeliest to have seen.

IdDispatchCodec is the class that makes a protocol out of a pile of codecs: a list of serialisers, a type-to-int map, a var-int written in front and a delegation behind it. IdDispatchCodec.DontDecorateException is the marker meaning rethrow me as I am, do not wrap me, and IdDispatchCodec.Builder.build refuses a duplicate PacketType outright, so registering one type twice in a protocol fails loudly rather than silently shadowing an id. Loudly, but not always early: the check runs when the template is bound, which for the four phases whose codecs are fixed — handshaking, status, login and configuration — is class-load, and for play is the first connection’s configuration-to-play switch (which buffer, and why play needs its own, below).

Which buffer, and why play needs its own

phase and directionwhat the codec is handedbound
status, serverboundthe raw ByteBuf — the identity function, no wrapper at allat class-load
handshaking, status clientbound, login, configurationFriendlyByteBufat class-load
play, both directionsRegistryFriendlyByteBuf — a FriendlyByteBuf with one more fieldper connection, at the switch into play

Three rows and no figure, because there is no order here to draw: the third row is not the second one wrapped again, it is a subclass of it, and every one of the three is built round the raw buffer.

FriendlyByteBuf is a ByteBuf decorator declaring a hundred and forty-six readers and writers, counting every overload and static twin separately, eighty-eight of which add a wire format the plain buffer knows nothing about — FriendlyByteBuf.readVarInt, FriendlyByteBuf.writeUtf, FriendlyByteBuf.readIdentifier, FriendlyByteBuf.writeResourceKey, FriendlyByteBuf.readNbt, FriendlyByteBuf.readEnumSet, FriendlyByteBuf.readBlockPos, FriendlyByteBuf.readWithCodec and so on. It holds the two length constants FriendlyByteBuf.MAX_STRING_LENGTH and FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH.

RegistryFriendlyByteBuf extends it and adds exactly one field, a RegistryAccess, behind RegistryFriendlyByteBuf.registryAccess. It exists because a numeric registry id means nothing on its own — enchantment number 3 is only an enchantment relative to the registry set the server sent during configuration (identifiers and registries). Every codec that reads the RegistryAccess needs one: ByteBufCodecs.registry, ByteBufCodecs.holderRegistry, ByteBufCodecs.holder, ByteBufCodecs.holderSet, ByteBufCodecs.fromCodecWithRegistries, ByteBufCodecs.fromCodecWithRegistriesTrusted and ByteBufCodecs.registryFriendlyLengthPrefixed (ByteBufCodecs.idMapper, over a fixed map such as the block states, needs none) — and therefore ItemStack.STREAM_CODEC, DataComponentPatch.STREAM_CODEC (data components), ComponentSerialization.STREAM_CODEC and HashedStack.STREAM_CODEC. The wrapper is a throwaway, not a pipeline object: ProtocolInfoBuilder applies the decorator — RegistryFriendlyByteBuf.decorator for play — through StreamCodec.mapStream, so a fresh view is constructed round the raw buffer on every single encode and every single decode.

When each of those bindings happens is the other half of the same fact. Handshaking, status, login and configuration are the four static phases: they bind their buffers eagerly at class-load, and the serverbound status protocol binds the identity function, so it is the one phase and direction that never wraps its bytes at all. Play — the fifth — binds per connection, at the configuration-to-play transition. On the client that really is the first moment a RegistryAccess exists; on the server the registries have been loaded since startup, and the rebind happens because the protocol changed rather than because the registries arrived.

Below all of it the actual encodings live in VarInt, VarLong, Utf8String and LpVec3, the low-precision vector behind Vec3.LP_STREAM_CODEC, mostly velocities. Two everyday values are worth naming because they are not special-cased: Identifier.STREAM_CODEC is a plain UTF-8 string under the ordinary 32,767-character cap — an identifier on the wire is text, never an interned number — and UUIDUtil.STREAM_CODEC is two longs.

Where a packet’s number comes from

ProtocolInfo is what a configured connection holds: ProtocolInfo.id (a ConnectionProtocol), ProtocolInfo.flow, ProtocolInfo.codec — the single phase-wide StreamCodec — and a nullable ProtocolInfo.bundlerInfo. It is built by ProtocolInfoBuilder, whose ProtocolInfoBuilder.addPacket and ProtocolInfoBuilder.withBundlePacket are the registration calls and whose ProtocolInfoBuilder.buildUnbound yields an UnboundProtocol or a SimpleUnboundProtocol — a protocol that knows everything except which buffer type to wrap the bytes in (and, for the one context protocol, its context). UnboundProtocol.bind supplies that. Underneath it, ProtocolCodecBuilder is the layer that talks to IdDispatchCodec.

The id is a registration index. ProtocolCodecBuilder.add appends to a list and IdDispatchCodec.Builder.build walks that list assigning 0, 1, 2 in call order, so a packet’s wire number is its position in the chain of ProtocolInfoBuilder.addPacket calls — ProtocolInfoBuilder.withBundlePacket’s delimiter counting, in the one protocol that has one — in GameProtocols, ConfigurationProtocols, LoginProtocols, StatusProtocols or HandshakeProtocols. ProtocolCodecBuilder.add also asserts that the type’s PacketFlow matches the protocol’s, so a clientbound type cannot be registered into a serverbound protocol.

Those numbers are readable from outside for one reason. ProtocolInfo.DetailsProvider and ProtocolInfo.Details let tooling enumerate a phase, and ProtocolInfo.Details.listPackets hands a ProtocolInfo.Details.PacketVisitor each PacketType with its network id. The tooling is the data generator: PacketReport walks every template and writes every packet’s id in every phase into a report — the only place in the project those numbers are written down.

There are four registration entry points, one per direction and per context-or-not: ProtocolInfoBuilder.serverboundProtocol, ProtocolInfoBuilder.clientboundProtocol, ProtocolInfoBuilder.contextServerboundProtocol and ProtocolInfoBuilder.contextClientboundProtocol. The context ones let a codec ask the connection a question, and exactly one protocol uses one: GameProtocols.SERVERBOUND_TEMPLATE, whose context is GameProtocols.Context and whose two questions are GameProtocols.Context.hasInfiniteMaterials and GameProtocols.Context.canUseCommandBlocks, the second deciding how long a command the server will read from a ServerboundCommandSuggestionPacket. The other eight templates — nine in all, one per direction per phase bar handshaking’s serverbound-only one — are a SimpleUnboundProtocol; CodecModifier is the hook a context-aware codec is installed through.

A bundle is two empty markers round ordinary packets

ClientboundBundlePacket has a PacketType and no wire id at all. ProtocolInfoBuilder.withBundlePacket registers only the delimiter, ClientboundBundleDelimiterPacket, serialised with StreamCodec.unit, and records a BundlerInfo beside the codec list, so a bundle on the wire is two empty markers with ordinary, individually numbered packets between them. PacketBundleUnpacker explodes an outgoing bundle into delimiter-packets-delimiter and PacketBundlePacker collects an incoming run back up. BundlePacket is the abstract class that holds the sub-packets and ClientboundBundlePacket its one subclass; BundlerInfo is the logic, split between BundlerInfo.unbundlePacket outgoing and its nested BundlerInfo.Bundler incoming, and BundlerInfo.BUNDLE_SIZE_LIMIT caps a bundle at 4,096 sub-packets. Only the clientbound play protocol declares a bundle at all.

What a bundle buys is atomicity against the client’s tick. ClientPacketListener.handleBundlePacket hops to the Render thread once for the whole bundle and then handles the sub-packets inline, so the client can never tick or render with half a bundle applied. There are only two senders, both in ServerEntity: ServerEntity.addPairing, which collects what ServerEntity.sendPairingData writes into a list and sends the result as one bundle, so an entity never appears mid-initialisation (what the client is told); and the motion-plus-power pair sent for a hurtling projectile.

What stops a hostile sender

The frame limit does most of the work and it is not the codec layer’s: a frame length is at most three var-int bytes, and Varint21FrameDecoder refuses a wider prefix or a zero length before any codec sees anything (the connection owns it, and it is in the table below because a reader chasing a limit should not have to know which layer imposed it). Above that sit two separate collection defences, and only the second is famous. ByteBufCodecs.readCount is the first — it compares the declared count against the codec’s own maximum and refuses outright, which is what makes the three-argument ByteBufCodecs.collection different from the two-argument one whose maximum is effectively unbounded. Behind it, ByteBufCodecs.MAX_INITIAL_COLLECTION_SIZE clamps the allocation to 65,536 entries whatever the count says, so even an accepted count cannot force a huge array up front. ByteBufCodecs.lengthPrefixed bounds the bytes instead of the count, handing the inner codec a slice it physically cannot read past.

Trusted is a real distinction in the codec library, and the mechanism is a read budget: ByteBufCodecs.fromCodecTrusted gives the NBT reader an unlimited heap quota where plain ByteBufCodecs.fromCodec gives it the default two mebibytes at depth 512. The rule for choosing between them is direction — the server wrote it, so the client may trust it — which is why the trusted half of each pair is clientbound only. Hence ByteBufCodecs.COMPOUND_TAG / ByteBufCodecs.TRUSTED_COMPOUND_TAG — the plain one carries CustomData and the predicates, and the trusted one has exactly one call site in the game, ClientboundBlockEntityDataPacket; ByteBufCodecs.TAG / ByteBufCodecs.TRUSTED_TAG, whose trusted form has none at all and is a pair only on paper; and ComponentSerialization.STREAM_CODEC / ComponentSerialization.TRUSTED_STREAM_CODEC, the live one — every clientbound chat packet uses the trusted variant, which is the codec this page’s own opening figure draws on a ClientboundSystemChatPacket’s Component field (text components).

Exactly one packet lets a client hand the server an arbitrary item, and it is fenced three ways. ServerboundSetCreativeModeSlotPacket uses ItemStack.OPTIONAL_UNTRUSTED_STREAM_CODEC, which differs from ItemStack.OPTIONAL_STREAM_CODEC only in using DataComponentPatch.DELIMITED_STREAM_CODEC: every component’s payload is length-prefixed, and ByteBufCodecs.lengthPrefixed hands the inner codec a slice and advances the outer reader past the whole region before delegating, so a component that lies about its own length cannot mis-frame the ones after it. That is containment, not recovery — nothing catches a component that throws, and one bad component still fails the whole packet. It is then wrapped in ItemStack.validatedStreamCodec, which re-encodes the decoded stack through ItemStack.CODEC against NullOps purely to collect its errors — the output is thrown away, and the point is to prove that the persistent codec would have accepted it. And the registration carries GameProtocols.HAS_INFINITE_MATERIALS, a CodecModifier that refuses the packet whenever its context says the connection is not in creative — a SkipPacketDecoderException one way and a SkipPacketEncoderException the other, landing on one side only because the client’s context answers GameProtocols.Context.hasInfiniteMaterials true unconditionally while the server’s context is ServerGamePacketListenerImpl, answering from the real player. The packet is refused in the decoder, before any handler exists to fool.

Serverbound defence is layered, and the three layers sit on three different packets. Full re-validation is the creative slot above. ServerboundCustomClickActionPacket takes the second: it builds its own, much tighter NbtAccounter and length-prefixes its payload, so a custom click’s tag is bounded rather than checked. And the third is the strongest, because there is nothing to check.

The ordinary container click is defended by carrying nothing to validate. ServerboundContainerClickPacket sends a HashedStack, which is an item, a count and a hash per component its patch adds (the removed ones by type alone), so on that packet no client-supplied component content crosses the wire at all. What the shape is and what the server does with the claim belong to containers and menus.

The limits in one table:

limitvaluewhere
frame length prefixthree var-int bytesVarint21FrameDecoder
compressed framejust under 2 MiB, all a three-byte prefix can countVarint21FrameDecoder
decompressed frame8 MiB, for every sender, and on receipt for the server onlyCompressionDecoder.MAXIMUM_UNCOMPRESSED_LENGTH
default string32,767 charsFriendlyByteBuf.MAX_STRING_LENGTH
component as string262,144FriendlyByteBuf.MAX_COMPONENT_STRING_LENGTH
player name16ByteBufCodecs.PLAYER_NAME
collection allocation65,536ByteBufCodecs.MAX_INITIAL_COLLECTION_SIZE
sub-packets in a bundle4,096BundlerInfo.BUNDLE_SIZE_LIMIT
slots in one click128ServerboundContainerClickPacket.MAX_SLOT_COUNT
var-int / var-long5 / 10 bytesVarInt.read, VarLong.read

Custom payloads, a seam in a fixed packet set

The packet set is code, fixed at compile time, and no data pack adds to it. The seam after login is CustomPacketPayload, with CustomPacketPayload.Type, CustomPacketPayload.createType and CustomPacketPayload.codec, carried by ClientboundCustomPayloadPacket and ServerboundCustomPayloadPacket. The route in for an id nobody registered is CustomPacketPayload.FallbackProvider, the factory the dispatch asks for one, with CustomPacketPayload.TypeAndCodec as the registration pair; BrandPayload is vanilla’s own use of the mechanism.

An unrecognised payload decodes to DiscardedPayload — a record of the identifier and nothing else. Its decoder checks the remaining length against a per-direction maximum and then skips every byte, and its encoder writes nothing. The payload is discarded, not held.

The login phase has its own parallel set — CustomQueryPayload, CustomQueryAnswerPayload and the discarding implementations of each — because the play-phase seam does not exist yet when a login is negotiated. Vanilla decodes every one of them to the discarding form and never sends a query, which is why any answer is a disconnect (protocol phases).

What a skippable packet actually costs

A chat line that fails to encode does not end your connection, and the reason is worth following because it is the codec layer deciding that an error is not a fault — as it also does in the creative-slot fence above and in the command-suggestion packet’s chat-only cap. SkipPacketException is a bare marker; SkipPacketEncoderException and SkipPacketDecoderException implement it and IdDispatchCodec.DontDecorateException. PacketEncoder turns a failure on a Packet.isSkippable packet into one. PacketDecoder drains the rest of the frame and rethrows. What keeps the connection alive is Connection.exceptionCaught, which logs the marker and returns (the connection).

The old shape is weaker only where it does its own counting. FriendlyByteBuf has no list or map reader, so a hand-written packet’s counted list goes through a ByteBufCodecs codec — ClientboundSetPlayerTeamPacket builds its player list with ByteBufCodecs.list — and gets the clamped allocation with it. A count the packet decodes itself gets no such cap: ClientboundSectionBlocksUpdatePacket sizes both of its arrays from the raw decoded count with nothing of its own bounding it.

One type, several encodings

Type and encoding are separate objects, and three packets prove it by having two encodings each — ServerboundCommandSuggestionPacket’s are the protocol context’s choice, above. ClientboundCustomPayloadPacket declares ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC and ClientboundCustomPayloadPacket.CONFIG_STREAM_CODEC; ClientboundShowDialogPacket declares ClientboundShowDialogPacket.STREAM_CODEC and ClientboundShowDialogPacket.CONTEXT_FREE_STREAM_CODEC. For the other two, the same PacketType is registered with the first in GameProtocols and with the second in ConfigurationProtocols — the same message, typed against the buffer each phase has, and for the dialog written differently, since only play has a RegistryAccess to write a registry reference against.

Which packets a phase registers, and in what order, is therefore the whole definition of that phase, so the next page is protocol phases, where nine codec tables — one per direction per phase, bar handshaking’s serverbound-only one — become the four languages a joining connection speaks in turn.

For a 1.21-era reader. Packet no longer knows how to write itself. There is no write method on the interface and no buffer constructor it is obliged to have. Serialisation is a STREAM_CODEC static field that the protocol description reads — which is why one packet class can have two of them, and why a packet class need not own one at all. FriendlyByteBuf.readList, readMap, readCollection and limitValue are gone: ByteBufCodecs.list, ByteBufCodecs.map and ByteBufCodecs.collection, which can take the cap as an argument, do their work.

Where to look

Three files in this order and the page’s argument is in front of you. Packet is an interface of four methods and takes a minute. GameProtocols is the chain — scroll it, and the claim that an id is a line’s position stops being a claim. ProtocolInfoBuilder is what the chain is calling, and ProtocolInfoBuilder.addPacket is the line that assigns the number without ever naming one.

Then the two layers underneath. StreamCodec for the combinators, and StreamCodec.composite in particular, because the fourteen arities are the whole reason a packet needs no write method. IdDispatchCodec for how a phase’s codecs become one codec, and for the duplicate check that fires at bind time rather than at registration.

ByteBufCodecs is a library rather than a read: open it when you want to know how one kind of value is written, and read ByteBufCodecs.readCount and ByteBufCodecs.lengthPrefixed whatever else you skip, since between them they are most of the defence. FriendlyByteBuf is the same for the old shape.

Two doors this page only points at: PacketReport, a short data generator and the only place a packet number is ever written down, and CustomPacketPayload, the seam a modded packet goes through after login.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Protocol phases

Verified against Minecraft 26.3 · Part IX · A login: from clicking a server in the list to standing in the world.

Click a server in the multiplayer list and one TCP connection opens, and over the next few seconds it speaks four different languages in turn: handshaking, login, configuration, play. There is a fifth, status, and a login never touches it — it is the phase the server list uses to ask a server how many players it has, and it is a dead end by design. Each of the five is a ConnectionProtocol, each has its own packet set, and each that hands over to another does so by a packet marked terminal — a flag on the packet class that makes the codec which decoded it tear itself out of the pipeline as it passes (the connection). All but handshaking have a listener at both ends; handshaking is serverbound only, so the client has nothing to listen with.

The surprise is not in any of that. It is that the ServerPlayer — the object, its save data, its position, the chunks under it — is prepared during configuration, by PrepareSpawnTask, and constructed only after the client has acknowledged that configuration is over. For the seconds in between, a server is holding chunk tickets for a player that does not exist, and by the time the object is built, the server’s side of the connection already encodes play.

The cast

classrolethread
ConnectionProtocolthe five phases — a bare enum of labels; the codec lives in a ProtocolInfo, the behaviour in a PacketListener—
Connectionone channel, and Connection.setupInboundProtocol / Connection.setupOutboundProtocol at every transitionNetty
ServerHandshakePacketListenerImplthe three-way switch and the version gateNetty
ServerLoginPacketListenerImplthe login state machine; its handlers set a volatile state and its ServerLoginPacketListenerImpl.tick acts on itNetty and the authenticator thread, ticked from Server
ServerConfigurationPacketListenerImplthe serial task queue, and the handler that finally builds the playermixed — see below
ClientHandshakePacketListenerImpl · ClientConfigurationPacketListenerImplthe client’s two halves: the session-service call, then the registries and tags it accumulates before constructing ClientPacketListenerNetty, the IO pool for the session call, and Render for most of the second
ServerCommonPacketListenerImpl · CommonListenerCookiewhat configuration and play share — the packets legal in both, and the state that survives a switch between themNetty and Server
PrepareSpawnTaskfinds a spawn, tickets its chunks, waits — and later, on request, spawns the playerServer, but Netty for its start straight after a code of conduct

The five phases

stateDiagram-v2
    [*] --> HANDSHAKING : TCP accept
    HANDSHAKING --> STATUS : ClientIntentionPacket, intent STATUS
    HANDSHAKING --> LOGIN : ClientIntentionPacket, intent LOGIN or TRANSFER
    STATUS --> [*] : pong, then the server hangs up
    LOGIN --> CONFIGURATION : ClientboundLoginFinishedPacket, ServerboundLoginAcknowledgedPacket
    CONFIGURATION --> PLAY : ClientboundFinishConfigurationPacket, ServerboundFinishConfigurationPacket
    PLAY --> CONFIGURATION : ClientboundStartConfigurationPacket, ServerboundConfigurationAcknowledgedPacket
    PLAY --> [*] : disconnect
    note right of HANDSHAKING : every packet named here is terminal, so the codec that decoded it is already gone

The five phases, each arrow between two of them carrying the packet — or the pair of them — that changes one. The arrow back out of play is the only one that goes up, which is what makes configuration the only phase entered from two others.

Seven packets in the game carry the terminal flag, and every one of them labels an arrow above. ClientIntentionPacket labels two, which is the striking one: the very first packet a connection ever sends already tears out the codec that decoded it. The remaining six are three pairs, and every pair enters or leaves configuration, the only phase entered from two others.

ConnectionProtocol is five constants — ConnectionProtocol.HANDSHAKING, ConnectionProtocol.STATUS, ConnectionProtocol.LOGIN, ConnectionProtocol.CONFIGURATION, ConnectionProtocol.PLAY — each carrying only a string ConnectionProtocol.id. There is no number, no packet table and no lookup by id. What a phase is lives in two other places: the packet set, bound as a ProtocolInfo, and the listener that handles it.

phaseserverbound listenerclientbound listenerProtocolInfo
ConnectionProtocol.HANDSHAKINGServerHandshakePacketListenerImpl, or MemoryServerHandshakePacketListenerImpl in singleplayernone — there is no clientbound handshake protocolHandshakeProtocols.SERVERBOUND, one packet
ConnectionProtocol.STATUSServerStatusPacketListenerImplan anonymous ClientStatusPacketListener inside ServerStatusPingerStatusProtocols.SERVERBOUND binds a raw buffer; StatusProtocols.CLIENTBOUND a FriendlyByteBuf
ConnectionProtocol.LOGINServerLoginPacketListenerImplClientHandshakePacketListenerImplLoginProtocols.SERVERBOUND / LoginProtocols.CLIENTBOUND
ConnectionProtocol.CONFIGURATIONServerConfigurationPacketListenerImplClientConfigurationPacketListenerImplConfigurationProtocols.SERVERBOUND / ConfigurationProtocols.CLIENTBOUND
ConnectionProtocol.PLAYServerGamePacketListenerImplClientPacketListenernot pre-bound — GameProtocols.SERVERBOUND_TEMPLATE and GameProtocols.CLIENTBOUND_TEMPLATE are bound per connection

The bindings in the last column are the per-phase codec tables that packets and stream codecs builds, and the swap itself is the pipeline surgery the connection performs; this page is what the swaps are for. The one thing about the column that is a fact about the phases is that play is the only row not pre-bound: its codecs write registry ids, so its two templates are bound per connection at the configuration-to-play switch, which is the only transition in a connection’s life that ties the codecs to a set of registries as well as changing which packets are legal.

Two listeners sit under the phases. ServerCommonPacketListenerImpl is the shared base of the server’s configuration and play listeners and holds nearly everything legal in both (each subclass handles the client information itself) — keep-alive, latency, custom payloads, resource-pack responses and the flush suspension — with ClientCommonPacketListenerImpl its client counterpart; that inheritance is why the common packets in reference/packets.md belong to no single phase. And the state that crosses a phase change is a CommonListenerCookie: on the server a small record of profile, latency, client information and a transferred flag; on the client a much larger one carrying registries, feature flags, cookies, chat state and the server brand.

Handshake

The handshake is one packet and a three-way switch. ClientIntentionPacket carries the protocol version, the address the client dialled and a ClientIntent, and ServerHandshakePacketListenerImpl.handleIntention branches on it. ClientIntent.STATUS installs the status listener, or disconnects at once if the server does not reply to status. ClientIntent.TRANSFER disconnects if the server does not accept transfers, and otherwise joins ClientIntent.LOGIN in ServerHandshakePacketListenerImpl.beginLogin, which compares the client’s protocol version against this build’s and refuses a mismatch, and the two refusals are not symmetric: a version below protocol 754 is told outdated_client, and anything else that does not match is told incompatible. All three of the handshake’s refusals on the login path — these two and the transfer refusal — go out as ClientboundLoginDisconnectPacket under the login clientbound protocol, so the client can render a reason: ServerHandshakePacketListenerImpl.beginLogin installs it for every login before its check, and the transfer refusal installs it purely to send one. The status refusal is the exception and the rudest: the status clientbound protocol is installed before the branch, and then the connection is dropped with no packet at all.

The client does not wait for any of that. Whichever of its three entry points opened the connection — ConnectScreen for a listed or direct server, Minecraft for the integrated server’s memory channel, RealmsConnect — it sends ServerboundHelloPacket immediately after the intention packet; there is no round trip between them. The profile id the hello carries is decoded and then never read — the server mints or fetches identity for itself. All of this runs on the Netty thread, and the intention packet, being terminal, has already torn out the codec that decoded it.

Status, the phase nobody logs in through

ConnectionProtocol.STATUS is two packets each way and a deliberate dead end. ServerStatusPacketListenerImpl answers exactly one ServerboundStatusRequestPacket — a second one disconnects the caller — and answers ServerboundPingRequestPacket with ClientboundPongResponsePacket and then hangs up. A status connection is expected to be thrown away, which is why ServerStatusPinger opens one per server in the list and why Connection.initiateServerboundStatusConnection exists as a separate entry point.

Login

The login phase is a state machine on one field, and the states are the ones the server moves through between accepting a socket and having a profile it believes.

stateDiagram-v2
    [*] --> HELLO
    HELLO --> KEY : online mode, ClientboundHelloPacket
    HELLO --> VERIFYING : the singleplayer profile
    HELLO --> VERIFYING : offline mode
    KEY --> AUTHENTICATING : ServerboundKeyPacket
    AUTHENTICATING --> VERIFYING : the authenticator thread returns
    VERIFYING --> WAITING_FOR_DUPE_DISCONNECT : tick, a duplicate is online
    VERIFYING --> PROTOCOL_SWITCHING : tick, ClientboundLoginFinishedPacket
    WAITING_FOR_DUPE_DISCONNECT --> PROTOCOL_SWITCHING : tick, the old one is gone
    PROTOCOL_SWITCHING --> ACCEPTED : ServerboundLoginAcknowledgedPacket
    note right of VERIFYING : the three tick edges are the server thread's, one the authenticator thread's, the rest Netty's

Seven of the eight ServerLoginPacketListenerImpl.State constants, and the three edges the server thread owns. Look at where the forks are: the machine branches once out of the hello, on how — or whether — this login is authenticated, and then again on whether the profile is already playing.

No handler on ServerLoginPacketListenerImpl ever schedules itself onto another thread (there is not one PacketUtils.ensureRunningOnSameThread call in the class), and yet the login is finished on the server thread, which is why its ServerLoginPacketListenerImpl.state field is volatile: the packet handlers run on the Netty thread and set the state, as the authenticator thread below does, and ServerLoginPacketListenerImpl.tick — reached from MinecraftServer.tickConnection through ServerConnectionListener.tick and Connection.tick, the tick a game thread gives a connection (the connection) — reads it on the server thread and does the login. The tick is also where ServerLoginPacketListenerImpl.MAX_TICKS_BEFORE_LOGIN, six hundred ticks, is enforced: a client that has not reached the end of the phase in thirty seconds is disconnected for a slow login — a transition out of every state above but the last, and the one the diagram cannot draw.

The diagram names three states the paragraphs below do not: ServerLoginPacketListenerImpl.State.AUTHENTICATING is the wait on the session service, ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING is the gap between the server sending ClientboundLoginFinishedPacket and the client acknowledging it, and ServerLoginPacketListenerImpl.State.ACCEPTED is where the machine stops, because configuration has its own listener.

Three branches out of the hello. If the name matches the singleplayer profile, verification starts at once with no encryption. If the server uses authentication and this is not a memory connection, the state becomes ServerLoginPacketListenerImpl.State.KEY and ClientboundHelloPacket carries the server’s RSA public key and a four-byte challenge. Otherwise — offline mode — the profile is minted from the name by UUIDUtil.createOfflineProfile and nothing is encrypted.

Both sides authenticate, and the client goes first.

sequenceDiagram
    box transparent the client
    participant CHPL as ClientHandshake<br/>PacketListenerImpl
    end
    box transparent the server
    participant SLPL as ServerLoginPacket<br/>ListenerImpl
    end
    participant Sess as the session service

    SLPL->>CHPL: ClientboundHelloPacket, an RSA public key and a challenge
    CHPL->>CHPL: a new AES secret, and a digest over id, secret and key
    CHPL->>Sess: joinServer, on the client's IO pool, before the key packet
    CHPL->>SLPL: ServerboundKeyPacket, RSA-encrypted, ciphers attached to the send
    SLPL->>SLPL: the challenge checked, the secret recovered, then<br/>Connection.setEncryptionKey
    SLPL->>Sess: hasJoinedServer, on a fresh User Authenticator thread
    Sess-->>SLPL: the authenticated profile, state VERIFYING
    Note over SLPL: the next server tick runs bans, whitelist, compression, duplicates

Both ends authenticating against the same external service, the client first. Read it downwards: the server has installed both ciphers by the fifth line, two lines before it has any idea who this is.

Read the diagram for the order, because the order is the point: the server installs both ciphers while handling the key packet, before its own session-service call has begun. An unauthenticated connection is already encrypted. The client’s asymmetry is the mirror of it — it calls the session service before ServerboundKeyPacket goes anywhere and sends it from that callback, attaching its own ciphers to the send, so the key packet itself travels in the clear and nothing after it does.

Authentication is a plain thread with two fallbacks. It calls the session service and, on success, reports login activity, stores the profile and flips the state to ServerLoginPacketListenerImpl.State.VERIFYING. On failure it disconnects — unless the server is a singleplayer host, in which case both a null result and an unreachable authentication service fall back to an offline profile. That is how a LAN world admits a guest whose account cannot be checked. A login over a real socket against an authenticating server starts exactly one such thread; an offline-mode, memory or singleplayer-profile login starts none.

The tick does the real login. ServerLoginPacketListenerImpl.verifyLoginAndFinishConnectionSetup runs on the server thread: PlayerList.canPlayerLogin for bans, whitelist and capacity; the compression switch; and PlayerList.disconnectAllPlayersWithProfile for a duplicate login, after which the machine waits in ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT until the old player has left the player list.

Login ends with a terminal packet in each direction, and the two sides install their codecs in mirror order. ClientboundLoginFinishedPacket then ServerboundLoginAcknowledgedPacket — but the server installs outbound configuration when the acknowledgement arrives, in ServerLoginPacketListenerImpl.handleLoginAcknowledgement, whereas the client installs inbound configuration before sending it and outbound immediately after. Both packets are terminal, so the codecs tear themselves out as they pass (the connection). The client then volunteers two packets straight away: its own BrandPayload and ServerboundClientInformationPacket, which carries the client’s language, view distance and skin-part settings — and is therefore the packet a server with a code of conduct reads before choosing which translation of it to send.

What disconnects a login: a version mismatch, a refused transfer, a ban, a whitelist the player is not on, a full server, a failed session check on a non-singleplayer host, any ServerboundCustomQueryAnswerPacket or cookie answer, a malformed or out-of-order packet, or six hundred ticks. Either answer disconnects because vanilla never asks: ServerLoginPacketListenerImpl.State.NEGOTIATING is declared and never assigned, ClientboundCustomQueryPacket decodes every payload as DiscardedQueryPayload, and nothing sends a cookie request.

Configuration

Configuration is a queue. The order below is fixed, each task finishes before the next begins, and the last two exist only to get a player into a world.

flowchart TD
    S["up to three packets, sent before any task runs"]:::server
    S --> Q
    subgraph Q["the serial queue"]
      direction TB
      R["SynchronizeRegistriesTask"]:::server --> C["ServerCodeOfConduct<br/>ConfigurationTask"]:::server
      C --> P["ServerResourcePack<br/>ConfigurationTask"]:::server
      P --> PS["PrepareSpawnTask: a spawn, and a ticket"]:::server
      PS --> J["JoinWorldTask: the terminal packet"]:::server
    end
    Q -- "ClientboundFinishConfigurationPacket" --> CL["the client rebuilds its registries"]:::client
    CL -- "ServerboundFinishConfigurationPacket" --> F["the play protocol, the gate again, the player"]:::server

The phase as a queue of five tasks, strictly one at a time — the middle two only if the server has a code of conduct or a resource pack — with the packets that precede the queue and the round trip that ends it. The two arrows at the foot are the phase’s oddity: between them the server is holding a ticket on chunks for a player that does not exist yet.

SynchronizeRegistriesTask is the reason configuration exists, and the queue around it is strictly serial. ServerConfigurationPacketListenerImpl.startConfiguration sends up to three things outside the queue — the server’s BrandPayload, ClientboundServerLinksPacket if there are links, and ClientboundUpdateEnabledFeaturesPacket — then queues the registry task, a code-of-conduct task if the server has one (ServerCodeOfConductConfigurationTask, whose two packets are ClientboundCodeOfConductPacket and the client’s ServerboundAcceptCodeOfConductPacket) and a resource-pack task if it has one, before ServerConfigurationPacketListenerImpl.returnToWorld appends PrepareSpawnTask and JoinWorldTask and starts the first. Each ConfigurationTask finishes before the next begins: ServerConfigurationPacketListenerImpl.finishCurrentTask rejects a completion naming the wrong task type, and an exception out of any task disconnects the client.

Registry and tag sync is why this phase exists, and what crosses belongs to identifiers and registries — the known-pack negotiation, the all-or-nothing comparison, the empty payloads for elements the client can re-read from its own jar, and the two layers the client rebuilds. What belongs to the phase is the order and the count: one ClientboundSelectKnownPacks and its reply, then one ClientboundRegistryDataPacket per registry, then a single ClientboundUpdateTagsPacket that covers the networkable registries and the static ones too — the surprise, since the data packets are dynamic-only, and the reason a tag has to land before any play traffic does (tags). None of it is applied as it arrives: RegistryDataCollector accumulates the whole exchange and only resolves it at ClientConfigurationPacketListenerImpl.handleConfigurationFinished, the handler that goes on to construct the ClientPacketListener.

The seam is not where it looks. Four of the server’s configuration handlers hop to the Server thread — ServerConfigurationPacketListenerImpl.handleSelectKnownPacks, ServerConfigurationPacketListenerImpl.handleConfigurationFinished and, from the common base, the resource-pack response and ServerCommonPacketListenerImpl.handleCustomClickAction — while the client-information and code-of-conduct handlers, like the keep-alive and ping handlers they inherit, stay on the Netty thread. That last one is load-bearing: accepting a code of conduct finishes a task, which starts the next task on the Netty thread, and the next task may be PrepareSpawnTask, whose first act is to read a player save file and resolve a spawn position. ServerConfigurationPacketListenerImpl.tick runs each tick to drive the current task and keep the spawn chunks loaded.

PrepareSpawnTask is two states, and only the second builds the player, and only when asked. Its PrepareSpawnTask.Preparing state waits for the spawn position the task’s start resolved (a search, for a player with none saved), tickets the chunks and then reports not finished from ConfigurationTask.tick until they arrive; PrepareSpawnTask.Ready reports finished and does nothing but re-arm the ticket until it is asked to spawn. How it finds the spawn, why the ticket has to be re-armed every tick, and what the two whole-file reads cost belong to players and sessions. JoinWorldTask then sends the terminal packet, and only when the client’s ServerboundFinishConfigurationPacket arrives does ServerConfigurationPacketListenerImpl.handleConfigurationFinished swap the outbound protocol to play, refuse a duplicate of the player, run the admission gate a second time (admission is a Component or nothing — a ban or a full server can arrive in the seconds a configuration takes) and call PrepareSpawnTask.spawnPlayer, which is where the ServerPlayer is finally constructed and handed to PlayerList.placeNewPlayer. Everything between the join task and that handler is a server holding a ticket on chunks for a player that does not exist — which is the phase’s own oddity, and the reason this page opens on it.

What disconnects a configuration: a task that throws on start or on tick, a completion for the wrong task, a declined resource pack the server requires, a cookie answer, a duplicate at the finish or a refusal at the admission gate’s second run (a ban, the whitelist, a full server), and an exception while placing the player. Notably not a deadline on the queue. Login has one — six hundred ticks — and configuration’s queue has none: ServerConfigurationPacketListenerImpl.tick counts nothing, calling the common base’s keep-alive and then the current task, so what kills a stuck task is a client that stops answering keep-alives rather than one that takes too long. Only the round trip that ends the phase has a clock, for anyone but the singleplayer host: the terminal packet closes the listener, which stops sending keep-alives and allows fifteen seconds for the answer (the connection). A slow configuration can be the server still finding chunks for the player, and the queue lets it be.

Play, and the way back

The play protocol is installed in two halves on each side: the server swaps outbound early in the finish handler and inbound inside PlayerList.placeNewPlayer; the client, in one handler, swaps inbound, sends the finish packet, then swaps outbound. That is why the server’s side already encodes play while still nominally in the configuration listener, and a refusal at the finish goes out as a play packet. Chat session keys are not part of any of this: they are negotiated in play, after the client learns the server’s mode from ClientboundLoginPacket (chat and signing).

A reconfigure does not re-run configuration. ServerGamePacketListenerImpl.switchToConfig removes the player from the world, sends ClientboundStartConfigurationPacket and swaps outbound; the client’s ClientPacketListener.handleConfigurationStart flushes its chat queue, stashes the chat state and carries the registry access, feature flags, brand and server links forward in a rebuilt cookie, then answers ServerboundConfigurationAcknowledgedPacket; and ServerGamePacketListenerImpl.handleConfigurationAcknowledged installs a configuration listener without calling ServerConfigurationPacketListenerImpl.startConfiguration. No registries, tags, feature flags or brand are re-sent, and none need to be. The player parks in configuration with an empty queue until ServerConfigurationPacketListenerImpl.returnToWorld re-queues the spawn and join tasks. Both directions are reachable in vanilla only from DebugConfigCommand, which the game registers only in a dedicated server’s command set, and only when started with its debug system properties.

Cookies, transfers and the chat reset are proxy hooks

A ServerboundCookieResponsePacket arriving at any server listener is an unexpected query and a disconnect, and nothing in the tree constructs ClientboundStoreCookiePacket or ClientboundCookieRequestPacket. Of the transfer machinery only ClientboundTransferPacket has a vanilla caller, /transfer: it sends the client to another server, which sees a ClientIntent.TRANSFER handshake and a transferred flag in its cookie, and ClientCommonPacketListenerImpl.shouldHandleMessage keeps accepting store-cookie and transfer packets while a transfer is in flight, which is what lets a proxy’s trailing state land. ClientboundResetChatPacket is registered and handled and never sent. The rest is fully implemented on the client and unused by the server.

For a 1.21-era reader. The assumption that “packet handlers run on the game thread” is exactly backwards for the first two phases: the handshake and login listeners’ handlers run to completion on the Netty thread, and the first PacketUtils.ensureRunningOnSameThread in a connection’s life is in configuration.

Where to look

Read the four listeners, the server’s three in the order a login meets them, and the whole page is in them. ServerHandshakePacketListenerImpl is little more than one switch. ServerLoginPacketListenerImpl is the one to spend time on: read ServerLoginPacketListenerImpl.tick first, then the handlers below it, and the volatile state field above both will explain itself. ServerConfigurationPacketListenerImpl is the task queue, and ServerConfigurationPacketListenerImpl.handleConfigurationFinished is where the player is finally built. ClientHandshakePacketListenerImpl is the other side of the encryption exchange and worth reading against the server’s.

Then the tasks themselves — ConfigurationTask is a three-method interface, and PrepareSpawnTask and JoinWorldTask are the two that matter here; SynchronizeRegistriesTask is the reason the phase exists and is Part II’s subject as much as this page’s.

Two doors this page only points at: ConnectionProtocol, which takes ten seconds to read and is worth it for how little is in it, and Crypt, the key, digest, cipher and salt helpers the login and chat signing share, in one utility class.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What the client is told

Verified against Minecraft 26.3 · Part IX · a creeper walks into view: everything the server decides to say, in order, and everything it decides not to.

A creeper three chunks away steps across a section boundary, and at the next tick’s tracking sweep, unless the player moves first, the server decides a player may know about it. One bundled packet goes out, and the position in that packet is not where the creeper is. It is where the creeper’s tracker last said it was — up to an update interval out of date. That is not a bug being tolerated. Every relative move the viewer is sent after it is a difference from the last position it was told, not a position, and the viewer adds each difference to its own copy of that base — so a viewer that started from anywhere else would be wrong by the gap until the next absolute position reset it. So the server sends the base rather than the truth, and the rest of this page is that trade made over and over: which chunks a player is sent and how fast, which entities they are told about, and what counts as a change worth a packet.

The cast

classwhat it decidesthread
ChunkMapwhich players can see which chunks and which entities — it keeps the tracked entities and moves each player’s chunk viewServer
ChunkMap.TrackedEntityone entity’s audience, ChunkMap.TrackedEntity.seenBy, and the range test that fills itServer
ServerEntitythe change detector: the baseline, the interval gate, the packet shapeServer
ChunkTrackingViewwhich chunks a player is owed, and what a movement turns intoServer
PlayerChunkSenderhow many chunks leave for one player this tickServer
ChunkHolderthe per-chunk block-change batch, one flush a tickServer
EntityTypethe per-type tracking range, update interval and delta exclusionstatic, from the builder
ChunkBatchSizeCalculatorthe client’s answer — how many chunks a tick it wantsclient Netty thread, and it never hops

One entity’s tick, and the gates it does not pass

flowchart TD
    TICK["ChunkMap.tick walks every tracked entity"]:::server --> SEC{"changed section"}
    SEC -- yes --> UP["re-test every player in the level"]:::server
    UP --> G1{"gate 1: range, hook, chunk"}
    G1 -- "all three hold" --> IN["joins the audience: the introduction bundle"]:::server
    G1 -- "any one false" --> OUT["leaves it: ClientboundRemoveEntitiesPacket"]:::server
    SEC -- no --> G2{"gate 2: section, flag, or range"}
    IN --> G2
    OUT --> G2
    G2 -- "any one is enough" --> SC["ServerEntity.sendChanges"]:::server
    G2 -- "none of the three" --> MUTE["nothing, and the counter does not advance"]:::server
    SC --> FREE["three feeds that skip gate 3"]:::server
    SC --> G3{"gate 3: interval, flag, or dirty data"}
    G3 -- "any one is enough" --> D["zero to five packets"]:::server
    G3 -- "none of the three" --> WAIT["wait for a later call"]:::server

The three gates, each a three-term test: gate 1 is a conjunction and gates 2 and 3 are disjunctions. That is the asymmetry to carry away — one wrong answer at gate 1 keeps a viewer from seeing the entity at all, and one right answer at either of the others lets it speak, if it has anything to say.

The figure is the page. Three gates stand between an entity moving and a player hearing about it, and each is a three-term test: gate 1 is a conjunction, where all three must hold, and gates 2 and 3 are disjunctions, where any one term is enough. What comes out the bottom is not a description of the entity but a description of the difference between the entity and what this viewer was last told. The sections below are one per gate, then one per feed that does not go through them at all.

Gate 1: who is allowed to see it

ChunkMap.tick re-tests visibility only when something moved between sections — the entity’s own section against the tracker’s last, and, for players who changed section, every entity against that player. ChunkMap.move, which runs on every move a player makes, re-tests every entity against that player at once, and moves its chunk view when it crosses a boundary (tickets and loading). The test itself is three conjuncts.

The distance is horizontal. ChunkMap.TrackedEntity.updatePlayer compares squared x/z distance against the smaller of the entity’s effective range and the player’s view distance in blocks. Y is ignored entirely — an entity directly above you, at any height, is in range.

The range is the maximum over the entity and everything riding it. ChunkMap.TrackedEntity.getEffectiveRange takes the largest EntityType.clientTrackingRange among the entity and all its indirect passengers, then scales it by MinecraftServer.getScaledTrackingDistance, which both server classes override: DedicatedServer applies the entity-broadcast-range-percentage property, and IntegratedServer applies the client’s own Entity Distance video option. In singleplayer a graphics slider decides how far away mobs are tracked.

And the chunk must already be there. Entity.broadcastToPlayer looks like a per-entity hiding hook and is not: it defaults to true and is overridden exactly once, by ServerPlayer, to keep a spectator out of every non-spectator’s view — and out of a fellow spectator’s while it is looking through something else’s eyes. And ChunkMap.isChunkTracked is false while the chunk is still queued in PlayerChunkSender, so an entity is never sent before the ground it stands on — the ordering is guaranteed rather than hoped for.

One case never reaches the test at all: ChunkMap.TrackedEntity.updatePlayer returns immediately for the player’s own entity. You never track yourself. That is why ServerEntity.Synchronizer has a self-directed verb, ServerEntity.Synchronizer.sendToTrackingPlayersAndSelf, beside ServerEntity.Synchronizer.sendToTrackingPlayers and ServerEntity.Synchronizer.sendToTrackingPlayersFiltered, and why damage, knockback and synched data must all reach for it.

The introduction is one bundle

What gate 1 opening costs, in packets, is one:

sequenceDiagram
    participant CM as ChunkMap
    participant CMTE as ChunkMap.<br/>TrackedEntity
    participant SE as ServerEntity
    participant CPL as ClientPacketListener

    CM->>CMTE: the creeper changed section, so re-test every player
    CMTE->>CMTE: all three conjuncts hold, so the audience gains this connection
    CMTE->>SE: addPairing
    SE->>SE: sendPairingData fills one list, in a fixed order
    SE->>CPL: one ClientboundBundlePacket
    Note over SE,CPL: add-entity at the tracker baseline, then synched data, attributes, equipment, ClientboundSetPassengersPacket, ClientboundSetEntityLinkPacket
    CPL->>CPL: the bundle is applied inside one task, so nothing renders half-built

Gate 1 opening, and what the new viewer is sent the moment it does: one bundle, in one fixed order, applied on the client inside a single task. The position in it is the tracker’s baseline, not the creeper’s.

ServerEntity.sendPairingData produces the list and ServerEntity.addPairing sends it as a single ClientboundBundlePacket, so the creeper can never be seen half-initialised. The synched values are not read fresh: they come from ServerEntity.trackedDataValues, the cached snapshot of the entity’s non-default values, refreshed whenever dirty data is flushed (synched entity data). Only the syncable attributes go (attributes), and equipment, passengers and leash links go only if there are any.

That bundle is where the page’s claim first bites: what it carries is the tracker’s baseline, not the creeper. It has three exceptions and two refusals. Paintings, item frames and leash knots build their own ClientboundAddEntityPacket from their real position, bypassing ServerEntity’s baseline — every concrete BlockAttachedEntity but the cushion, which takes the default, and a block-attached entity has no moves to add up. EnderDragonPart refuses outright, and ChunkMap never asks it. Marker throws outright if anyone asks — which nobody does, because its tracking range is zero and ChunkMap never tracks it. Everything else reads its position from ServerEntity.getPositionBase and its rotations and motion from the last-sent fields, however old they are.

Gate 2: whether the detector is called at all

ChunkMap.tick calls ServerEntity.sendChanges when any of three things is true: the entity changed section, Entity.needsSync is set, or its chunk is in entity-ticking range. Two public fields on Entity do the forcing. Entity.needsSync appears at this gate, again at gate 3 and again in the velocity decision — three of the cascade’s terms are the same flag — and is set by being pushed, by being loaded from disk, and by over twenty other classes for their own reasons. Entity.syncPosition is subtler: it re-phases the call counter to the next interval boundary, so a bounced entity syncs at once rather than up to an interval late, unless it is a living entity other than a shulker, whose SteppedInterpolationTracker takes the flag first and records the bounce as a step.

The interval itself is per type and the page leans on it below, so it is worth a number: EntityType.updateInterval defaults to three ticks, thirty types give it another number and eight never let it open at all — and the direction is the opposite of the obvious one. The three Display entities get one, because they move by interpolation and a missed tick shows; an arrow, an experience orb, a dropped item and a falling block get twenty, because a ballistic path is something the client can extrapolate exactly. The thing moving fastest is told least often.

The two counters inside ServerEntity are deliberately out of step. ServerEntity.tickCount advances on every call, gate 3 open or shut, so ServerEntity.FORCED_POS_UPDATE_PERIOD counts calls. ServerEntity.teleportDelay advances only inside gate 3, so ServerEntity.FORCED_TELEPORT_PERIOD counts gated calls — the forced absolute sync is rarer than the forced position packet by however long the interval gate stays shut.

This is also where a distant entity goes quiet, and it has nothing to say: a mob outside entity-ticking range is not ticked either, so it does not move itself, and the gate spares a call that would usually send nothing. A push from outside sets Entity.needsSync and opens it again.

Gate 3, and the position it chooses

The first three rows are the four terms of the source’s one conjunction, in its order, Entity.getRequiresPrecisePosition first. A relative packet must pass all four, and only then is the delta’s size tested, which is why the fourth row’s absolute sync, unlike the others, resets neither the teleport count nor the ground flag. A passenger, the last row, is branched off before any of them.

conditionresult
Entity.getRequiresPrecisePositionabsolute sync
ServerEntity.teleportDelay past ServerEntity.FORCED_TELEPORT_PERIOD — four hundred gated calls, so about 1,200 ticks for an entity tracked every tick on the three-tick default interval, fewer when data or a push opens the gateabsolute sync
the entity just dismounted, or its ground flag differs from the one the conjunction last recorded — so a landing or a step off a ledge that a gated call sees costs a full packetabsolute sync
delta beyond what a short can hold — about eight blocks — or, for a dropped item against a wall, floor or ceiling, one the encoding would roundabsolute sync
none of those, and the squared position delta is below ServerEntity.TOLERANCE_LEVEL_POSITION with rotation within ServerEntity.TOLERANCE_LEVEL_ROTATIONnothing sent
none of those, and something movedClientboundMoveEntityPacket.Pos, .Rot or .PosRot
the call count is a multiple of ServerEntity.FORCED_POS_UPDATE_PERIODa position packet even if nothing moved — but only on a call that got through gate 3
the entity is a passengerrotation only — the base is silently re-set, and the first call to open gate 3 after it dismounts forces an absolute sync

The gate covers more than the position. The same test that opens the table above also opens the synched-data flush, which is why shearing a sheep runs that sheep’s position branch in the same call (synched entity data has the data channel’s half); the ItemFrame branch — one section down, under the feeds that skip gate 3 — is the only path to that flush which skips the interval test, and ServerEntity.handleMinecartPosRot reaches it from inside the gate rather than around it.

Rotations are single bytes, so one unit is a little over a degree, and the base — a VecDeltaCodec, which is the object holding the last-sent position the whole page’s opening rests on — advances only when a position is sent, on both sides alike: that is what keeps the two sides’ arithmetic identical, and the two places that re-set the server’s without one, a passenger’s and a new-behaviour minecart’s, are the two whose next position goes whole. An arrow never takes a partial path — AbstractArrow is excluded from the position-only and rotation-only branches, so every open gate sends it a full position-and-rotation packet. A minecart on the new movement behaviour skips the table altogether: ServerEntity.handleMinecartPosRot diverts it into ClientboundMoveMinecartPacket, which carries the minecart’s own list of steps.

That first term is set by three classes in the whole game. Entity.setRequiresPrecisePosition is asked for by a happy ghast on its still timeout — a large ridable platform, whose rounding error a player has to stand on — and by a falling block or lit TNT while it is touching a block.

Velocity is the third decision and a separate channel. When the entity wants deltas — it is in the EntityType.trackDeltas set, Entity.needsSync is set, or it is a LivingEntity currently elytra-flying — ServerEntity compares the current delta movement against ServerEntity.lastSentMovement and sends ClientboundSetEntityMotionPacket, bundled with ClientboundProjectilePowerPacket for a hurtling projectile. EntityType.trackDeltas is a third tracking parameter, switched off on the builder by EntityType.Builder.dontTrackDeltas: players, llama spit, the wither, bats, item frames, leash knots, paintings, end crystals, evoker fangs and cushions are out, everything else is in.

Three feeds that skip gate 3, and one that skips ServerEntity

ServerEntity.sendChanges opens with Entity.updateDataBeforeSync, the hook LivingEntity overrides to reconcile its own effects — so a mob effect that expired this tick can dirty the synched container and open gate 3 in the same call that goes on to read it (synched entity data). Below it, three feeds ignore gate 3, a smaller exemption than it sounds: all three live inside ServerEntity.sendChanges, so a mob that fails gate 2 gets none of them either. Entity.syncVelocity sends a motion packet to the trackers, and to the entity itself when it is a player, which is why a player feels knockback at once, its own client moving it; a creeper, which a client only interpolates, shows its knockback with its next position packet. A changed passenger list is diffed on every call and goes out as a ClientboundSetPassengersPacket to every tracker, a riding player included, though ServerPlayer.startRiding or ServerPlayer.removeVehicle has already told that player directly. An ItemFrame flushes its synched data every tenth call, and if it holds a map with saved data it walks every player in the level — not its trackers — to push map updates.

Equipment changes do not pass through ServerEntity. ClientboundSetEquipmentPacket comes from LivingEntity.handleEquipmentChanges, and it goes to the trackers without the self-directed variant, so a player is never sent their own equipment. A straight main-hand to off-hand swap does not even get that far: LivingEntity.handleHandSwap compresses it into a one-byte entity event. And damage crosses without a number — ClientboundDamageEventPacket carries the source, not the amount, and the health bar moves because of a separate synched value (damage and death).

Chunks arrive on a loop the client paces

Chunk visibility is the same kind of decision one level up: made per player rather than per entity, and paced by a loop whose set point the client supplies.

Which chunks enter and leave

ChunkMap.applyChunkTrackingView diffs the player’s old and new ChunkTrackingView and turns the difference into a queue of pending sends and a run of ClientboundForgetLevelChunkPackets. Which chunks are in that view, what shape the view is — neither a disc nor a square, because ChunkTrackingView.isWithinDistance shrinks each axis before it squares — and what makes a chunk eligible to leave at all belong to tickets and loading. The one class the iteration needs and no other page names is ChunkTrackingView.Positioned, which walks one chunk beyond the view distance for exactly that reason.

The rate the client asks for

Then, once per tick, PlayerChunkSender.sendNextChunks runs per player from the phase MinecraftServer.tickChildren reaches after every level has ticked (the server tick):

  • it stops if too many batches are unacknowledged — PlayerChunkSender.maxUnacknowledgedBatches starts at one and is raised to PlayerChunkSender.MAX_UNACKNOWLEDGED_BATCHES, which is ten, on the first reply. One batch in flight until the client answers, ten after it: the first batch of a login is a hard round-trip barrier, and it is why the world arrives in a pause and then a flood;
  • it accumulates PlayerChunkSender.batchQuota by the client’s desired rate and stops if it is below one;
  • it takes that many chunks nearest first from PlayerChunkSender.pendingChunks — or, on a memory connection, or whenever fewer are pending than the budget allows, all of them at once;
  • and it brackets them with ClientboundChunkBatchStartPacket and ClientboundChunkBatchFinishedPacket.

The client times the bracket in ChunkBatchSizeCalculator, clamps the sample against the running average by ChunkBatchSizeCalculator.CLAMP_COEFFICIENT, folds it into a weighted mean against ChunkBatchSizeCalculator.MAX_OLD_SAMPLES_WEIGHT, and reports a rate back in ServerboundChunkBatchReceivedPacket. The server clamps that between PlayerChunkSender.MIN_CHUNKS_PER_TICK and PlayerChunkSender.MAX_CHUNKS_PER_TICK and uses it as next tick’s budget. It is a closed control loop, and it is the client that sets the set point.

Seven milliseconds of client time per tick is what ChunkBatchSizeCalculator.getDesiredChunksPerTick divides by its running estimate of nanoseconds per chunk. It starts pessimistic, at two milliseconds a chunk — three and a half chunks a tick against PlayerChunkSender.START_CHUNKS_PER_TICK, nine. That opening figure is never sent as it stands: the client folds the first real batch into the average before it answers, so the first number the server hears is already half measurement.

What is being measured is narrower than it looks. ClientPacketListener.handleChunkBatchStart and ClientPacketListener.handleChunkBatchFinished are two of the nine handlers on the client’s play listener that never hop off the network thread, so the loop is timing the batch’s arrival and decode on Netty, not mesh building (the nine are listed in threads).

What a chunk packet carries

A chunk packet — ClientboundLevelChunkWithLightPacket — carries the client-facing heightmaps, every section’s counts, paletted block states and biomes, a block-entity entry per block entity holding BlockEntity.getUpdateTag rather than its save data, and the light layers. See chunk anatomy and lighting.

Block changes: one flush a tick, two audiences

ServerLevel.sendBlockUpdated sends nothing. It marks a section dirty on the ChunkHolder — in ChunkHolder.changedBlocksPerSection, one short set per section — and adds the holder to a set on ServerChunkCache, unless the chunk is loaded but not ticking, in which case nothing is recorded and the change is never broadcast to anyone. ServerChunkCache.broadcastChangedChunks drains that set through ChunkHolder.broadcastChanges once per level tick, inside ServerChunkCache.tickChunks (on a frozen tick too, though never in a debug world), early in the tick and before entities move (the level tick) — so one broadcast carries the block changes made so far this tick and the rest since the last broadcast: the previous tick’s block events, entities and block entities, and players’ actions.

Light goes first, and to a narrower audience — the border of each player’s sent region rather than everyone tracking the chunk, through ChunkMap.isChunkOnTrackedBorder. Why the border player needs it and the player in the middle does not belongs to lighting, and it is the one feed of this flush whose audience is narrower than the chunk’s trackers.

Then blocks, to everyone tracking the chunk. Exactly one changed block in a section becomes a ClientboundBlockUpdatePacket, two or more become a ClientboundSectionBlocksUpdatePacket, and every change within the tick collapses into at most one packet per section in this flush. What collapses them is that the set holds positions, not values: ChunkHolder.broadcastChanges reads the level again when it builds the packet, so a position written five times in one tick is sent once, carrying the value it ended on and none of the four it passed through. A whole redstone cascade can therefore run, settle and be broadcast as a single state per position (signal and dust).

And block entities alongside the blocks, not after them. The check runs inside the same per-section loop, immediately after that section’s own update packet — interleaved, not a third pass. ChunkHolder.broadcastBlockEntityIfNeeded tests whether the state has a block entity at all and delegates to ChunkHolder.broadcastBlockEntity, one of the two places in the game that ask BlockEntity.getUpdatePacket (the other resends the blocks under a player the server thinks is standing on air). Only overriding types produce one here, and the fallback is not “it rides the chunk packet instead” — which is why chest contents are invisible until the chest is opened. What the two sync hooks default to, and which types override which, belong to block entities.

The rest of the block-shaped traffic

It is small: ClientboundBlockEventPacket from the deferred event set (pistons and block events), ClientboundBlockDestructionPacket for other players’ mining progress, ClientboundChunksBiomesPacket when biomes are re-sent, and ClientboundBlockChangedAckPacket, sent at most once per connection per tick and on any tick where a loaded client sent a block action, a use-on or a use — including an unsequenced abort or change of dig direction, which produces an ack of zero and settles nothing. The rules that receipt obeys — and the ordering rule that makes a receipt without a verdict sufficient — belong to prediction and acknowledgement.

The level’s own feeds

Entities and chunks are the two big feeds. The level itself has several small ones, all bypassing the change detectors entirely — most on ServerLevel, though time comes from MinecraftServer and the view distances from PlayerList:

  • Time, once a second. MinecraftServer.forceGameTimeSynchronization runs every twentieth tick, and the packet it broadcasts carries the overworld’s game time and an empty clock map: clock state travels only when a clock is changed, a player joins, respawns or changes dimension, or the rule that advances time changes (what crosses the wire).
  • Weather, on change. ServerLevel.advanceWeatherCycle broadcasts rain- and thunder-level changes and the start/stop pair as ClientboundGameEventPackets, and PlayerList re-sends the same set to a joining player.
  • Sounds, level events, particles and entity events, each with its own helper and its own audience. Two radii are worth naming because they are not the tracking distance: another player’s mining progress reaches everyone within thirty-two blocks except the miner, and a block event reaches sixty-four.
  • View distances, as ClientboundSetChunkCacheRadiusPacket and ClientboundSetSimulationDistancePacket — the two integers that are the client’s entire knowledge of the ticket system, and the only feed here the receiver acts on structurally rather than by storing a value: the first one rebuilds the client’s chunk storage array.
  • The debug feed, which the next section’s table names as an exception more than once, and a narrow one. ServerLevel owns a set of per-subscriber debug synchronizers that push neighbour updates, POI state, chunk sends and entity tracking to a client that has opted in — sixteen subscriptions in all, registered in DebugSubscriptions, covering brains, goal selectors and paths among them. Most of the next section has no subscription at all: not the seed, not the loot tables, not the game rules, not a scheduled tick.

What the client is never told

what never crossesthe server-side ownerwhere it is explained
all AI — targets, goals, brains, paths — bar a few synched flags and target ids (the aggressive flag, a guardian’s beam target, the wither’s head targets), except through the debug channelMob.goalSelector, Mob.targetSelector, Mob.getTarget, Brain, Mob.navigationAI, goals and brains
scheduled block and fluid ticks — the client’s equivalents are emptyServerLevel.blockTicks, ServerLevel.fluidTicksscheduled ticks
points of interest, except through the debug channelChunkMap.poiManagerpoints of interest
the ticket graph — the client gets a radius and a simulation distance, as two integersTicketStorage, DistanceManager, ChunkHolder.ticketLevel, FullChunkStatustickets and loading
worldgen, and the world seed — ClientLevel gets only a biome zoom seed; structures only through the debug channelChunkGenerator, RandomState, ServerLevel.structureManager, StructureStartthe generation pipeline
the worldgen heightmaps, and any block-entity field outside BlockEntity.getUpdateTag, bar an operator’s query, a command block’s screen, a creative player’s pick-block and an opened container’s slotsBlockEntityblock entities
non-syncable attributes, loot tables and loot seeds, the natural spawn state, raids and the dragon fight beyond their boss bars, except through the debug channelAttributeMap, LootTable, NaturalSpawnerattributes
game rules — the full set reaches the client only on request, and only for a player with the command permission; the few that change how the client behaves are pushedGameRuleslevel data and rules
the creeper’s fuse length and its swell counter — of its three synched values, none is the counterCreepersynched entity data
everything out of range: entities past tracking range, chunks past the view, and every other level on the serverChunkMap, MinecraftServer.levels—

Choosing what the client may be wrong about

Every gate above is a decision not to say something, and the server can afford them because the receiver is not passive. ClientLevel never admits a chunk is missing, runs its own light engine and its own clock, ticks entities it does not own — Creeper.tick runs locally, swell counter and all — and guesses at block placements through a sequence-numbered ledger. That half of the story is Part X’s: the client level for what the receiver fakes and simulates, prediction and acknowledgement for what it guesses, and authority for which side is allowed to move what. The client also applies a whole burst of these packets once per frame, before that frame’s ticks, so at a high frame rate it takes the server’s updates far more often than it ticks — the two-loops figure in anatomy is the shape, the client loop the detail.

Which is the design constraint under all of it: because the client will happily simulate in the absence of data, the server’s job is not to keep the client correct — it is to choose what the client is allowed to be wrong about.

For a 1.21-era reader. Routine movement no longer travels as ClientboundTeleportEntityPacket: that packet survives, but ServerEntity never touches it, and an absolute position is ClientboundEntityPositionSyncPacket. And in 26.3 the passenger list goes to every tracker, where 1.21 skipped a player whose own riding had changed.

Where to look

ServerEntity.sendChanges is the page. It is one long method, and gate 3, the head-yaw branch and the velocity branch are all visible in it at once, with the relative-or-absolute conjunction one call away in ServerEntity.createMovePacket; read it before anything else here, and read it twice.

Then the two callers that decide who hears it: ChunkMap.tick for the sweep that runs it, and ChunkMap.TrackedEntity.updatePlayer for gate 1, which is short and the whole visibility policy. ServerEntity.addPairing and ServerEntity.sendPairingData are the introduction bundle, in the order it is built.

For the other two feeds: PlayerChunkSender.sendNextChunks is the control loop from the server’s end and ChunkBatchSizeCalculator is the client’s half of it — open them together or neither. ChunkHolder.broadcastChanges is the block flush, and the line in it that re-reads the level is why a redstone cascade costs one state per position.

One door this page only points at: VecDeltaCodec, the base itself and the reason the whole policy works.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Chat and signing

Verified against Minecraft 26.3 · Part IX · A player presses T, types a line and hits enter: the message is signed on the way out, taken apart on the way in, and verified again by every client that holds the sender’s session.

A player presses T, types hey and hits enter. Before the line leaves the machine, ChatScreen.normalizeChatMessage has squeezed the whitespace and cut it to 256 characters, and ClientPacketListener.sendChat has taken a timestamp, a random salt and the signatures of the messages the player saw among the last twenty signed ones it was sent, and signed all of it with a key Mojang issued to that account. The server pulls the packet apart on the Netty thread, hands the cryptography to the Server thread, filters it, decorates it, broadcasts it — and every receiving client that holds the sender’s session verifies the signature again before drawing a character. Every one of those steps can say no, and no does not mean the same thing twice. Forge a signature and you stay connected: you get a red line and your chain dies, so everything else you say this session fails too. Miscount which messages you have seen — a number nothing in the game shows you — and the server closes the connection mid-sentence. The bookkeeping is defended harder than the cryptography is, and that is the right way round.

The cast

classwhat it decidesthread
ServerGamePacketListenerImplthe order the checks run in, and which failure closes the connectionNetty for the window, the characters and the chat-visibility refusal, Server for everything after
LastSeenMessagesValidatorwhether the client’s twenty-slot acknowledgement still matches the server’s mirrorNetty, and Server for the pending count — inside a lock on itself
SignedMessageChain.Decoderwhether this message continues the sender’s chain — and whether the chain survives the answerServer
PlayerChatMessagewhat a signature covers: the chain link, the content, the timestamp, the salt, the windowwherever a message is built
MessageSignatureCachewhich of those signatures travel as a small index instead of 256 bytesboth sides, one 128-slot cache each
PlayerList · OutgoingChatMessagewho gets a copy, and whether it goes as a signed message or a disguised oneServer
SignedMessageValidator.KeyBasedwhether the receiving client believes the sender said thisRender
ChatTrustLevelsecure, modified or not secure — the tag drawn beside the lineRender

A message is not the text you see

Two packets carry all of this, and the page names them once here: a line leaves its author in a ServerboundChatPacket and reaches everybody else in a ClientboundPlayerChatPacket, which is the one packet in the game whose contents a recipient checks a player’s signature over before drawing.

A message on the wire is a PlayerChatMessage: a SignedMessageLink saying where in the sender’s chain it sits, a MessageSignature, a SignedMessageBody of exactly four fields, and — optionally — a Component to display instead of the signed string. That Component is Part II’s subject (text components); all this page needs from it is that it is a different object from the signed text and that the signature does not cover it. Vanilla’s decorator never produces one: MinecraftServer.getChatDecorator is hard-coded to ChatDecorator.PLAIN, so PlayerChatMessage.withUnsignedContent always finds the decorated copy equal to the original and drops it — that method keeps a copy only when it differs from the signed text. Vanilla still reaches it by another road: MessageArgument.resolveChatMessage, behind the message-shaped commands, hands it the resolved text, which differs exactly when a selector expanded. Type a selector into a whisper and the recipient sees a string the signature does not cover — the security half of a mechanism the Commands section below picks up.

One line, typed and delivered

sequenceDiagram
    box transparent the sender's client
    participant CScr as ChatScreen
    participant CPL as ClientPacketListener
    end
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant PL as PlayerList
    end
    box transparent every recipient's client
    participant RCPL as ClientPacketListener
    participant CLis as ChatListener
    end

    CScr->>CScr: normalizeChatMessage squeezes the whitespace, cuts to 256
    CScr->>CPL: sendChat
    CPL->>CPL: a timestamp, a salt, the last-seen window, then the signature
    CPL->>SGPL: ServerboundChatPacket
    SGPL->>SGPL: Netty thread, apply the last-seen update, check the characters
    rect rgba(0, 0, 0, 0.04)
        Note over SGPL,PL: the rest of the server's work, all of it on the Server thread
        SGPL->>SGPL: SignedMessageChain.<br/>Decoder.unpack, the signature check
        SGPL->>SGPL: the filter starts, the decoration is immediate, a FutureChain joins them
        SGPL->>PL: broadcastChatMessage, bound to ChatType.CHAT
    end
    PL->>RCPL: ClientboundPlayerChatPacket, cached last-seen signatures sent as ids
    RCPL->>RCPL: the global index, then the cache ids, then the signature
    RCPL->>CLis: handlePlayerChatMessage: the delay queue, then trust level and blocklist
    CLis->>RCPL: markMessageAsProcessed
    RCPL->>SGPL: ServerboundChatAckPacket, once the offset passes 64

One line’s whole journey, three machines in three boxes: the two ClientPacketListener lanes are the sender’s and every recipient’s, and the same signature is checked once in the middle box and again in the right-hand one. The shaded band is the server’s work on the Server thread, which ends in the arrow that leaves it.

Five things in that picture are worth naming before the checks are.

The client signs the conversation, not just the sentence. The window it signs is the window it now treats as acknowledged, so the signature binds the context the sender had in front of them — which is what makes a report show what a message was a reply to.

The hop is deliberate. ServerGamePacketListenerImpl.handleChat never calls the usual same-thread guard (the connection has what that guard normally does): the window and the character check run on the Netty thread, and only then does ServerGamePacketListenerImpl.tryHandleChat post the rest to the server. That posted task, and the FutureChain continuation that joins the text filter to it, drain with every other queued server task — so a slow filter service delays delivery by however many ticks it takes (the server tick).

The filter is an HTTP service, and usually there is none. TextFilter is the per-player interface every chat line — and every signed command message, book page, sign line and anvil name — passes through, and MinecraftServer.createTextFilterForPlayer hands out TextFilter.DUMMY, which passes everything, unless the server is a dedicated one whose text-filtering-config property names an endpoint. ServerTextFilter.createFromConfig reads that property at startup (starting a server) and builds either a LegacyTextFilter or a PlayerSafetyServiceTextFilter depending on the configured version, each a pool of Chat-Filter-Worker threads posting JSON to that endpoint. What comes back is a FilteredText — the raw string and a FilterMask of which characters were objected to — so the filter is never destructive here and never binary: the mask travels with the message and is applied per recipient, later. Filterable is the same idea frozen into saved data, a raw value beside an optional filtered one.

Decoration is not sequenced after filtering. The handler starts the filter future, decorates immediately and synchronously, and only then registers the continuation that joins the two. A decorator never sees filtered text.

Broadcast is per recipient, and decided in three places. PlayerList.broadcastChatMessage logs the line — marked Not Secure by PlayerList.verifyChatTrusted if it has no signature or has expired — and then asks the sender’s ServerPlayer.shouldFilterMessageTo of each player in turn. ServerPlayer drops the copy unless that player’s setting is ChatVisiblity.FULL; OutgoingChatMessage.Player applies the filter mask to a filtered copy and skips one that was filtered away entirely, and PlayerList tells the sender once if any was. A message whose sender is Util.NIL_UUID is a system message and leaves as an unsigned, unreportable ClientboundDisguisedChatPacket instead.

Three ways to say no, and one way not to ask

Everything above happens to a message that survives. Five of the checks run before the server thread ever sees the packet, and where a refusal lands decides how much of the session it takes with it.

flowchart TD
    P["ServerboundChatPacket"]:::netty --> N{"the window, the characters, chat visibility"}:::netty
    N -- "the window, or a character" --> XC["the connection dies"]
    N -- "chat turned off" --> XM["the message dies, and the next one may still land"]
    N -- "all five pass" --> S{"SignedMessageChain.<br/>Decoder.unpack"}:::server
    S -- "no signature, or key expired" --> XM
    S -- "out of order, or signature invalid" --> XB["the chain dies, and every later message this session with it"]
    S -- accepted --> OK["filter, decorate, broadcast"]:::server

The five checks on the Netty thread, in green, and the Server thread’s work, in blue, drawn so that the arrows meet at the three things a refusal can kill. The middle terminal is reached from both, which is the picture’s point: where a check runs says nothing about what it costs.

Those three endings are the whole vocabulary of failure here, and every check in the next section lands on exactly one of them.

The message dies alone: it is dropped, the sender usually gets a red system line explaining why, and the next thing they send is judged on its own merits. The chain dies for the session: SignedMessageChain clears the link it was going to advance, and from then on no unpack can succeed — the chain broken error if the message is otherwise well-formed, and a missing-key or expired-key error before that if it is not. Only a new session key, announced with ServerboundChatSessionUpdatePacket, or a new connection restores it. The connection dies immediately — through the same funnel as any other fault (the connection) — and the player is back at the multiplayer list.

And there is a fourth way, which is why the flowchart starts where it does. A client can refuse before the server is ever asked. ChatAbilities is a client-side permission set the server has no part in, stripped independently by game options, launcher policy and the account profile, and it decides whether this client will send or accept anything at all (permissions). Nothing below applies to a message that was never sent, and a player in that state sees no error from the server because there was no exchange to fail. ChatVisiblity is the sliver of the same idea the server is told about, and it has three values, not two — ChatVisiblity.HIDDEN still lets action-bar text through.

Every check, and what it costs

The first fifteen rows are the server treating the client as the adversary. The last three are the client treating the server as one — the same design mirrored, because a server can lie about who said what at least as easily as a client can.

The first nine rows are in the order a chat line meets them, and the first five run on the Netty thread, before the message is handed to the server at all: the three window checks inside ServerGamePacketListenerImpl.unpackAndApplyLastSeen, then the character check and the chat-visibility refusal inside ServerGamePacketListenerImpl.tryHandleChat. Four of those five close the connection and the fifth does not, which is the split the flowchart above draws; rows six to nine happen inside the task ServerGamePacketListenerImpl.tryHandleChat posts to the server, and the rate check in the continuation that broadcasts. The rest are other paths: a command, each recipient’s copy, a new session key, and the receiving client.

the checkwhat it catcheswhat dies
LastSeenMessagesValidator.applyOffset, from a chat packet or a bare ServerboundChatAckPacketa client advancing its window past messages the server has not sent itconnection
LastSeenMessagesValidator.applyUpdate, the acknowledged bitsa bit set longer than twenty, one naming a slot the server does not hold, or one un-acknowledging a slot already acknowledgedconnection
LastSeenMessages.Update.verifyChecksumthe two sides holding different signatures in slots whose bits agree — a desync the crypto would otherwise report as a bad signatureconnection, unless the client sent LastSeenMessages.Update.IGNORE_CHECKSUM
ServerGamePacketListenerImpl.isChatMessageIllegal, over StringUtil.isAllowedChatCharactersection signs and control characters — formatting injected into everyone else’s chatconnection
ServerPlayer.getChatVisibility, non-commands onlya player who turned chat off and sent a line anywaymessage, with a red chat.disabled.options back to the sender
SignedMessageChain.Decoder.unpack, no signature presentan unsigned message once a chat session exists — unconditionally, whatever MinecraftServer.enforceSecureProfile says: that flag decides what the decoder used before a session exists does with an unsigned message, and separately gates the unsigned-command refusal six rows downmessage
the same, ProfilePublicKey.Data.hasExpireda session key past its expiry still being used to signmessage
the same, a timestamp before the last accepted onea replayed or reordered message from this senderchain
the same, PlayerChatMessage.verifycontent, timestamp, salt or window that do not match the signature — a forgery, or a proxy editing text in flightchain
ServerGamePacketListenerImpl.collectSignedArguments, an unknown argument namea client signing arguments of a command the server’s own parse does not havechain, broken explicitly
the same, a signable argument with no signaturesignatures stripped from some arguments of a signed commandmessage — the chain is left intact
ServerGamePacketListenerImpl.performUnsignedChatCommanda signable command sent down the plain command packet with its signatures removedmessage, and only when MinecraftServer.enforceSecureProfile is on
ServerGamePacketListenerImpl.detectRateSpam, a TickThrottler per playerflooding: each message costs 20, one point decays per tick, and the ceiling is twenty per configured second — chat-spam-threshold-seconds, ten by default, so 200 points, or about ten messages sent faster than the decayconnection, except for operators, and on an integrated server for anyone, its threshold being zero
ServerGamePacketListenerImpl.sendPlayerChatMessage, via LastSeenMessagesValidator.trackedMessagesCounta client that is sent signed messages and never acknowledges themconnection, past 4,096 pending
ServerGamePacketListenerImpl.handleChatSessionUpdatea key that expires earlier than the one it replaces, or one RemoteChatSession.Data.validate cannot trace to Mojang’s services keyconnection
ClientPacketListener.handlePlayerChat, the global indexa server dropping, duplicating or reordering messages beneath the player, which would falsify any report drawn from the logconnection
MessageSignature.Packed.unpack against the client’s cachea server naming a cached signature the client has never heldconnection
SignedMessageValidator.KeyBased — expired key, failed signature, or a link that is not SignedMessageLink.isDescendantOf the lasta server inventing lines in another player’s namechain, latched: the validator never returns to valid

Two rows want a sentence more. The checksum is the only check in the table a client may decline: LastSeenMessages.Update.verifyChecksum passes anything when the byte is zero, and a real checksum that computes to zero is bumped to one so it can never be mistaken for the opt-out — though the vanilla client never opts out, because LastSeenMessagesTracker.generateAndApplyUpdate always computes one. And chain broken is a latch on both sides: the server’s SignedMessageChain and the receiving client’s SignedMessageValidator.KeyBased both refuse everything afterwards, so one bad signature costs a sender their voice until a new key or a new connection, not one line.

Why losing the window is worse than losing the signature

The asymmetry looks backwards until you notice what the window is for. The last-seen list — twenty LastSeenTrackedEntry slots on the tracker — is signed in full but sent as a bitset over a window the server mirrors, and passed on to other clients as MessageSignature.Packed entries, each an index into a cache both ends keep in step where that cache holds the signature. If the server’s mirror ever disagrees with the client’s window, the server reconstructs a different SignedMessageBody, and every signature after that fails for a reason no cryptographic error message can explain. There is no recovery from inside: the state is shared, and half of it is wrong.

So the game ends the connection at the first sign of that divergence, and LastSeenMessagesValidator is written to be suspicious. It rejects an acknowledgement of a slot it does not hold, an un-acknowledgement of a slot it already acknowledged, a negative offset, an offset larger than the number of tracked messages outside the window it has sent, and a bit set wider than the window; a checksum mismatch on top of all that says the client and server must have desynced in as many words.

A bad signature is the opposite kind of problem: local, provable and attributable. One sender is misbehaving, everyone else’s conversation is intact, and the proportionate answer is to stop trusting that sender — not to end a session for every other player in the room.

What keeps the two caches identical is that neither end chooses what goes in. MessageSignatureCache.push takes a message’s body and its signature, builds a queue of the last-seen signatures with that signature behind them, and writes them into 128 slots newest-first, shuffling back any displaced entry the new batch did not already contain. It is called in exactly two places, and they are the two ends of one connection: the server as it sends a signed message to that player, and that player’s client as it handles the packet. Same input, same method, same order — which is why an index means the same thing at both ends, and why nothing can put them back in step once they are not.

Sixty-four messages is the acknowledgement offset a client may accumulate before ClientPacketListener.markMessageAsProcessed sends a bare ServerboundChatAckPacket unprompted. That packet exists so that a player who only listens never reaches the server’s 4,096 pending messages and gets dropped for saying nothing all evening.

What the signature covers

PlayerChatMessage.updateSignature feeds the signer a version constant, then the link — sender id, session id, index — then the body: the salt, the timestamp in seconds, the length of the content, the content bytes, the count of last-seen signatures and each one’s raw bytes. Not fed to it: the decorated Component, the FilterMask, the ChatType.Bound that supplies the someone said wrapper — a ChatType is a data-pack registry entry and ChatTypeDecoration is the translation key and argument list that turns a message into so-and-so said this, so the phrasing around a line is data and the line is not — and the global index the receiving client checks. A server is free to change any of those. The signature is over what the player typed and what they had seen when they typed it, and nothing else.

On the receiving client all of that lands in ChatListener, the class the figure at the top of this page ends on. ChatListener.handlePlayerChatMessage records a signed message in the client’s window whether or not it is drawn (a refused one as an empty slot, never acknowledged) and puts the line, with that bookkeeping, in ChatListener.delayedMessageQueue rather than on the screen when the chatDelay option is set — a queue its own tick drains one line at a time, which is why the option is a client-side stagger and not a server one.

That gap is what ChatTrustLevel exists to expose, and it tests for it crudely on purpose. ChatTrustLevel.evaluate calls a message modified the moment the rendered string does not contain the signed string — the limb that catches a server rewriting what someone said. Only if that passes does it look at style, and only inside the unsigned copy — which vanilla sends only for a command message whose selector expanded, and that copy has already failed the contains test. A message with no signature at all, or one older than seven minutes, is not secure instead. The tag is normally all that happens; with Options.onlyShowSecureChat on, a not-secure message is discarded rather than drawn — that test runs first, and Minecraft.isBlocked and Minecraft.isFriendOnlyRestricted can swallow what survives it.

The session key is signed one level up. ProfilePublicKey.Data carries an expiry, the public key and a signature over the profile id, the expiry in milliseconds and the encoded key, checked against Mojang’s services key. The receiving client allows ProfilePublicKey.EXPIRY_GRACE_PERIOD — eight hours — that the signing chain on the server does not.

The private half lives nowhere on disk. LocalChatSession holds the pair for as long as a session lasts, and AccountProfileKeyPairManager writes the key file only when SharedConstants.IS_RUNNING_IN_IDE and deletes it otherwise, so a shipped client re-fetches from the account service every launch — and only if ClientboundLoginPacket.onlineMode said the server was in online mode, which is the packet that opens the play phase (protocol phases).

Commands: one signature per argument

ClientPacketListener.sendCommand parses the command locally and builds a SignableCommand. If nothing in it needs signing it sends ServerboundChatCommandPacket, which carries the string and nothing else. If something does, it sends ServerboundChatCommandSignedPacket with ArgumentSignatures: one signature per argument, each consuming its own chain index, all of them sharing one timestamp, salt and window. Signable means the argument type implements SignedArgument, and in 26.3 exactly one type does — MessageArgument, behind the message-shaped commands, and the enumeration of which commands register it belongs to brigadier and commands. MessageArgument is also the one route by which chat text has its selectors expanded, which is how a command message comes to carry unsigned content at all: the expansion itself, its permission and its parse belong to text components. What matters here is only that the expanded string is the one the recipient reads and the signature does not cover it.

The server re-parses its own copy and looks each signature up by argument name, which is where two rows of the table above come from: a name its parse does not have breaks the chain outright, while a signable argument the client left unsigned only fails the command. Both sides cap the shape of the packet at ArgumentSignatures.MAX_ARGUMENT_COUNT — eight — and ArgumentSignatures.MAX_ARGUMENT_NAME_LENGTH, sixteen. Commands run against their own TickThrottler, separate from chat’s and built the same way from command-spam-threshold-seconds, which also defaults to ten — so a player has two independent budgets and can spend both. A command message sent down the plain command packet, where secure profiles are not enforced, is broadcast as a ClientboundDisguisedChatPacket: chat-type decorated, unsigned, unreportable.

Questions players ask

What makes the “Not Secure” tag appear? No signature, or a timestamp more than seven minutes old by the receiving client’s clock. The server calls the same message expired after five, and logs it as Not Secure there too. Two machines whose clocks are a few minutes apart will flag perfectly honest messages, and the server says so in its log.

Why does a custom font not flag every line on my server? Because the style test only looks inside the unsigned copy, and vanilla’s decorator never produces one. For ordinary chat the font check is therefore dead, and it comes alive only on a server that decorates. A player’s own lines on an integrated server skip both tests and are secure by definition.

Why can I report some lines and not others? LoggedChatMessage.canReport needs a signature from the player being reported, and system messages, disguised command output and anything a broken chain swallowed carry none. What a report uploads is the signed material — index, session id, timestamp, salt, the last-seen signatures and the signed content — so it can be re-verified independently, with ChatReportContextBuilder.collectAllContext walking the last-seen links backwards for the conversation around it.

Does dying clear a broken chain? No, and nothing short of a new key or a new connection does. The chain decoder lives on ServerGamePacketListenerImpl, and a respawn builds a new ServerPlayer while keeping the same listener — PlayerList.respawn copies the connection across and ServerPlayer.restoreFrom copies the chat session with it — so the broken link survives death and the other respawn that replaces the player object, leaving the End. Within one connection only ServerGamePacketListenerImpl.handleChatSessionUpdate, given a different key, installs a fresh decoder.

And if somebody’s session key fails validation? Which side notices decides the cost. On the server it closes the connection. On another client, ClientPacketListener.initializeChatSession calls PlayerInfo.clearChatSession, and that player’s lines are treated as unsigned — the signature stripped by SignedMessageValidator.ACCEPT_UNSIGNED and the line tagged not secure, or refused by SignedMessageValidator.REJECT_ALL where secure profiles are enforced.

Where to look

Follow one message, in the order it travels. ClientPacketListener.sendChat is where a line becomes a signed object, and LastSeenMessagesTracker.generateAndApplyUpdate beside it is the window being committed to. ServerGamePacketListenerImpl.handleChat is the receiving end, and it is short: the two methods it calls, ServerGamePacketListenerImpl.unpackAndApplyLastSeen and ServerGamePacketListenerImpl.tryHandleChat, are the five Netty-thread checks in the table, in order. Then PlayerList.broadcastChatMessage for the per-recipient fan-out and ClientPacketListener.handlePlayerChat for the far end doing it all again in reverse.

Two classes are the argument rather than the route. LastSeenMessagesValidator is a short file of suspicion and is worth reading in full, because every rejection in it is a connection close. SignedMessageChain is the other, and the one line in SignedMessageChain.Decoder.setChainBroken that drops the next link is what chain broken means.

Three doors this page only points at: PlayerChatMessage.updateSignature, the exact byte order the signature covers; ChatTrustLevel.evaluate, the crude contains test behind the tag; and ChatReportContextBuilder.collectAllContext, which walks the last-seen links backwards and is the reason the window is signed at all.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

X · The client

Verified against Minecraft 26.3 · Part X · One thread, one loop, and everything else in the part answering the same question about itself: when in that loop does this happen?

A player already knows this part by its symptoms: the stutter where the world moves on without you, the block that appears and then disappears, the inventory that waits for the next tick to open. Every one of them is the same arrangement seen from a different angle. Everything in this part that changes the client’s world happens on one thread, and nothing in the client’s simulation is driven by a scheduler or a timer callback; despite the name printed in every stack trace there is no separate render thread, and the thread called Render thread is the same one that ticks the world, applies packets, handles your keyboard, decides what a screen looks like and asks the GPU to draw it. So the client has no way to do two of those at once, and no way to be interrupted into doing one of them late. Everything that looks like the client falling behind is one thread deciding what to spend a frame on.

The shape of the part

Part X is a hub and its spokes, and the spokes are cadences rather than stages: with two exceptions, noted below, nothing here hands off to anything. The client loop is the hub because it is the one page that says when anything on the client runs, and every other page in the part answers the same question about itself: when in that loop does this happen? The figure has seven spokes for twelve pages, because the four GUI pages answer it together and so do the two about sound.

flowchart LR
    LOOP["1 · The client loop — the hub"]
    LEVEL["2 · The client level"]
    PRED["3 · Prediction and acknowledgement"]
    INPUT["4 · Input and keybinds"]
    OPT["5 · Options"]
    GUI["6-9 · The GUI stack"]
    SND["10-11 · Sound"]
    DBG["12 · Debugging the running game"]
    LOOP -- "per tick, light per frame" --> LEVEL
    LOOP -- "per action, a window per try" --> PRED
    LOOP -- "per SDL event" --> INPUT
    LOOP -- "per save" --> OPT
    LOOP -- "per frame" --> GUI
    LOOP -- "per event, then off-thread" --> SND
    LOOP -- "per tick, and a packet on change" --> DBG

The hub and its spokes, numbered to the watch order, a range being pages watched together and the hub drawn sideways because it is not a pipeline. The labels on the arrows are cadences, not steps: each spoke answers the hub’s question — when in the loop does this happen?

The one genuine pipeline inside the part is the GUI stack, and it is a pipeline whose stages interleave rather than queue: a screen records itself into a tree, and the text in it is measured and baked while it is being recorded, because the tree cannot place a line it has not measured. So the tree comes before the text here — you cannot follow the text pipeline until you know what it is recording into — with the HUD after them both as the other thing that records into the same tree. Outside that stack and one pair, nothing in the part depends on anything in the part except the hub: the client level and prediction share a ledger, and everything else answers the loop independently.

Before you start

Part IX, and not optionally — this part is the same wire watched from the receiving end. Three pages here begin at a packet that has already arrived, and the connection is what it took to get there.

Part I’s anatomy for the two-loops figure, which is the premise of the whole part: the server’s tick loop and the client’s frame loop are different clocks, and two of the pages here, the client level and prediction, open on one of them waiting on the other.

Authority from Part VI, because “what the client is allowed to decide” is the question this part splits in two: the client level answers what it may simulate, prediction and acknowledgement what it may show before it is told, and neither re-derives the five predicates.

Three smaller ones, each for one page. Containers and menus from Part VII before GUI and screens, which opens on a menu and takes for granted that the chest you see is not the chest. Part V before prediction and acknowledgement: the ledger’s six windows open around rather more than a block placed and a block broken, but those two are the ones a viewer needs to have seen, and Part V’s landing page already rules that its pages are watched first. And text components from Part II before text and fonts, which starts from “you have a Component”.

Watch in this order

  1. The client loop — the hub, and the one page every other page in the part leans on. How much simulated time a frame owes, what it spends it on, and what happens to the time it cannot afford. Watch this before anything else in Parts X and XI.
  2. The client level — the same Level class the server runs, with its authority removed. A comparison: what the client really simulates, and what it only pretends to.
  3. Prediction and acknowledgement — the block that appears and then disappears. One ledger, one counter, and a receipt that is not a verdict. Watch it straight after two: the ledger it turns on lives on ClientLevel and is reached through four of that class’s methods.
  4. Input and keybinds — everything between the operating system and a key being down, the four places a press can be swallowed on the way, and a fifth before a click counts.
  5. Options — one flat file and nine fields the server ever hears about. A policy page: what saving does, and who is told.
  6. GUI and screens — what a screen is: the manager, the lifecycle, the widget family, and the four routes by which a screen comes to exist.
  7. The GUI render tree — what a screen records into. Nothing in the 2D UI draws anything; it all appends to a tree that infers its own layering from bounding boxes.
  8. Text and fonts — the one thing recorded into that tree that is a pipeline of its own: six stages from a Component at one end to a quad with a glyph on it at the other.
  9. The HUD — the other thing that records into that tree, and the part’s second policy page: what is drawn over the world, in what order, and under exactly which conditions.
  10. Sound: the engine — the page in the part with the most threads in it: five take part, a block placed near you crosses four of them on its way to an OpenAL source, and one hop to the sound thread it cannot skip.
  11. What makes a sound happen — the content model: three doors a sound comes through, only one of which names it.
  12. Debugging the running game — the closer, and the part’s one pattern lecture: one subscription mechanism, sixteen instances, all of them shipped and fifteen of them unreachable without a JVM flag.

Six to nine are the GUI stack and are watched together. Ten and eleven are the two halves of sound and can be watched in either order; the engine first is the easier way round.

Where the part stops

At one end, the profiler’s frame zone: the frame is one span inside a turn of the client loop, which picks up again after it to settle the pause, and everything inside that zone is Part XI’s — though a few of this part’s own cadences run inside it and stay here for what they decide, the per-frame light pass and the GUI record pass among them. What the server chose to send is Part IX’s.

At the other end, and much the larger boundary: this part explains what a screen, a widget and a glyph are, not the hundred and thirty-odd screens the game ships. 27% of the part’s lines are named on no page in the book, and about four fifths of that is one more Screen or one more widget — the friends and social screens, the pack picker, the recipe book, a class per container. GUI and screens covers them as one pattern with four routes into it, on purpose. Some of the part’s packages hold other parts’ subjects too: the model tree under client/resources is Part XI’s, and client/player, client/multiplayer and the dialog screens are shared with Part VIII, Part IX and Part XIII. One subject belongs to nobody: player reporting, which this book skips; the atlas counts its chat-report package in no part, and its report screens here.

Reference this part uses

Diagram lanes for the abbreviations these pages’ figures use, and the threads, which is where the sound engine’s own thread sits among the rest of the game’s. HUD elements is the gate table the HUD is built on, in record order. Packets for everything arriving from Part IX, and the glossary for partial tick, prediction ledger and extract. Naming drift is the one to keep open beside this part in particular: eight of these twelve pages carry a for a 1.21-era reader box.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The client loop

Verified against Minecraft 26.3 · Part X · one turn of Minecraft.run: how much simulated time a frame owes, what it spends it on, and what happens to the time it cannot afford.

The client has one loop and no schedule. A tick is not a timer callback and not a thread — it is something the loop does on its way to a frame, as many times as the clock says it owes. The clock is asked once per iteration, it answers in whole ticks, and the loop then runs at most ten of them. A frame that earned fifteen runs ten and loses five — and the five are not deferred. The clock keeps its leftover as a fraction of a tick and hands out whole ticks as they come due; a tick that has been counted has left that leftover for good, so nothing will ever run the five, and the world you are standing in has skipped forward without simulating the gap. That the two programs keep different clocks at all is the two loops, and the contrast is sharpest here: the server drops ticks too, but only once it is more than the overload threshold plus twenty ticks behind, and it logs Can’t keep up! when it does (the server tick). The client does it on any frame that needs to, at a ceiling of ten, and says nothing.

The loop on this page runs on one thread, and it is the client’s in the four threads. Main.main renames the JVM’s main thread to "Render thread" and RenderSystem.initRenderThread claims it, so Minecraft.gameThread, BlockableEventLoop.isSameThread and RenderSystem.assertOnRenderThread all agree about that one thread. The name is the trap. There is no separate render thread — no second thread drawing while the first one ticks — and there never was one in this version. What the stack traces call Render thread is the thread that runs the loop, ticks and draws, and it is what every Render thread in this part’s cast tables means.

The cast

classwhat it decidesthread
Minecraftthe loop itself, and — being a ReentrantBlockableEventLoop — the main thread’s task queueRender thread
DeltaTracker.Timerhow many whole ticks this frame owes, and what the leftover fraction isRender thread
PacketProcessorwhere packets decoded on Netty threads wait to be appliedfilled on Netty, drained here
TickRateManagerthe tick rate the Timer’s target comes from — a shared class the client holds one of, whose rate and freeze only the server setsread here, decided there
FramerateLimitTrackerwhat the frame cap actually is, which is not always the optionRender thread
FramerateLimiterthe park that enforces itRender thread
Mainthe process: the config, the shutdown hook, the thread’s nameJVM main = Render thread

One turn of the loop

Minecraft.run spins until Minecraft.running goes false, and each iteration is RenderSystem.pollEvents followed by Minecraft.runTick. The figure is that iteration. It is drawn as a flowchart rather than a conversation because the fact worth having is a decision — the clamp, and what falls off the end of it.

flowchart TD
    POLL["RenderSystem.pollEvents — SDL events, inline"]
    subgraph RT["Minecraft.runTick"]
        PRE["Pre render: Window.shouldClose, then a pending reload"]
        ASK["DeltaTracker.Timer.advanceGameTime — whole ticks owed"]
        DRAIN["PacketProcessor.processQueuedPackets, then BlockableEventLoop.runAllTasks"]
        TEX["TextureManager.tick, once if a tick is owed and the world is not frozen"]
        CLAMP{"more than ten ticks owed?"}
        TICK["Minecraft.tick, up to ten times"]
        PREFRAME["Render: SoundManager.updateSource, then MouseHandler.handleAccumulatedMovement"]
        FRAME["Minecraft.renderFrame, then the limiter parks"]
        POST["Post render: recompute Minecraft.pause, the timer's pause and freeze"]
        PRE --> ASK --> DRAIN --> TEX --> CLAMP
        CLAMP -- "no" --> TICK
        CLAMP -- "yes: the excess already left the residual" --> TICK
        TICK --> PREFRAME --> FRAME --> POST
    end
    POLL --> PRE
    POST -- "Minecraft.run, next iteration" --> POLL

One iteration, with the boundary drawn: only RenderSystem.pollEvents is outside Minecraft.runTick, which is why a key press lands in no profiler zone. Both answers to the clamp reach the same node, which is the point: at most ten run either way, and what differs is what became of the rest.

Read it as owe, spend, draw, settle. The clock says how much simulated time has passed; the loop spends it on packets, tasks and up to ten ticks; the frame draws whatever the world looks like afterwards; and only then does the loop notice whether the game is now paused — which is why the first frame of a pause is drawn unpaused.

Two of the figure’s nodes need unpacking. The drain is two calls, not one — PacketProcessor.processQueuedPackets takes the packets Netty decoded, BlockableEventLoop.runAllTasks takes everything else the thread was handed — and the pair between the ticks and the frame is SoundManager.updateSource, which sends the camera to the OpenAL listener on the sound thread, then MouseHandler.handleAccumulatedMovement, which turns the mouse delta into rotation. Both of the second pair run after the ticks and before the frame, so an iteration’s ticks run on the old rotation and its frame shows the new one.

Pre render, Render and Post render, which head three of the figure’s nodes, are Window.setErrorSection calls — the crash report’s breadcrumb, so a client that dies takes whichever of the three it was in to the report with it. What happens inside the frame is the frame; this page does not follow the loop inside the profiler’s frame zone, and every zone inside it is that page’s. Note where the frame limiter sits: inside Minecraft.renderFrame, after the present, with only the fpsUpdate zone after it, and before the pause is recomputed.

The ten, and the arithmetic behind it

DeltaTracker.Timer.advanceGameTime takes the elapsed milliseconds, divides by whatever DeltaTracker.Timer.targetMsptProvider returns for DeltaTracker.Timer.msPerTick, adds the result to DeltaTracker.Timer.deltaTickResidual, takes the whole part out and returns it. The fraction that stays behind is the partial tick everything interpolates against. The clamp then happens in the loop, not in the Timer — so the ticks above ten are not deferred to the next frame, because they left the residual when they were counted.

Ten ticks is the ceiling, the number Minecraft.MAX_TICKS_PER_UPDATE names. The clamp in Minecraft.runTick shows the number rather than the name because javac writes a static final int set to a literal in at every use site, so a decompile can never tell a documented constant from a dead one.

The divisor is not the client’s to choose. DeltaTracker.Timer gets its target from DeltaTracker.Timer.targetMsptProvider, which is Minecraft.getTickTargetMillis, which asks the level’s TickRateManager for TickRateManager.millisecondsPerTick whenever it TickRateManager.runsNormally, and never less than fifty milliseconds. /tick rate is a server command that changes the arithmetic inside the client’s frame loop — when it slows the world down; a faster rate leaves the client at twenty ticks a second. /tick freeze is a different lever — under it the Timer’s divisor falls back to fifty milliseconds — and the loop reads it directly rather than through the Timer: Minecraft.isLevelRunningNormally asks the level’s TickRateManager again, and that is what stops TextureManager.tick — which is why freezing the world freezes the water texture — and what gates ClientLevel.animateTick and ParticleEngine.tick inside the tick. The Timer is told the same answer at the end of the iteration, through DeltaTracker.Timer.updateFrozenState, so that the partial tick it hands out stops moving too.

Alongside the game clock the Timer runs a second, unpausable one. DeltaTracker.Timer.advanceRealTime produces DeltaTracker.getRealtimeDeltaTicks, which the menus’ panorama and the autosave indicator animate against, so they move when game time does not. The two constants DeltaTracker.ZERO and DeltaTracker.ONE — two instances of the one nested DeltaTracker.DefaultValue — exist so that code which needs a partial tick can be handed no interpolation or complete interpolation without a branch.

What a tick is, in order

Minecraft.tick is one long method, its order is a dependency order, and the column that matters is the third: most of it is inside a gate, and two of the things that are not are the reason the main menu has music.

in orderwhat runswhat gates it
1Minecraft.clientTickCount advancesnothing
2TickRateManager.ticka level, and not paused
3the game modea level, and not paused
4Minecraft.pick, at a partial tick of onenothing
5Tutorial.onLookAt, with what the pick foundnothing
6Gui.tick — Minecraft.missTime pinned high while a screen is opennothing
7the keybind drain, Minecraft.handleKeybindsneither a screen nor an overlay — and nothing about a level
8GameRenderer.tick, ClientLevel.tickEntities, Level.tickBlockEntities, then LocalPlayer.sendChanges — the input and movement packetsa level, and not paused
9the music and sound managersnothing
10the first-server toast, Tutorial.tick, then ClientLevel.tick alone inside a crash-report handlera level, and not paused
11ClientLevel.animateTick, ParticleEngine.ticka level, not paused, and the tick rate manager running normally
12ServerboundClientTickEndPacketa level, a connection, and not paused
13KeyboardHandler.tick, where the F3+C crash countdown livesnothing

Six of the thirteen rows are gated on nothing where they are called, and four of them are what a client with no world is still doing: it counts ticks, it runs the interface, it plays music, and it runs the F3+C crash countdown. The pick and the tutorial’s look are called too, and each returns at once without a level. Everything that is the world is inside level != null && !pause. With no level that middle collapses — but not into one branch. Two separate else arms at two points in the method clear the spectated entity’s post-effect and tick the pending connection, with row nine running between them.

Two of those rows are load-bearing elsewhere in the book. Row twelve is why a client still in configuration sends no tick-end packet at all: it sits inside the level block, and the server reads the packets it does get to decide that a player who sent no movement this tick is standing still. And row four is only half of Minecraft.pick, which runs once per tick and once per frame — the tick’s call at a partial tick of one, the frame’s at the real one, and it is the frame’s result the crosshair and the block outline use. A frame that runs three ticks calls it four times; a frame that runs none calls it once.

Where work leaves this thread, and where it comes back

Five ways off this thread, and one re-entry that is not a way off it.

  • Packets decoded on Netty threads are parked by PacketProcessor.scheduleIfPossible and drained in the scheduledPacketProcessing zone — once per frame, not once per tick. That single fact is behind most of what looks like network jitter; the connection is the other side of it.
  • Tasks from other threads land in scheduledExecutables through BlockableEventLoop.execute. From this thread the same call usually runs inline instead of queueing — but not while a task is already running, inline or queued, because ReentrantBlockableEventLoop.scheduleExecutables returns true for the whole of ReentrantBlockableEventLoop.doRunTask, which is what stops a task from re-entering itself. SDL events, dispatched by SDLEventHandler.pollEvents inside RenderSystem.pollEvents, are not inside one, so they execute before the tick that will observe them — see input and keybinds.
  • Section meshing goes to Util.backgroundExecutor and is collected by SectionRenderDispatcher (Part XI).
  • Timers, of which the client has exactly two, and neither changes the game from its own thread. PeriodicNotificationManager and RemoteFriendListUpdateHandler each own a scheduler, and each hops back here with BlockableEventLoop.execute before touching the game (the friends list swaps its own fetched data in on its own thread) — which is why nothing in the client’s simulation is ever driven by a timer callback.
  • GPU work registered with RenderSystem.queueFencedTask is picked up by RenderSystem.executePendingTasks, which stops at the first unsignalled fence rather than waiting. It looks general and is not: the one thing in the tree that queues a fenced task is the OpenGL backend’s asynchronous texture readback.

The re-entry is BlockableEventLoop.managedBlock, which pumps that second queue while the loop is blocked waiting for the integrated server or for a world’s resources to load — so work runs on this thread at a moment when the thread is not running its loop at all. The mechanism is the server tick.

The profiler wraps all of it. Minecraft.constructProfiler picks one per iteration — InactiveProfiler, or the frame-profile ContinuousProfiler behind the F3 pie chart — combined with the MetricsRecorder’s profiler while it records and a SingleTickProfiler when a debug flag asks for one, and Minecraft.finishProfilers ends the single-tick one and posts the pie chart’s results. RenderSystem.pollEvents is inside the profiler scope but outside Minecraft.runTick, so input polling lands in no named zone and shows up on the pie chart as unspecified time. That is not why RenderSystem.isFrozenAtPollEvents exists, though: its one caller is ClientCommonPacketListenerImpl.handleKeepAlive, which holds the keep-alive reply back while a poll has been stuck for more than 200 milliseconds and sends it from the connection’s first tick after the poll returns — so a game stuck in the poll by a window drag reaches the server as latency.

Pausing, which is a field the screen votes on

Minecraft.pauseIfInactive, called during the frame, opens the pause menu when no screen is up, the window has been unfocused for more than half a second and Options.pauseOnLostFocus is on — and the menu pauses the game only through its vote. Minecraft.pause — the field — is recomputed at the very end of Minecraft.runTick as a conjunction of three: singleplayer, and the GUI says we are pausing, and the world is not open to LAN. Only the middle one is a question about the interface — Gui.isPausing asks the current screen and overlay, and Screen.isPauseScreen defaults to true, which is why the options screen stops a singleplayer world and a chest does not (GUI and screens owns the vote and the override a container screen casts). On the rising edge the loop calls SoundManager.pauseAllExcept, sparing music and UI sounds, and every iteration it hands the state to DeltaTracker.Timer.updatePauseState.

The frame cap is usually the option, and sometimes is not

FramerateLimitTracker.getFramerateLimit returns the option unchanged normally; caps it at thirty after a minute idle; replaces it with ten when the window is iconified or after ten minutes idle; and replaces it with sixty in a menu with no level — which can be more than the player asked for. The iconified case is iconified, not unfocused — a different state with a different consequence, one section above — except in exclusive fullscreen, where an unfocused window counts as iconified. The two idle cases apply only when Options.inactivityFpsLimit is set to the AFK behaviour, and the iconified test wins over both. FramerateLimiter.limitDisplayFPS is skipped entirely at or above 260 — the option’s own maximum, i.e. “unlimited”; below it, it parks for most of the remainder, correcting for how much the JDK’s park habitually overshoots, and busy-spins the last fraction. The profiler notices too, though it does not stop: FramerateLimitTracker.isHeavilyThrottled is the ContinuousProfiler’s suppress warnings predicate, so a throttled client still measures itself but stops complaining that its frames are slow.

The frame’s timing is three different measurements, two of them on the F3 screen, and it is worth knowing which is which. Minecraft.fps is a static field sampled once a second, and is the one printed as fps. Minecraft.frameTimeNs is a CPU span that stops at the blit, before the present and before the limiter, and is read by nothing but telemetry — so it appears on no line of the overlay. The third has no name of its own: the frame-time graph is fed wall-clock between frames, measured after the limiter, so it includes the sleep — as the frame count does and Minecraft.frameTimeNs does not.

Starting, and the three ways of stopping

Main.main builds a GameConfig from the command line, installs a shutdown hook, renames the thread, calls RenderSystem.initRenderThread and constructs Minecraft; a SilentInitException out of that constructor exits quietly rather than crashing. Minecraft.running is set true inside that constructor, but five statements after the Options are read from disk. A setting’s listener only fires while that flag is true, so every OptionInstance.set performed while loading options.txt silently skips its own — the loading guard is that rule and what it costs. Main.main then calls Minecraft.exitWorldAndClose, and its last statement arms ClientShutdownWatchdog.startShutdownWatchdog over what follows. That is the second of two armings, not the only one: the window-close callback arms the same watchdog against the Render thread while the game is still running, so a close the window is asked for is watched from the moment of the request, where a Quit button’s is not watched until Main.main arms it after Minecraft.exitWorldAndClose returns. The two do not have the same powers, either. Both start a daemon thread that sleeps fifteen seconds and, unless a later arming has moved the counter on by then, builds a crash report from the main thread’s stack — but the close-callback arming stops there, at report only, while the one after Minecraft.run has returned goes on to take the process down — unless the close-callback watchdog has already fired, which leaves the counter where no later arming can start.

Stopping has three doors and one corridor. Minecraft.stop sets Minecraft.running false, and is what Window.shouldClose triggers at the top of Minecraft.runTick. Minecraft.emergencySaveAndCrash is where a ReportedException or any other throwable from the loop body ends up, by way of Minecraft.emergencySave, which releases the reserved memory block, halts the integrated server and shows the saving screen. And an out-of-memory error does not necessarily end anything: the first one makes Minecraft.run stop advancing game time altogether — GUI only, no ticks, no packets, no world — after an emergency save; a second one rethrows.

The corridor is Minecraft.exitWorldAndClose and then Minecraft.close, which tears down in a fixed order — the time source first, outside the try, then an offline presence to the friends service, the friends list, the timer query, telemetry, compliancies, the atlas and font managers, the game renderer, the shader manager, the level renderer, the sound manager, the three texture managers, resources, the Tracy capture, the narrator, FreeType, the surface and the renderer — and then, in a finally block, only the window, the graphics backend’s library, SDL’s own shutdown and the executors. There is no Minecraft.destroy.

For a 1.21-era reader. Names to stop hunting for: Minecraft.getPartialTick, Minecraft.noRender, Minecraft.tell, Minecraft.destroy, Minecraft.screen and Minecraft.setScreen (both now on Gui), Timer (now DeltaTracker.Timer), GLFW and its callbacks (now SDL events, drained by SDLEventHandler.pollEvents), and initGameThread / isOnGameThread, which do not exist because the second thread they distinguished does not either.

Where to look

Minecraft.run and Minecraft.runTick — the loop is those two methods. DeltaTracker.Timer.advanceGameTime for the tick arithmetic and Minecraft.getTickTargetMillis for who sets its rate. Minecraft.tick for the ordered contents of a tick. FramerateLimitTracker.getFramerateLimit for the frame cap that is not the option. Main.main for how the process starts, and Minecraft.close for the order in which it comes apart.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The client level

Verified against Minecraft 26.3 · Part X · the same Level class the server runs, with its authority removed: what the client really simulates, and what it only pretends to.

Place a repeater on the client and it is there — drawn, collidable, part of the world. Ask the client whether that repeater has a tick scheduled and it will tell you, confidently, that it does not. ClientLevel’s two scheduled-tick lists are the empty list BlackholeTickAccess hands out: it accepts a schedule, drops it, reports no when asked whether a tick is pending, and counts zero. Shared block code that consults the level therefore gets a wrong answer rather than an error, and anything that reschedules itself looks inert until the server speaks.

That is the shape of the whole class. ClientLevel is not a passive receiver and not an authority either: it simulates hard — every block entity with a client ticker ticks regardless of distance, every local block change that alters how light passes is relit locally, it keeps its own clock and free-runs between corrections — while inheriting a set of shared Level methods that have been quietly reduced to constants. Reading ClientLevel is largely a matter of noticing which overrides are empty. (Authority is a word with five predicates behind it, and authority is where they are set out; this page needs only the root, Entity.isLocalInstanceAuthoritative, which decides whose positions arrive from the server at all, and one fact about blocks: the server alone decides a block’s state.)

The cast

classwhat it decidesthread
ClientLevelwhat the client simulates, and what it answers when askedRender thread
ClientChunkCachewhich chunks exist, in a fixed-size array a few chunks wider than the view diameter, indexed modulo its sideRender thread
ClientLevel.ClientLevelDatathe client’s own game time, difficulty, horizon height and void darknessRender thread
LevelLightEnginethe light the client computes for itself, unbudgetedRender thread
LevelExtractorthe main route from the level to the renderer — pushed and pulledRender thread
ClientPacketListenerthe view radius and simulation distance the server announcedRender thread
TransientEntitySectionManagerentity storage with no persistence, no chunk save, no index to diskRender thread
Entitywhether a position update is a snap or an interpolationRender thread

Where the two levels differ

The comparison is the page. Every row but the last is a method both sides inherit from the shared hierarchy — Level itself, or one of the interfaces above it — and that the two sides answer differently; the last row is not a method but the storage behind one. Seven of the nine rows are the client hollowing something out, the storage among them. Two run the other way — Level.setBlocksDirty, empty on the server and real here, and Level.shouldTickDeath.

shared methodon the serveron ClientLevel
Level.shouldTickBlocksAtticket rangeinherited — unconditionally true
ScheduledTickAccess.scheduleTickreal LevelTicksboth lists are BlackholeTickAccess.emptyLevelList
Level.explodethe real thingempty override — particles arrive by packet
LevelAccessor.gameEventvibrations, sculkempty override
LevelReader.getUncachedNoiseBiomegeneratesreturns plains
LevelReader.hasChunkasks the sourceunconditionally true
Level.setBlocksDirtyemptyreal — and it notifies LevelExtractor, never LevelRenderer
Level.shouldTickDeathtruestricter — within the server’s simulation distance
entity storagepersistent: a disk store, known UUIDs, per-chunk load statestransient: the same lookup, none of the bookkeeping

Three of those deserve their own sentence. LevelReader.hasChunk returning true unconditionally means that particular question is useless on the client — though Level.isLoaded still works, because it goes through the chunk source instead. Level.explode doing nothing is why an explosion you can see is not a simulation: one ClientboundExplodePacket carries the sound, the particle and the knockback, and the handler plays all of it, the sound when the packet’s flag asks. And Level.shouldTickDeath is the only row where the client is the stricter of the two: it uses the server’s announced simulation distance to decide whether a dying mob plays its death animation.

That last row is also one of only two things in the client that read the server’s announced simulation distance: the death-animation test, and LevelExtractor’s render-stats string. (The client’s own Options.simulationDistance is a different number with its own readers.) The level keeps its own copy in ClientLevel.serverSimulationDistance, but never decides it: the value arrives from the server on ClientPacketListener.serverSimulationDistance, beside ClientPacketListener.serverChunkRadius — both seeded at login and updated by their own packets — and is handed to each new ClientLevel at construction and thereafter through ClientLevel.setServerSimulationDistance.

What it does simulate: the two cadences

Per client tick, from Minecraft.tick: ClientLevel.tickEntities, then Level.tickBlockEntities, then ClientLevel.tick — which does Level.updateSkyBrightness unconditionally and then, only if the tick rate manager is running normally, the world border, the clock, the weather effects and the breaking-progress sweep. After that, unconditionally again: the sky flash countdown, the End flash state, and the explosion tracker. ClientLevel.animateTick and ParticleEngine.tick follow, both gated on the game not being frozen.

So “ticks regardless of distance” is true of distance and false of /tick freeze: Level.shouldTickBlocksAt is unconditionally true here, but Level.tickBlockEntities still checks the tick rate manager, and the loop skips the whole tick while paused.

Per frame, and only per frame: ClientLevel.update, which calls ClientLevel.pollLightUpdates and then runs the light engine. It is gated on the game being loaded, the level existing, and the frame being one that advances game time — so the blocking loops that draw a frame without ticking do no lighting either.

The light budget is a slope that ends in a cliff, and it budgets the wrong half of the work on purpose. Below ClientLevel.LIGHT_UPDATE_QUEUE_SIZE_THRESHOLD the frame runs a tenth of ClientLevel.lightUpdateQueue, floored at ClientLevel.NORMAL_LIGHT_UPDATES_PER_FRAME; at the threshold or above it runs the entire queue. Then LevelLightEngine.runLightUpdates drains the engine’s own propagation queue completely, every frame, with no budget at all. The budget controls how fast the client accepts the server’s light, not how fast it computes its own — so a burst of chunks below a thousand queued updates is spread over frames, a tenth of the queue (at least ten) each frame, and only a queue of a thousand or more is taken in one long frame.

A chunk arrives

The grounding trace, and the one place the whole class is visible at once. The shaded band is what the figure is for: everything in it is one turn of Minecraft.runTick, so the light that arrives at the foot is not a later frame’s work — while the queue ahead of it fits the frame’s share.

sequenceDiagram
    participant CPL as ClientPacketListener
    participant CCC as ClientChunkCache
    participant CL as ClientLevel
    participant LLE as LevelLightEngine
    participant LX as LevelExtractor

    rect rgba(0, 0, 0, 0.04)
    Note over CPL,LX: one Minecraft.runTick
    CPL->>CPL: handleLevelChunk<br/>WithLight, on the Render thread
    CPL->>CCC: replaceWithPacketData — blocks now
    CCC->>CL: unload, if the torus slot was taken
    CCC->>CL: onChunkLoaded — tint caches, ticking
    CPL->>CL: queueLightUpdate — light later
    CL->>CL: tickEntities, then tickBlockEntities
    CL->>CL: update, then pollLightUpdates
    CPL->>LLE: the queued lambda — setLightEnabled, updateSectionStatus
    CPL->>CL: then setSectionRangeDirty, a 3x3 of columns
    CL->>LLE: runLightUpdates, unbudgeted
    CCC->>LX: setSectionDirty, from onLightUpdate
    end

One packet, one Minecraft.runTick, and the light arriving at the bottom of it while the light queue is short: the band is the whole span, so the gap between the blocks and the light is not a wait. Note that the last arrow leaves the chunk cache, not the level.

Four things about the shape. Blocks and light are separated in the handler, so a chunk exists, ticks and can be walked on before it is lit. The separation is not a wait while the light queue is short: the whole band is the one Minecraft.runTick that handled the packet, and above forty frames a second the frame usually owes no tick at all, so the light is applied with nothing having ticked in between. In a burst below a thousand queued updates the frame takes only its share, and the rest waits for later frames. It is also one arrival, not two: the lambda ClientLevel.queueLightUpdate files runs both ClientPacketListener.applyLightData and ClientPacketListener.enableChunkLight, which is why the listener’s lane wakes up again at the foot of the figure. And the renderer is reached two ways — the level pushes (ClientLevel.sendBlockUpdated, ClientLevel.setBlocksDirty, ClientLevel.setSectionRangeDirty), but the chunk cache and four of ClientPacketListener’s own handlers reach LevelExtractor directly (the biome resend marks whole sections dirty, a transient block is queued through it, and login and the game-test highlight reach through it for the debug renderers), and the extractor also pulls: ClientLevel.entitiesForRendering, ClientLevel.destructionProgress and ClientLevel.getGloballyRenderedBlockEntities are read each frame and are mutated with no notification at all.

Unloading is the same trace backwards: ClientChunkCache.drop clears the slot, ClientLevel.unload clears the chunk’s block entities, switches its light off and stops ticking its entities, and the removal of its light data is queued behind the other light updates.

The chunk cache is a torus

ClientChunkCache is not a map. ClientChunkCache.Storage is a flat AtomicReferenceArray whose side is the view diameter plus a margin, indexed by the chunk coordinates modulo that side — so moving the origin evicts the ring behind you by overwriting it. The array is atomic, the two centre coordinates are declared volatile, and so is the reference to the ClientChunkCache.Storage itself, because the packet handlers are not the only readers. The section-compile workers are: a RenderSectionRegion resolves biome tint live rather than from its snapshot, so RenderSectionRegion.getBlockTint goes through ClientLevel.getBlockTint into the chunk cache from a background thread while the Render thread is moving the origin. The ThreadLocal and the read-write lock inside BlockTintCache are the same contract said out loud.

ClientChunkCache.calculateStorageRange makes the array a few rings wider than the view distance, ClientChunkCache.Storage.inRange and ClientChunkCache.Storage.getIndex decide where a chunk lands, and ClientChunkCache.updateViewCenter moves the origin by assigning two integers. Its verbs are ClientChunkCache.replaceWithPacketData, ClientChunkCache.drop, ClientChunkCache.replaceBiomes, ClientChunkCache.updateViewRadius and ClientChunkCache.onLightUpdate, and it keeps four delta sets — ClientChunkCache.addedEmptySections, ClientChunkCache.removedEmptySections, ClientChunkCache.addedLoadedChunks and ClientChunkCache.removedLoadedChunks — double-buffered by ClientChunkCache.flipUpdateTrackingSets so the renderer can ask what changed since last frame.

Who interpolates, and who snaps

An entity position arriving from the server does not simply become the entity’s position. Entity.moveOrInterpolateTo hands it as a target to the entity’s InterpolationHandler (Entity.getInterpolation), and if the handler declines, the position, yaw and pitch are assigned directly. The base Entity.createInterpolationHandler returns InterpolationHandler.NO_OP, which declines everything, so the default across the entity tree is to snap; seven classes override it, and six opt in (a minecart only outside the minecart-improvements experiment).

supplies an InterpolationHandlersnaps
LivingEntity — so nearly every mob and every remote playerAbstractArrow and every other projectile but FishingHook
DisplayPrimedTnt
ExperienceOrbItemEntity — a dropped item
FishingHookFallingBlockEntity
AbstractBoat and AbstractMinecartShulker, whose override hands back the no-op
everything else that does not override

That table is the reason a dropped item’s movement looks different from a mob’s over the same connection: nothing is smoothing it. The handler itself — its window (for a living entity, as many steps as its type’s update interval: three by default, two for a player, an allay or a mannequin; for the others a fixed three, or a display’s teleport duration), and the 64-block distance past which ClientPacketListener does not hand it the move at all and snaps instead — belongs to movement and collision; what this page owns is who has one. Entity.getClientPositionAndRotation is what ServerboundMoveVehiclePacket and PositionMoveRotation both ask, and it answers with the interpolation’s target when there is one or the entity’s current position.

The clock and the weather run themselves, and neither interpolates

Two of the things in that tick are the client keeping numbers the server also keeps, and both are worth a sentence because both are the class’s character in miniature: it free-runs, and it is corrected rather than told.

The clock. ClientLevel.tickTime increments ClientLevel.ClientLevelData.gameTime every tick the tick rate manager runs normally and hands the result to a ClientClockManager — owned by ClientPacketListener, reached through ClientLevel.clockManager, and the thing anything asking the client what time it is asks. ClientPacketListener.handleSetTime is the only correction: it is the only caller of ClientLevel.setTimeFromServer, and it also hands the clocks their updates. So the time in a screenshot is the client’s own count since the last correction, not the server’s.

The weather. Weather on the client is presentation only: ClientLevel.tickWeatherEffects spawns rain particles and picks rain sounds, while the rain and thunder levels are ramped on the server by ±0.01 a tick and broadcast on every tick they change — about a hundred packets across a five-second transition. What the client does not do is interpolate within a tick: Level.setRainLevel writes the old and new values to the same number, so the partial tick buys nothing and the level steps twenty times a second rather than smoothly.

Two more numbers in that tick are deliberately coarse in the same way. The breaking-progress sweep over ClientLevel.destroyingBlocks and ClientLevel.destructionProgress runs only on every twentieth tick, and drops a crack nobody has updated for twenty seconds, which is what clears one left by someone who disconnected mid-dig (an abandoned dig is cleared at once by the server’s -1); the progress itself is applied the moment its packet arrives. And ClientLevel.animateTick samples 667 positions at radius sixteen and another 667 at radius thirty-two every tick regardless of the machine, with ClientLevel.doAddParticle culling by distance afterwards and able to downgrade the particle setting stochastically on top.

The one thing in the class that is not coarse is the sound of your own footsteps, which LocalPlayer.playSound plays straight through ClientLevel.playLocalSound while the server leaves you out of its broadcast, so what lags is what you hear of other people (who hears it owns the exclusion rule and the sounds this level defers for distance).

What else it holds

ClientLevel.tickingEntities is an EntityTickList, fed by ClientLevel.EntityCallbacks, whose ticking pair ClientLevel.entityStorage calls as a chunk starts and stops ticking and as an entity arrives in, crosses into or out of, or leaves a ticking one. Three more fields are each the whole of a mechanism another page owns: ClientLevel.globallyRenderedBlockEntities is the set that draws from anywhere, ClientLevel.blockStatePredictionHandler is the ledger prediction and acknowledgement owns, and ClientLevel.explosionTracker is a per-tick budget of at most 512 block particles that empties itself every tick rather than deferring anything.

The four tint caches, and the soft biome edge

ClientLevel.tintCaches holds four BlockTintCaches — grass, foliage, dry foliage, water — and each is what makes a biome colour boundary look softer than the biome boundary is. ClientLevel.calculateBlockTint box-blurs the per-block answer over the columns the biome blend radius option names and caches that, over a lookup that is itself on the ragged side of the two biome borders (biomes). The blur is the client’s, entirely: nothing on the server knows the edge is soft. That is also why a chunk arriving invalidates all four of them at once, in the trace above.

The client runs a real light engine — block light always, sky light only where the dimension has it — and every client-side Level.setBlock that passes the shared LightEngine.hasDifferentLightProperties test the server uses relights (lighting owns what that test asks; the consequence here is that a purely cosmetic change to a stair or a slab relights anyway). Its push list, on the other hand, is one entity wide: ClientLevel.getPushableEntities returns at most the local player.

And ClientLevel never notifies LevelRenderer. It has no reference to it, and not one per-block or per-section dirty method is left on LevelRenderer — they are all on LevelExtractor now. What LevelRenderer keeps is whole-world invalidation, LevelRenderer.invalidateCompiledGeometry and its neighbours, which the extractor calls.

For a 1.21-era reader. Gone: ClientLevel.levelRenderer, every dirty method on LevelRenderer (now on LevelExtractor), and ClientLevel.getStarBrightness and ClientLevel.effects (both now EnvironmentAttribute lookups — see environment attributes and timelines).

Where to look

ClientLevel.tick and ClientLevel.update for the two cadences. ClientChunkCache.Storage for the torus, and ClientChunkCache.replaceWithPacketData for what a chunk packet does. ClientLevel.pollLightUpdates for the budget arithmetic. ClientLevel.tickEntities for the entity walk and ClientLevel.EntityCallbacks for what joins the ticking set. Entity.moveOrInterpolateTo for the snap-or-smooth fork. Then read the empty overrides — ClientLevel.explode, ClientLevel.gameEvent, ClientLevel.getBlockTicks — because they are the page in miniature.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Prediction and acknowledgement

Verified against Minecraft 26.3 · Part X · a block placed against a wall the server will not allow: the client shows it, the server refuses it, and a numbered receipt decides when the lie ends.

The client cannot wait a round trip to show you the block you just placed, so it places it locally and tells the server afterwards. Everybody who has described this system has described it as a rollback: the client guesses, the server judges, the ack says yes or no. That is not what happens. ClientboundBlockChangedAckPacket carries no verdict at all — it is a receipt for a number, sent (once the server considers the client loaded) for actions the server refused exactly as it is sent for actions it allowed, and sent carrying zero for an aborted dig or a changed dig direction. Nothing in the protocol ever says no. What makes the system correct instead is an ordering rule: any correction the server intends travels earlier in the stream than the receipt for it, because the correction leaves from inside the handler or with the level’s own tick, while the receipt is only a number the connection flushes after both.

This page is the machinery under that rule: one ledger per level, one counter per connection, six windows in which a prediction can be opened, and four methods that between them decide whether the world moves. It is also the client’s second answer to the question authority asks — the client level answers what may I simulate, and this answers what may I show before I am told — and Part V’s block interaction and block breaking are where the two applications are watched.

The cast

classwhat it decidesthread
MultiPlayerGameModewhen a prediction window opens, and what goes in itRender thread
BlockStatePredictionHandlerthe ledger: what was there before, under which sequence numberRender thread
ClientLevelthe four writes — the prediction, the absorbed correction, the trigger, the settleRender thread
PredictiveActiona one-method interface: given the sequence, produce the packetRender thread
ServerGamePacketListenerImplone integer per connection, and when it is emittedServer thread
ServerPlayerGameModethe authoritative version of the same actionServer thread

Two state machines, running against each other

The whole mechanism is one small state machine on each side, and neither knows the other’s state. The client’s runs per position; the server’s runs per connection.

stateDiagram-v2
    state "Client — one position in the ledger" as CLIENT {
        [*] --> Absent
        Absent --> Retained : ClientLevel.setBlock
        Retained --> Retained : again, sequence refreshed
        Retained --> Corrected : ClientLevel.setServerVerifiedBlockState
        Retained --> [*] : BlockStatePredictionHandler.<br/>endPredictionsUpTo
        Corrected --> [*] : the same call
    }
    state "Server — one integer per connection" as SERVER {
        [*] --> Idle
        Idle --> Raised : ServerGamePacketListenerImpl.<br/>ackBlockChangesUpTo
        Raised --> Raised : another acked action, same tick
        Raised --> Idle : flushed, head of the listener's tick
    }

Two machines that never read each other, on different clocks: the client’s runs once per position, the server’s once per connection. Both exits to the end are the same call, which writes the entry’s state wherever it differs from the screen: the state from before the prediction if no update arrived, the server’s if one was absorbed.

Read the two columns as running at different rates. The client’s machine advances several times per tick, once per position touched. The server’s advances on every packet that reaches a ServerGamePacketListenerImpl.ackBlockChangesUpTo call. There are three such call sites: ServerboundUseItemOnPacket, ServerboundUseItemPacket, and the four destroying actions inside ServerboundPlayerActionPacket’s handler. ServerboundPlayerActionPacket carries nine actions in all, and the other five — dropping one item, dropping a stack, releasing a use, swapping hands and the stab — raise nothing. The counter then empties once per connection tick, which is why five acked actions in one tick produce one receipt carrying the highest number, and why one ack can drive a dozen positions out of the ledger at once.

Note what the diagram does not contain: any transition on which the server says no. There is none. Both of the client’s exit transitions are driven by the same packet, and which of the two a position takes is decided entirely by whether a block update reached it first.

The four writes

Everything the ledger does happens through four methods on ClientLevel, and the difference between them is the whole mechanism.

ClientLevel.setBlock — the ordinary write. While a prediction is open, and only if the write succeeded, it calls BlockStatePredictionHandler.retainKnownServerState with the state that was there before. If the position already has an entry, only the sequence is refreshed: the ledger keeps the first pre-change state it ever saw for that position, and the player position recorded with it.

ClientLevel.setServerVerifiedBlockState — every inbound block update goes through here. If the position is in the ledger it overwrites the entry and the world is not touched, so the prediction stays on screen. If it is not in the ledger — the common case, for blocks the player did not touch — it writes the world immediately. The absorption is per position, not per packet.

ClientLevel.handleBlockChangedAck — the trigger. The receipt’s way into the ledger: it hands the receipt’s number straight to BlockStatePredictionHandler.endPredictionsUpTo, which removes every entry at or below it and passes each one’s recorded state to the settle.

ClientLevel.syncBlockState — the settle. Applies the recorded state only if it differs from what is there, with flags Block.UPDATE_NEIGHBORS plus Block.UPDATE_CLIENTS plus Block.UPDATE_KNOWN_SHAPE (the whole set is in block update flags). That third flag suppresses the shape pass, and neighbour updates are inert on the client anyway — so the restore is a bare state write plus a remesh. A prediction can touch more than the block you clicked: placing one half of a door writes the other, and a shape update can walk outwards from the block that changed. Call that spread the cascade. The cascade does not re-run on the way back. Reconciliation is correct only because every position the cascade touched got its own ledger entry on the way out.

A placement the server refuses

The state diagram says what the states are; this says what order the packets arrive in, which is the part correctness rests on.

sequenceDiagram
    participant MPGM as MultiPlayer<br/>GameMode
    participant BSPH as BlockState<br/>PredictionHandler
    participant CL as ClientLevel
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SPGM as ServerPlayer<br/>GameMode

    MPGM->>BSPH: startPredicting — currentSequenceNr becomes n
    MPGM->>CL: setBlock, from performUseItemOn through ItemStack.useOn
    CL->>BSPH: retainKnownServerState — the truth, filed under n
    MPGM->>SGPL: ServerboundUseItemOnPacket(hand, hit, n)
    MPGM->>BSPH: close — the window shuts and the block is on screen
    SGPL->>SGPL: hasClientLoaded? then ackBlockChangesUpTo — first
    SGPL->>SPGM: useItemOn — the place fails canPlace, so nothing changes
    SGPL->>CL: two ClientboundBlockUpdatePackets, whatever the outcome
    CL->>BSPH: updateKnownServerState — the entry is overwritten, the world is not
    Note over SGPL,SPGM: later in the same server tick, the listener's tick opens by flushing it
    SGPL->>CL: Clientbound<br/>BlockChangedAckPacket, carrying n
    CL->>BSPH: endPredictionsUpTo(n)
    BSPH->>CL: syncBlockState — air goes back
    CL->>CL: Entity.absSnapTo — only if the restored block now intersects you

The two ClientboundBlockUpdatePackets leave from inside the handler; the ack is only a field the listener’s tick flushes, later in the same server tick. That is the ordering rule for a refusal, visible here as the distance between the block-update arrow and the note.

The ack is recorded before the action is attempted, so by the time the refusal happens the receipt is already promised. The correction is not the ordinary chunk broadcast but a targeted pair of resends the handler makes for every click that passes the build-height test, whether or not the action was then attempted. It skips them only when an earlier gate turns the packet away — an item a feature flag disables, a target out of reach, a hit point more than a block from the target’s centre, a target past the build limit — and then the ledger’s own record undoes the prediction. Whenever a correction is sent it is ahead of the receipt: the resends go out inside the handler, an accepted change’s broadcast with the level’s tick, and the ack is only a field assignment that ServerGamePacketListenerImpl.tick flushes after both. The settle is what finally moves the world, and it is a no-op when the absorbed state is already what is on screen. And the snap only happens when the restored block turns out to be inside the player.

Because the ack is buffered rather than sent, where a handler records it does not affect stream order — ServerGamePacketListenerImpl.handleUseItemOn and ServerGamePacketListenerImpl.handleUseItem record it as the first statement inside the ServerGamePacketListenerImpl.hasClientLoaded test, ServerGamePacketListenerImpl.handlePlayerAction after running the break action, and in both cases the correction still reaches the client first.

The six windows

A window is a few microseconds long and entirely synchronous: on the Render thread, inside one call from Minecraft.tick. The counter is raised, the local effect runs, the packet is built with the new sequence and sent, and the window closes — in that order in five of the six, MultiPlayerGameMode.useItem building its packet before its effect — so the packet is always constructed while the ledger is still recording. MultiPlayerGameMode.startPrediction is the private method all six go through, and it is not the same thing as BlockStatePredictionHandler.startPredicting, which is what it calls: the first is the window, the second is the pre-increment of BlockStatePredictionHandler.currentSequenceNr that gives the window its number, and the switch that sets ClientLevel.setBlock recording. ClientLevel.getBlockStatePredictionHandler is package-private, so nothing outside client/multiplayer can reach the ledger at all.

wherewhat it predicts locallywhat it sends
MultiPlayerGameMode.startDestroyBlock, creativethe block removed at onceServerboundPlayerActionPacket START
MultiPlayerGameMode.startDestroyBlock, survivalBlockBehaviour.attack, then removal if the block breaks instantlySTART
MultiPlayerGameMode.continueDestroyBlock, creativeremoval, every sixth tickSTART
MultiPlayerGameMode.continueDestroyBlock, at full progressremovalSTOP
MultiPlayerGameMode.useItemOnMultiPlayerGameMode.performUseItemOn — the block’s hook, the empty-hand hook, ItemStack.useOnServerboundUseItemOnPacket
MultiPlayerGameMode.useItemItemStack.use, including a transformed held itemServerboundUseItemPacket

So the ledger covers rather more than “blocks the player placed or broke”: it covers every ClientLevel.setBlock performed inside one of those windows. That includes the second half of a door, every position a shape cascade revisits, a door a right-click opens or shuts, and — the one attack hook that does it — RedStoneOreBlock.attack, whose lighting change is not side-gated and therefore files a ledger entry when you merely left-click redstone ore.

What the ledger does not cover

Half the value of this page is the list of things people assume it handles. MultiPlayerGameMode’s non-predicting verbs are the giveaway: MultiPlayerGameMode.attack, MultiPlayerGameMode.interact, MultiPlayerGameMode.stopDestroyBlock, MultiPlayerGameMode.releaseUsingItem, MultiPlayerGameMode.piercingAttack and every container verb open no window at all.

  • Breaking progress. MultiPlayerGameMode.destroyProgress and its companions are a plain parallel clock with no sequence and no reconciliation; the crack overlay is written straight into the client level. Of the progress clock itself nothing is predicted — only the removals at either end of it are.
  • Item use. MultiPlayerGameMode.useItem opens a window, but a consumed item, a started use and a cooldown are not block states and the ack does nothing for them. They are corrected by other means: the living-entity flags in synched data, ClientboundCooldownPacket, and the menu resend the server performs when the stack changed.
  • Dropping. MultiPlayerGameMode.dropItem predicts the removal from the selected slot and sends its action packet with sequence zero — a local mutation with no rollback path at all.
  • Movement. Rubber-banding is a different mechanism entirely: an id-matched teleport handshake with no sequence and no ledger, described in input to movement. The two systems touch at one point in each direction — ClientPacketListener.handleMovePlayer calls BlockStatePredictionHandler.onTeleport, so a teleport disarms the ledger’s position snap until a receipt passes the teleport’s number, and the settle calls Entity.absSnapTo on the LocalPlayer when the restored block is inside you.

What one ack does, and the numbers that are not sequences

One receipt is not one action undone. Every entry at or below the acknowledged sequence is removed and handed to the settle, and there the paths part: write nothing, because the recorded state is already on screen (the correct prediction); write the state; or write the state and snap the player. A single ack can produce all three across the map in one pass — which is what the player-position recorded in each entry is for, and the only thing that reads it. BlockStatePredictionHandler.lastTeleportSequence is the exception that makes the snap coarse: it is compared against the acknowledged sequence rather than each entry’s, and is never reset. While the receipt is at or below the teleport’s number every snap in the batch is suppressed; once a receipt passes it, every snap is allowed, those of entries filed before the teleport included.

Zero is not a sequence. BlockStatePredictionHandler.startPredicting pre-increments, so the first real sequence is one and no genuine prediction is ever numbered zero. The three-argument ServerboundPlayerActionPacket constructor defaults the sequence to zero and aborting a dig and changing its direction use it, so the server dutifully sends a ClientboundBlockChangedAckPacket carrying zero — and it settles nothing.

Nothing expires. There is no timeout and no cap on the ledger: nothing clears it except a settle, and while the server considers the client not yet loaded, sequenced packets are dropped before the ack is recorded. So a wrong guess made then stands on screen until a later receipt settles it with everything below it. The only reset is a new ClientLevel.

And the two directions do not use the same flags. The predicted removal in MultiPlayerGameMode.destroyBlock — the write inside the four breaking rows of the window table — goes out with Block.UPDATE_IMMEDIATE in the mix; the restore comes back with Block.UPDATE_KNOWN_SHAPE. One cascades, the other deliberately does not, which is the four writes’ asymmetry stated in flags.

Questions players ask

Why does the block come back and then vanish again? Because the two progress clocks disagreed and the ack does not care — block breaking follows both clocks. What is this page’s is the consequence: the action is acknowledged whatever the server decided, so the entry settles, the stone comes back, and it vanishes again when the server’s own delayed destroy finishes. Releasing the mouse does not do this — that is MultiPlayerGameMode.stopDestroyBlock, which opens no window at all.

Why can I not right-click while mining? Because Minecraft.startUseItem is gated on MultiPlayerGameMode.isDestroying, one of the click gates block interaction lists. A spectator is stopped in the other direction, and the ledger treats the two halves of that asymmetrically: MultiPlayerGameMode.useItem returns early, before the window opens, while MultiPlayerGameMode.useItemOn returns inside it — so the sequence is burned and the packet is sent anyway.

Where to look

MultiPlayerGameMode.startPrediction — the whole client side is that one private method and its six call sites. BlockStatePredictionHandler end to end; it is short and every line of it matters, including BlockStatePredictionHandler.currentSequence, BlockStatePredictionHandler.isPredicting and BlockStatePredictionHandler.close. ClientLevel.setBlock, ClientLevel.setServerVerifiedBlockState, ClientLevel.handleBlockChangedAck and ClientLevel.syncBlockState for the four writes, in that order. Then the server’s half, which is smaller than the client’s: ServerGamePacketListenerImpl.ackBlockChangesUpTo — a method and, confusingly, a field of the same name, the field holding minus one when there is nothing to send — and the first statement of ServerGamePacketListenerImpl.tick, which is where it goes out.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Input and keybinds

Verified against Minecraft 26.3 · Part X · holding sneak: an SDL event, four chances to be swallowed, and a key that stays down while you are not touching it.

Turn on toggle sneak and hold the key. ToggleKeyMapping.setDown sees the press, flips the mapping to down, and then swallows the release entirely — so as far as the rest of the game is concerned you are still holding a key you let go of. Open your inventory and the mapping is released along with every other one; close it and the mapping comes back on, because the toggle remembered it was released by a screen rather than by you. Neither the flip nor the swallowed release involves the tick. The press and the release have both already happened by the time the tick that observes them runs, and the tick’s part is only to read what the mapping now says.

What the movement keys mean once they are down belongs to input to movement; this page stops at the mapping.

A key press is not queued

SDL events are not queued by the game. SDLEventHandler.pollEvents drains them from SDL’s own queue inside RenderSystem.pollEvents, which Minecraft.run calls immediately before Minecraft.runTick; SDLEventHandler hands the input events to their handlers through BlockableEventLoop.execute, but on the Render thread that call runs the task rather than queueing it — the qualification the client loop puts on the same method. Any description of Minecraft input that says a key press is “queued onto the client thread” is describing a different game.

The cast

classwhat it decidesthread
KeyboardHandlerthe key, character and pre-edit handlers, and four of the five gates a press runsRender thread
MouseHandlermotion accumulation, the sensitivity curve, and who is allowed to turn the playerRender thread
KeyMappingwhether a mapping is down, and how many clicks are owedRender thread
ToggleKeyMappingthe four mappings that can behave as toggles: sneak, sprint, use, attackRender thread
InputConstantsthe key universe, and the string a binding is saved asRender thread
InputQuirksfive platform constants that visibly change behaviourRender thread
KeyboardInputwhich of the seven movement mappings are down, once per tickRender thread
Guithe housekeeping at both ends of a screen’s lifeRender thread

Holding sneak

sequenceDiagram
    participant KH as KeyboardHandler
    participant MC as Minecraft
    participant Screen as Screen
    participant KM as KeyMapping
    participant KI as KeyboardInput
    rect rgba(0, 0, 0, 0.04)
    Note over KH,KI: inside RenderSystem.pollEvents, on this thread
    KH->>KH: keyPress — the SDL key event, run inline, never queued
    KH->>MC: handleGlobalKeyPress — fullscreen, screenshot, friends
    KH->>Screen: keyPressed — a screen that consumes it ends the story
    KH->>KH: Options.<br/>keyDebugModifier down? then handleDebugKeys
    KH->>KM: set — only with no screen open
    KM->>KM: ToggleKeyMapping.<br/>setDown — flips, and will ignore the release
    KH->>KM: click — counted, though nothing drains sneak's
    end
    rect rgba(0, 0, 0, 0.04)
    Note over KH,KI: the next client tick
    KI->>KI: tick, from LocalPlayer.aiStep — isDown on seven mappings
    KI->>KI: an Input record, then a normalised move vector
    end

Four gates, all in the top band; a drained press meets a fifth, the no-screen-no-overlay test Minecraft.tick makes before it drains the clicks, which sneak, read as held, never reaches. Everything in that band is one SDL event, the lower band is the next client tick, and nothing between them asked for the key.

A key press has five chances to be swallowed before it counts — the global-key check, an open screen, the debug modifier, the no-screen gate on recording, and the no-screen-no-overlay gate on draining, which a mapping read only as held, like sneak, never meets. And the two ends of a screen’s life are where the input system does its housekeeping: opening a screen releases every mapping, and closing one restores those toggles that asked to be restored — with default bindings, sneak and sprint, and only when there is a level.

Two ways gameplay reads a mapping, and they behave differently

KeyMapping.isDown is a boolean the movement code samples. KeyboardInput.tick reads seven of them — forward, back, left, right, jump, sneak, sprint — once per client tick, packs them into an Input record and derives a normalised move vector. Nothing is consumed; a key held for ten ticks reads down ten times.

KeyMapping.consumeClick is a drain, not an edge. It decrements a counter that KeyMapping.click incremented, which means presses that happened faster than the tick rate are not lost — and that binding two mappings to one key code fires both, because KeyMapping.click increments every mapping registered under that key. Minecraft.handleKeybinds is the drain, and it runs from Minecraft.tick only when there is neither a screen nor an overlay.

KeyMapping.matches and KeyMapping.matchesMouse are the third way, used where no counter is wanted: they test an event against the binding directly, which is what screens do, and what KeyboardHandler.handleDebugKeys does twenty times over as it works through the F3 combinations one test at a time. KeyMapping.same, KeyMapping.isDefault and KeyMapping.isUnbound are what the binding screen asks.

The bulk operations, and their single callers

Options.keyMappings is the array every mapping is reached through — the third of the three shapes options lists. Beside it KeyMapping keeps two static registries of every mapping ever constructed — KeyMapping.ALL by name, KeyMapping.MAP by key — which is how a key code is turned back into the mappings that want it. Five static operations walk them. Four of the five are called from exactly one place each, and those four are the interesting ones — the fifth, KeyMapping.resetMapping, rebuilds the key index from every mapping and has two callers, the binding screen and Options.load.

operationcalled fromwhy
KeyMapping.releaseAllGui.setScreen (GUI and screens)a screen may swallow a release, and a stuck-held mapping is worse than a lost press
KeyMapping.restoreToggleStatesOnScreenClosedGui.setScreenput back the toggles that the release above turned off
KeyMapping.resetToggleKeysLocalPlayer.respawnyou should not wake up sneaking
KeyMapping.setAllMouseHandler.grabMouseasks SDL which keys are physically down and sets each willing mapping to match — only where InputQuirks.RESTORE_KEY_STATE_AFTER_MOUSE_GRAB is set

The asymmetry between a swallowed press and a swallowed release is worth stating plainly, because it is the reason the first two rows exist. A press a screen swallows is harmless: the mapping was never set down. A release a screen swallows would leave the mapping down with nothing to clear it, which the first row prevents for every mapping set down before the screen opened.

The mouse: accumulate, apply, discard

MouseHandler.onMove, MouseHandler.onButton, MouseHandler.onScroll and MouseHandler.onDrop are the handlers; MouseHandler.accumulatedDX and MouseHandler.accumulatedDY are the pending motion; and MouseHandler.handleAccumulatedMovement applies it — from Minecraft.runTick, between the sound update and the frame, once per frame rather than once per tick. So a look is applied at frame rate and a step is applied at tick rate.

Accumulated motion goes to whatever is in front of it and is then cleared unconditionally. With a screen open the pointer goes to the screen’s move handler and the delta to its drag handler, and neither to the player — not because the two are exclusive in MouseHandler, which tests them separately, but because opening a screen released the mouse. With the window unfocused nothing accumulates in the first place; and the reset at the end runs either way, so motion is never banked.

MouseHandler.turnPlayer holds the sensitivity curve, and it has an arithmetic surprise in it. The curve is a cube of the slider, and the ordinary and smooth-camera paths multiply the result by eight while the scoped path does not — so aiming a spyglass is exactly eight times slower, by construction. The scoped path additionally requires the smooth camera to be off, the camera to be first-person, and the player to be scoping. Minimum sensitivity is not zero either: the cubed term is taken of the slider scaled and offset, so the slowest setting still turns.

MouseHandler.grabMouse and MouseHandler.releaseMouse are two directions of one edge with Gui.setScreen, guarded so the two cannot recurse — though only MouseHandler.releaseMouse has the single caller; MouseHandler.grabMouse has five, and on its way it also sets the current screen to none. MouseHandler.isMouseGrabbed is the state and MouseHandler.setIgnoreFirstMove suppresses the jump after a resize. A control-click is a right click only on macOS and only with Options.ctrlClickEmulatesRightClick on, which it is not by default: MacosUtil.setCtrlClickEmulatesRightClick hands the setting to SDL as a hint, and MouseHandler rewrites no click itself. Double-click is a threshold plus two identities: the two clicks must be within a quarter of a second, on the same button, and on the same screen instance.

The two debug-key families, and which one is bindable

Debug shortcuts look like one family and are two, which is why some of them can be rebound and some cannot. Options.debugKeys holds twenty-one ordinary KeyMappings, every one of them rebindable and every one of them listed by the binding screen; KeyboardHandler.handleDebugKeys tests twenty of them, one KeyMapping.matches apiece, and KeyboardHandler.keyPress tests the twenty-first, the crash key, with the overlay and modifier keys. The second family is not mappings at all: a raw switch on key codes in KeyboardHandler.handleChunkDebugKeys, gated on the game’s debug flag and bindable to nothing. Two of the bindable twenty-one, hitboxes and chunk borders, switch F3 entries that print no line and exist only to carry a flag something else reads — which is the seam between this page and the HUD, whose F3 entry registry is what those flags feed.

New categories are a third thing again. KeyMapping.Category is a registrable record rather than an enum, so a mod can add one; registering a duplicate id throws. Ordering is plain registration order into one list, and the eight built-ins come first only because the record’s own static initialiser registers them first.

What a key press sends, and from where

KeyboardHandler sends packets for two keys itself: ServerboundChangeGameModePacket for F3+N and, through DebugQueryHandler, a block-entity or entity tag query for the copy-recreate key. Every other press made outside a screen is drained by Minecraft.handleKeybinds, which sends the swap-offhand action itself and hands the others to the verbs that own them: an attack’s ServerboundPunchPacket, a use, a pick, a drop and a mount’s inventory each go out inside that same call, and a hotbar key’s slot goes out on the next tick from MultiPlayerGameMode.tick. A press a screen consumes sends what that screen sends. The movement keys reach the server only from the tick, as the input, movement, player-command and abilities packets LocalPlayer sends there. None of those verbs is this page’s.

Two smaller types sit beside the two handlers. ScrollWheelHandler is the wheel’s accumulator, one of which MouseHandler keeps, and InputType, which belongs to neither, is the four-valued “what did the player last use” that decides initial keyboard focus, narration timing and whether a focused widget shows its tooltip.

Questions players ask

I bound two things to one key and both happen. They will. Conflict detection lives only in the binding screen — ControlsScreen over KeyBindsScreen over the KeyBindsList that draws one row per mapping — and is purely cosmetic: nothing in the input path resolves a collision. It also refuses to flag one when both mappings are still at their defaults, which quietly exempts the pairs the game itself ships colliding.

Why does F3 toggle on release? F3 is a key binding, and the debug modifier and the overlay toggle are the same key by default — so the overlay can only fire on the release, and only when no combination was used in between. Rebind either and that behaviour disappears.

For a 1.21-era reader. Input events are records now. The (key, scancode, modifiers, action) integer tuple is gone from every screen signature, replaced by KeyEvent, MouseButtonEvent, CharacterEvent and PreeditEvent in client/input, with shared helpers on InputWithModifiers — so a widget asks an event whether it is a confirmation or a paste rather than decoding modifiers itself. Also gone: categories as translation-key strings, and MouseHandler.lastMouseEventTime.

Where to look

KeyboardHandler.keyPress — four of the five gates are one method, read top to bottom. Then ToggleKeyMapping end to end, which explains this page’s opening paragraph; Minecraft.handleKeybinds for the drain; KeyboardInput.tick for the other way a mapping is read; Gui.setScreen for the housekeeping at both ends of a screen; and MouseHandler.turnPlayer for the sensitivity curve and its three gates.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Options

Verified against Minecraft 26.3 · Part X · the render-distance slider: a value that takes effect on a delay, rebuilds the world on a later frame, and is sent to the server only when the options are next saved, usually as a screen closes — with no reply.

There is no settings-changed hook in the Minecraft client. Saving is the event system. The server learns your new view distance because something called Options.save, and Options.save is the only caller of Options.broadcastOptions — which sounds like a tight, tidy rule until you notice that every cycle-option button calls Options.save on click. Change chat visibility and your ClientInformation is on the wire before you have left the screen; drag the render-distance slider and nothing is sent until the next save, which is usually the screen closing.

This page is the policy: what a setting is, when a change takes effect, who finds out, and what the server does with the nine fields it is told. The bindings that also live on Options are input and keybinds.

The cast

classwhat it decidesthread
Optionsthe file, the fields, and when to saveRender thread
OptionInstanceone setting: value, codec, caption, listener, and the widgetRender thread
OptionInstance.ValueSetthe legal values — and, by its subtype, whether the widget saves on clickRender thread
ClientInformationthe nine-field record the server is told, kept in server/levelcrosses the wire
LevelExtractornotices, next frame, that the effective render distance changedRender thread
ChunkMapclamps the request and re-tracks, silentlyServer thread
IntegratedServerthe singleplayer back door: reads the sliders directly, every unpaused server tickServer thread

What happens when a setting changes

Four decisions stand between a widget and the wire, and none of them is “run the listener and be done”.

flowchart TD
    CHANGE["a widget changes a value"]
    KIND{"slider or cycle?"}
    IMMED{"applies immediately?"}
    ARM["arm 600 ms, and let the widget's extract pass notice it expire"]
    SET["OptionInstance.set"]
    RUNNING{"Minecraft.running?"}
    SILENT["assign the field, no equality test, no listener — loading"]
    LISTEN["the instance's own listener runs"]
    CHANGE --> KIND
    KIND -- "slider" --> IMMED
    IMMED -- "yes, as it moves" --> SET
    IMMED -- "no" --> ARM --> SET
    KIND -- "cycle, on click" --> SET
    SET --> RUNNING
    RUNNING -- "no" --> SILENT
    RUNNING -- "yes" --> LISTEN

Everything above OptionInstance.set is the widget’s business and everything below it is the value’s. Note the Minecraft.running gate: the same method that changes a setting is the one that loads the file, and on the loading path no listener runs at all.

Saving is the second half, and it is where the two families stop behaving alike.

flowchart TD
    CYCLE["a cycle button's click"]
    SLIDER["a slider's value, set or armed"]
    CSAVE["Options.save, from the button's own handler"]
    CLOSE["OptionsSubScreen.onClose — apply anything still armed"]
    REMOVED["OptionsSubScreen.removed — Options.save"]
    WRITE["options.txt written"]
    BCAST["Options.broadcastOptions — build a ClientInformation"]
    SAME{"identical to the last one sent?"}
    SEND["ServerboundClientInformationPacket"]
    NOTHING["nothing is sent"]
    CYCLE --> CSAVE --> WRITE
    SLIDER --> CLOSE --> REMOVED --> WRITE
    WRITE --> BCAST --> SAME
    SAME -- "yes" --> NOTHING
    SAME -- "no" --> SEND

Two entrances, one exit: a cycle saves on the click, so the packet is built before you have stopped looking at the button, and a slider waits for the next save, usually the screen coming down. The gate at the bottom is ClientPacketListener.broadcastClientInformation, which compares the new record with the last one sent and does nothing if they match.

The asymmetry is not a policy anybody wrote down. It comes from the value set’s subtype and from nothing else: OptionInstance.CycleableValueSet.createButton puts Options.save straight into the button’s click handler, and OptionInstance.SliderableValueSet has no such call anywhere, so a slider’s value reaches the field as it moves, or 600 ms after it stops, and then waits for the next save, usually the screen coming down. OptionsSubScreen.onClose applies anything still in an armed timer first, so leaving fast does not lose your drag.

The last gate is worth naming: ClientPacketListener.broadcastClientInformation holds the last ClientInformation it sent and compares. Save a screen without changing one of the nine fields and no packet leaves at all — except the first save of each play phase, whose new listener has nothing yet to compare against.

The three ways a setting is stored

OptionInstance is the modern shape — a value, a codec, an initial value, a caption, a listener, and an OptionInstance.ValueSet that decides both the legal values and the widget. The concrete sets are OptionInstance.IntRange, OptionInstance.ClampingLazyMaxIntRange, OptionInstance.UnitDouble, OptionInstance.Enum, OptionInstance.LazyEnum, OptionInstance.AltEnum and OptionInstance.SliderableEnum.

Plain fields are the older shape, read and written by name in Options.processOptions: the language code, the resource-pack lists, Options.tutorialStep, Options.advancedItemTooltips, Options.joinedFirstServer, Options.startedCleanly and ten others. They have no listener and no widget machinery at all — and Options.smoothCamera, a plain field a keybind toggles, is not in the file.

The key-mapping array is the third, and is the input page’s subject.

Options.processOptions is worth naming twice: it is the file format and the field list in one method, which is why it is the first place to look for anything about options.txt. Options.dumpOptionsForReport and Options.processDumpedOptions are the smaller subset that goes into the profiling report.

The delay, and why the world rebuilds anyway

The render-distance slider’s listener does exactly one thing: it marks the graphics preset custom. It does not invalidate a single chunk. It is also one of only three options in the game that defer their value at all — with simulation distance and biome blend, it is built to not apply immediately, which is what arms the 600 ms; every other slider applies as it moves.

The world rebuilds because LevelExtractor notices, on the next frame, that Options.getEffectiveRenderDistance differs from the last value it saw, and calls its own full invalidation — tint caches cleared, the tracker rebuilt, all geometry dirty. The setting and the consequence are joined by a poll, not by a callback.

Sixteen other settings carry a listener that touches the graphics preset, and they split cleanly. Seven of them also reach straight into the level extractor and invalidate something: cloud range, cutout leaves, improved transparency, ambient occlusion, anisotropic filtering, texture filtering and biome blend radius. Those seven really are the callback the render-distance slider is not. The other nine do exactly what render distance’s does and nothing else: flip Options.graphicsPreset back to custom through Options.setGraphicsPresetToCustom. That is why “Custom” appears without anyone selecting it — a preset writes a batch of settings at once, and every one of the sixteen, set by hand, undoes the preset’s name — though not when the preset writes its own batch, which Options.applyGraphicsPreset does with a flag raised that Options.setGraphicsPresetToCustom checks first.

Elsewhere in the file the immediate listeners are real enough, and the interesting ones are far more interesting than their options. GUI scale resizes whichever screen is open. Vsync invalidates the surface configuration, which is what the window is drawn into. Fullscreen toggles the window and then writes the option back from what the window actually did. The unicode-font toggle throws away every glyph atlas. High contrast adds and removes a resource pack.

The guard that silences settings loaded at startup

OptionInstance.set is not an unconditional write. It first asks whether Minecraft.running is true, and when the game is not running it assigns the field and skips both the equality test and the listener. That is not a special path for loading: it silences any set performed before that flag is raised, part-way through the Minecraft constructor, and loading happens to be the biggest thing that happens there.

The ordering is what makes it bite. Options are read from disk inside the Options constructor, which runs inside the Minecraft constructor, five statements before Minecraft.running is set true (the client loop has the rest of that sequence). So every value that comes out of options.txt is installed with its side effect suppressed — no atlas thrown away, no window resized, no preset marked custom — and the game instead arranges for the startup state to be right by other means. A setting whose only effect is in its listener is therefore a setting that does nothing until you change it in the interface.

Who is told, and what is not said

Options.buildPlayerInformation assembles a ClientInformation: language, view distance, chat visibility, chat colours, skin model parts, main hand, text filtering, listing permission, particle status. Nine fields, and simulation distance is not one of them — the record has no place to put it.

The packet is ServerboundClientInformationPacket, and it is a common packet rather than a play one: the first is sent during configuration, straight from ClientHandshakePacketListenerImpl, before the play phase exists. Every later one comes from Options.broadcastOptions.

There is no acknowledgement, and the absence is the point. What the packet provokes goes outward, and none of it answers you: a hat-visibility update broadcast to the whole player list, you included, and — because ServerPlayer.updateOptions writes two of the nine fields into synched data rather than into fields of its own — the skin-customisation byte and the main hand travel to you and everyone tracking you as synched entity data. The other seven are read off the ServerPlayer by whoever needs them. Nothing in any of it tells you what happened to what you asked for. The only thing that ever sets Options.serverRenderDistance is the server announcing its own view distance — in the login packet, or by broadcasting ClientboundSetChunkCacheRadiusPacket whenever that distance changes. Your request is clamped by ChunkMap.getPlayerViewDistance and used for chunk tracking, and you are never told what it was clamped to; the client clamps itself, with Options.getEffectiveRenderDistance. ClientboundSetSimulationDistancePacket is likewise an announcement, not a reply.

Singleplayer short-circuits half of it. IntegratedServer.tickServer reads both the simulation and render sliders off the client’s options every unpaused server tick and pushes them into the player list, so both drive the server directly without waiting for a packet. Render distance travels as client information anyway — it is field two of the record, sent over the memory connection exactly as over a socket — and simulation distance never does, in singleplayer or out of it, because the record has no place for it.

Questions players ask

Why does my render-distance slider stop short? Its maximum is computed once, from the JVM’s maximum memory, and the option is a plain OptionInstance.IntRange built around that bound. It depends on your heap, not on your graphics card. (The genuinely lazy OptionInstance.ClampingLazyMaxIntRange is GUI scale’s, and reads the window.)

Why did none of my settings’ side effects run at startup? Because Minecraft.running is still false. OptionInstance.set checks it and, when the game is not running, assigns the field and skips both the equality test and the listener. That is not a special path for loading — it silences any set performed before that flag is raised, part-way through the Minecraft constructor, and loading happens in the Options constructor, which runs inside the Minecraft constructor five statements before Minecraft.running is set.

Which settings really need a restart? One on the settings screens, and it says so. The graphics backend is compared against a snapshot taken at startup, and Options.isRestartRequiredToApplyVideoSettings is what the screen asks; exclusive fullscreen applies at once. Related: Options.startedCleanly is written false at startup and true once the game is up, so a crash during boot can drop the client back to a safe backend.

Why does the tutorial never come back? Options are per installation, never per world — including Options.tutorialStep. Once any world drives the tutorial to the end, no later world shows the toasts again, and an unrecognised step name silently reads as finished.

I edited options.txt and broke it. Why did nothing complain? Failure is quiet by design: a bad line is logged and skipped, an OptionInstance value that will not decode is logged and ignored and one that decodes but lies outside the option’s values is reset to the initial value, a bad number keeps the current one, and a plain boolean field that is not true reads as false. A malformed resource-pack list, or an option value that is not well-formed JSON, ends the load, and every option after it keeps its default. Nothing about a corrupt options.txt stops the game starting. The file carries a version line and is run through the data fixer on load.

Where to look

Options.processOptions for the settings table. OptionInstance.set for the listener guard that explains the loading behaviour, and OptionInstance.CycleableValueSet for the button that saves on click. Options.buildPlayerInformation for the nine fields the server hears. ChunkMap.getPlayerViewDistance for what the server does with the one that matters, and IntegratedServer.tickServer for the singleplayer back door.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

GUI and screens

Verified against Minecraft 26.3 · Part X · pressing E: a screen the server is not told about until you close it, opened onto a menu that was built when you spawned.

Press E in survival and no packet is sent, no packet is received, and nothing on the server changes. Player.inventoryMenu has existed since the player object was constructed, it has no MenuType at all, and MenuScreens could therefore never build an InventoryScreen from a packet even if one arrived. The opening is entirely a client-side event. Press E again and the symmetry breaks: LocalPlayer.closeContainer sends ServerboundContainerClosePacket, and the server empties your 2×2 crafting grid on the way out, returning or dropping what was in it. The screen the server is never told about is one the server is told about exactly once, at the end.

The menu underneath it is not symmetric in the same way, and the qualification matters: InventoryMenu is constructed on both sides, and its crafting result is recomputed only on the server — so the client is rendering a result it did not compute, in a screen the server does not know is open.

This page is what a screen is: the manager that holds one, the lifecycle it runs through, the widget and layout families it is built from, and the four routes by which one comes to exist. How its contents become pixels is the GUI render tree; how a Component becomes glyphs is text and fonts.

The cast

classwhat it decidesthread
Guiwhich screen and which overlay exist, and what an absent screen meansRender thread
Screenone screen’s lifecycle, children, focus and narrationRender thread
AbstractWidgetthe final outer shape of a widget, and one inner hook per subclassRender thread
Layout over LayoutElementwhere widgets end up, re-arranged on most screens whenever the window changesRender thread
AbstractContainerScreena screen mirroring a server-side menu, and the slot geometryRender thread
MenuScreensMenuType to screen class — the registry ClientboundOpenScreenPacket looks a screen up inRender thread
Overlaytakes the screen’s place in the record pass, and suppresses its mouse and its typing — but not its key pressesRender thread
ScreenNarrationCollectorwhat has already been said, so it is not said twiceRender thread

The objects, and what contains what

classDiagram
    class Gui {
        one per game
        Screen screen
        Overlay overlay
        Hud hud
    }
    class Screen {
        List~GuiEventListener~ children
        List~Renderable~ renderables
        List~NarratableEntry~ narratables
    }
    class AbstractWidget {
        usually in all three lists
    }
    class AbstractContainerScreen {
        AbstractContainerMenu menu
    }
    class Layout {
        arranges; most screens re-arrange it on resize
    }
    Gui *-- Screen
    Gui *-- Overlay
    Gui *-- Hud
    Screen *-- AbstractWidget
    Screen <|-- AbstractContainerScreen
    AbstractContainerScreen *-- AbstractContainerMenu
    Layout ..> AbstractWidget : positions

A diamond is containment and a hollow arrowhead is inheritance, which the flowchart this replaced could not tell apart: AbstractContainerScreen is a Screen, and everything else here holds what it points at. Layout is the dashed one because it arranges widgets the screen owns.

The three lists on Screen are the shape worth remembering: a widget added with Screen.addRenderableWidget joins all three, and the sibling add methods exist precisely so that something can be in one or two of them and not the rest. A Tooltip is in none of them — it is held by a WidgetTooltipHolder — and MultiLineLabel is an interface rather than a widget at all.

Gui, which is not the HUD

Gui owns Gui.screen and Gui.overlay — set through Gui.setScreen and Gui.setOverlay — plus Gui.hud, Gui.toastManager, Gui.chatListener, Gui.splashManager and the reference to the frame’s render state. It has three cadences, not two: Gui.tick once per client tick, Gui.update once per frame — which advances toasts and fires delayed narration — and Gui.extractRenderState once per frame in the record pass. The rest of its surface is Gui.isPausing, Gui.handleKeybinds (the share of the drain Minecraft.handleKeybinds hands it), Gui.openChatScreen, Gui.canInterruptScreen, Gui.buildInitialScreens and Gui.setClientLevelTeardownInProgress.

Three of its behaviours are the sort of thing a reader assumes and gets wrong.

Gui.setScreen with a null screen does not mean “close the screen”. It means “decide what should be up instead”. With no level it substitutes the title screen; with a dead player it substitutes the death screen, or respawns; otherwise it restores the chat screen if one was saved. During a level teardown it throws, rather than return you to a world that is being dismantled.

Gui.isPausing is the screen’s vote on whether the game stops, and it is one of three conjuncts: the client loop owns the pause itself and the other two, and the vote is cast by Screen.isPauseScreen, which defaults to true and which AbstractContainerScreen.isPauseScreen answers false. An overlay pauses by default too, which the loop page does not say, and is why a resource reload stops a singleplayer world as surely as the options screen does.

An overlay does not stack on a screen: in Gui’s record pass it replaces it. Only LoadingOverlay itself records the screen beneath it, while it fades in and out, and the input rules are split — an overlay suppresses the screen’s mouse and its typing, but not its key presses. LoadingOverlay is the only implementation of Overlay in the game.

The lifecycle, and what is final

There are two methods called Screen.init and the difference between them is the lifecycle. The one that takes a width and a height is final — the framework’s entry point, which nothing overrides — and the no-argument one is the hook a screen implements to build its widgets. Wherever this page says the Screen.init hook it means the second.

A resize goes through Screen.resize to Screen.repositionElements. The default Screen.repositionElements rebuilds every widget through Screen.rebuildWidgets, which re-enters the hook — so on a plain screen everything really is rebuilt. Forty-one screens override Screen.repositionElements instead and keep their widgets, most of them just re-arranging their Layout. “Everything is rebuilt on resize” is true of a plain screen and false of most interesting ones.

The rest of the lifecycle is Screen.added, Screen.tick, Screen.removed and Screen.onClose, and the record entry point is the final Screen.extractRenderStateWithTooltipAndSubtitles.

That pairing is the framework’s whole style: fix the outer shape, hand the subclass one inner hook. Screen.init’s two-argument form, Screen.extractRenderStateWithTooltipAndSubtitles, AbstractWidget.extractRenderState, AbstractWidget.updateNarration, AbstractButton.extractWidgetRenderState and AbstractContainerScreen.tick are all final; AbstractWidget.extractWidgetRenderState and AbstractButton.extractContents are the hooks they leave open. The widget family under them is Button, EditBox, Checkbox, CycleButton, AbstractScrollArea and the selection lists over it — AbstractSelectionList, ObjectSelectionList, ContainerObjectSelectionList and OptionsList.

Layout is Layout over LayoutElement: LinearLayout, GridLayout, FrameLayout, EqualSpacingLayout, HeaderAndFooterLayout and SpacerElement, configured by LayoutSettings and resolved by Layout.arrangeElements and Layout.visitWidgets. Input arrives as the client/input records — KeyEvent, MouseButtonEvent and their siblings, which input and keybinds owns — through GuiEventListener and ContainerEventHandler; focus is a ComponentPath moved by a FocusNavigationEvent, ordered by TabOrderedElement.getTabOrderGroup; and geometry is ScreenRectangle, ScreenPosition, ScreenAxis and ScreenDirection.

One consequence of doing all this in a record pass: AbstractWidget.extractRenderState computes hovered as “inside my rectangle and inside the current scissor”, so a widget scrolled out of a list does not light up.

Pressing E

sequenceDiagram
    participant KH as KeyboardHandler
    participant MC as Minecraft
    participant Gui as Gui
    participant InvS as InventoryScreen
    participant MPGM as MultiPlayer<br/>GameMode

    rect rgba(0, 0, 0, 0.04)
    Note over KH,MPGM: the key press, then the tick that spends it
    KH->>KH: keyPress — no screen open, so the mapping records a click
    MC->>MC: handleKeybinds, with no screen and no overlay
    MC->>MPGM: isServerControlledInventory — false on foot
    MC->>Gui: setScreen — a new InventoryScreen, and the input housekeeping
    Gui->>InvS: added, then init
    InvS->>InvS: init — creative? replace myself with<br/>CreativeModeInventoryScreen
    end
    rect rgba(0, 0, 0, 0.04)
    Note over KH,MPGM: the next frame, recording
    Gui->>InvS: extractRenderState<br/>WithTooltipAndSubtitles
    InvS->>InvS: extractBackground — a gradient, no blur, no panorama
    InvS->>InvS: extractContents, then labels, slots, highlight, carried item
    end

Two bands: everything that makes the screen exist happens in the tick that spends the key, and nothing is recorded until the frame that follows it. Gui.setScreen’s housekeeping — the mouse ungrabbed, every mapping released, sneak with them, held or toggled — runs before Screen.init.

The busiest screen in the game is AbstractContainerScreen, and its record pass is worth following once: AbstractContainerScreen.extractContents draws the widget list, translates to the container origin, and runs AbstractContainerScreen.extractLabels, AbstractContainerScreen.extractSlots and the two slot-highlight passes; then AbstractContainerScreen.extractCarriedItem, then AbstractContainerScreen.extractTooltip. It holds AbstractContainerScreen.menu, AbstractContainerScreen.leftPos, AbstractContainerScreen.topPos, AbstractContainerScreen.hoveredSlot and the quick-craft state, and a click goes AbstractContainerScreen.slotClicked to MultiPlayerGameMode.handleContainerInput — see containers and menus, which also owns the fact this page’s second paragraph turns on: the client never computes a crafting result.

The final AbstractContainerScreen.tick closes the container when the player is dead or removed. The client notices first.

Who opens a screen

routeexamples
entirely client-sideTitleScreen, PauseScreen, OptionsScreen, ChatScreen, social interactions, the survival and creative inventories
ClientboundOpenScreenPacketevery menu with a MenuType — chests, furnaces, anvils, and a chest boat
ClientboundMountScreenOpenPacketa horse’s or a nautilus’s own inventory
other packetsBookViewScreen, AbstractSignEditScreen, DeathScreen, WinScreen, the demo popup, LevelLoadingScreen, dialogs

Three entities implement HasCustomInventoryScreen and they do not agree about which packet to use: AbstractHorse and AbstractNautilus send the mount packet, while AbstractChestBoat opens an ordinary menu like any other container.

Every screen in that table is an example, and the book keeps them that way. There are two hundred-odd classes in client/gui/screens and its eighteen sub-packages — the world-creation flow, the pack picker, the report screens, the friends and social lists, the recipe book, a class per container — and they are the single largest thing in Part X by line count. Every one of them is this section’s four routes, the lifecycle above and the widget and layout families above, and nothing else; the book explains the pattern once, here, and names a screen only where some other page’s scenario walks into it. The exception it does not cover is player reporting, declined for its own reasons in what this book skips.

The screens you see first are a chain rather than a screen. Gui.buildInitialScreens composes accessibility onboarding, ban notices, a forced name change and a banned-skin notice ahead of the title screen or a quick-play launch. And Minecraft.setScreenAndShow sets a screen and then renders one frame on the spot — synchronously — which is how progress appears during blocking main-thread work such as a world load; a backup or a file fix shows its screen the same way and then runs on the worker pool.

Narration, finally, is mostly timed rather than immediate — mostly, because Screen.init narrates the new screen at once before arming anything. What does the speaking is GameNarrator, a thin wrapper over Mojang’s text2speech library with a four-valued NarratorStatus option in front of it and two tempers: the queued methods, which yield to whatever is already being said, and GameNarrator.saySystemNow, which interrupts. Thereafter Screen.handleDelayedNarration fires from Gui.update once two clocks have both run out: a next narration time, armed longer after a mouse move than after a keyboard action, and a suppression time, armed by the two-argument Screen.init on every Gui.setScreen — two seconds ahead, or, when one screen replaces another after Tab or arrow-key navigation, until the next key press or click. Three intervals, two clocks. When both have passed it picks a single widget to narrate, by tab-order group and priority.

For a 1.21-era reader. Minecraft.screen is gone: the current screen belongs to Gui, which is now the screen-and-overlay manager rather than the HUD — the HUD is Hud, reached as Gui.hud. Gone with it: Minecraft.setScreen, Screen.render and renderBackground, AbstractContainerScreen.renderBg / renderLabels / renderSlot, AbstractWidget.renderWidget, ClickType (now ContainerInput) and MultiPlayerGameMode.handleInventoryMouseClick (now MultiPlayerGameMode.handleContainerInput). A screen no longer draws — it records.

Where to look

Gui.setScreen — the substitution tree, and the input housekeeping at both ends of a screen’s life that input and keybinds explains. Screen.init and Screen.resize for the lifecycle, Screen.extractRenderStateWithTooltipAndSubtitles for the record pass, and Gui.extractRenderState for the frame’s contributor order. AbstractContainerScreen.extractContents for the busiest screen in the game, and MenuScreens for the screen registry the menu types use — DialogScreens is another.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The GUI render tree

Verified against Minecraft 26.3 · Part X · a chest full of the same item: how the 2D UI decides what is in front of what, without anybody ever saying so.

Nothing in the client’s 2D UI draws anything. Every call a screen, a widget or the HUD makes appends a render state to a tree; later in the same frame a second pass resolves that tree, sorts it, batches it and issues the draws. And the tree is not told what order to put things in. Layering is inferred from bounding boxes: a new element goes above the highest existing element whose box it intersects, and two elements that do not overlap can share a node and be reordered freely by the batching sort. There is no Z value, no layer index, and no declared order beyond the call order and two explicit barriers.

That inference is what buys the cheapness of a chest full of identical stacks, and it buys it twice over. Because two elements that do not overlap may share a node, the fifty-odd slots of a chest land in the same layer, and a layer’s element list is free to be reordered — so they can be sorted together and issued as one draw call instead of fifty. And because each distinct item model that is neither animated nor oversized is rendered into a dynamic atlas once and reused for as long as it stays resident, those fifty slots between them cost one 3D render ever, not one per frame and not one per stack; what they then draw is a flat rectangle of that atlas, which is exactly the kind of thing the sort can batch. The model being rendered belongs to models and atlases, and what records into this tree in the first place — screens and widgets — is GUI and screens, the lecture before this one.

The cast

classwhat it decidesthread
GuiGraphicsExtractorwhat a screen is handed — every drawing verb, and the scissor stackRender thread
GuiRenderStatethe tree: strata, nodes, and where a new element belongsRender thread
GuiRenderState.Nodeone layer: five lists, of which only the element list is ever sortedRender thread
GuiElementRenderStateone blit, rectangle or glyph: the kind the mesh builder batchesRender thread
GuiRendererresolving, sorting, coalescing and issuing the drawsRender thread
GuiItemAtlaswhich item models are already rendered, and which give up their slotRender thread
GameRenderStatewho actually owns the tree — not Gui; the frame’s own snapshot (the frame)Render thread

The tree, and where a new element lands

flowchart TD
    NEW["a new element arrives"]
    NONE{"has bounds?"}
    DROP["silently discarded"]
    FAST{"does the previous element's box contain it?"}
    NEW --> NONE
    NONE -- "no" --> DROP
    NONE -- "yes" --> FAST
    subgraph GRS["GuiRenderState"]
        subgraph S0["stratum 0, closed"]
            N0["Node"]
        end
        subgraph S1["stratum 1, the floor"]
            N1["Node"]
            N2["Node, above it"]
            N1 --> N2
        end
    end
    FAST -- "yes: straight up, no intersection test" --> N2
    FAST -- "no: up to just above the highest box it touches" --> N2

GuiRenderState is a list of strata and each stratum a chain of nodes. Both answers land in the tree, and neither can land below the barrier: a GuiRenderState.nextStratum call closes stratum 0 to every later recording, so the worst the search can do is reach the bottom node of the current one.

Three consequences fall straight out of that picture.

An element with no bounds is silently discarded. Every recording add verb is conditional on the tree finding a node, and finding a node requires bounds. Glyph states deliberately have none — which is exactly why they are added through the layer-bypassing verb, GuiRenderState.addGlyphToCurrentLayer, which the draw pass calls rather than the record pass, and emitted after their node’s geometry. Glyphs are never sorted, and that is the whole mechanism behind “text draws on top of its own background”.

The search never descends below the current stratum. A stratum is a floor the layering search may not go under: opening one declares that everything recorded from now on sits above everything recorded so far, whatever the bounding boxes say. That is what makes GuiRenderState.nextStratum — reached by a recorder as GuiGraphicsExtractor.nextStratum, which is the same barrier under the name the caller sees — a hard rule rather than a hint. It is how the HUD keeps the hotbar block out of the crosshair’s layering, and how chat stays clear of the scoreboard.

The fast path is the common case. If the previous element’s box contains the new one — a label inside a button, a sprite inside a slot — it goes straight up with no test.

The tree’s verbs sort into three kinds, and the third kind is the one that catches people out: two of them are not recording verbs at all, and are called by the resolve pass rather than by a screen.

verbwho calls itwhat lands in the node
GuiRenderState.addGuiElementa recorderBlitRenderState, TiledBlitRenderState, ColoredRectangleRenderState
GuiRenderState.addTexta recorderGuiTextRenderState
GuiRenderState.addItema recorderGuiItemRenderState
GuiRenderState.addPicturesInPictureStatea recorderfive of the PictureInPictureRenderState family, below
GuiRenderState.nextStratuma recordernothing — a barrier
GuiRenderState.blurBeforeThisStratuma recordernothing — a barrier
GuiRenderState.addBlitToCurrentLayerGuiRenderer and PictureInPictureRenderer, in the resolve passthe flat rectangle an item or a picture-in-picture resolved to
GuiRenderState.addGlyphToCurrentLayerGuiRenderer, in the resolve passGlyphRenderState

The last two bypass the layering search entirely and append to the node their source element already chose, which is the whole reason a resolved thing never jumps in front of what recorded it. PanoramaRenderState has no verb at all: the title screen’s spinning backdrop is a nullable field on GuiRenderState, assigned by Panorama and drawn by GuiRenderer.render before the tree is resolved — so it is under everything by construction rather than by layering.

The PictureInPictureRenderState family is six — GuiEntityRenderState, GuiSkinRenderState, GuiBookModelRenderState, GuiBannerResultRenderState, GuiProfilerChartRenderState and OversizedItemRenderState — and each has a matching PictureInPictureRenderer subclass in client/gui/render/pip that resolves it into a texture during GuiRenderer.prepare. Five are recorded; the sixth, OversizedItemRenderState, is built by the resolve pass itself for an item drawn larger than its slot, and has no verb either. Those six, the item atlas and the panorama’s cube are where 3D drawing happens inside a 2D pass: the entity in an inventory that turns to follow the mouse, the skin on the skin-report screen, the enchanting-table book, and every item model in every slot.

The draw pass

Two phases, one thread, one frame. It is a data split rather than a thread split: the record phase touches game state, the draw phase touches the recorded objects and the GPU.

flowchart TD
    REC["Gui.extractRenderState — the contributors record"]
    subgraph RENDER["GuiRenderer.render"]
        subgraph PREP["GuiRenderer.prepare"]
            PIP["pictures-in-picture, to textures"]
            ITEM["items, into GuiItemAtlas"]
            TEXT["text, expanded per glyph"]
            SORT["GuiRenderState.sortElements"]
            MESH["GuiRenderer.addElementToMesh, coalescing Draws"]
            PIP --> ITEM --> TEXT --> SORT --> MESH
        end
        UP["the vertex buffer uploaded"]
        subgraph DRAW["GuiRenderer.draw"]
            BEFORE["everything before the blur"]
            BLUR["clear depth, run the post effect"]
            AFTER["everything after"]
            BEFORE --> BLUR --> AFTER
        end
        MESH --> UP --> BEFORE
    end
    END["GuiRenderer.endFrame — GameRenderer's call"]
    REC --> PIP
    AFTER --> END

The nesting is the point: the five steps in the inner box all happen inside GuiRenderer.prepare, not after it, and GuiRenderer.draw is its sibling rather than its successor. Only Gui.extractRenderState at the top and GuiRenderer.endFrame at the foot are outside GuiRenderer.render.

One thing happens eagerly during recording that looks like it should not: adding text forces the text to be prepared, because the tree needs its bounds to place it. Only the expansion into per-glyph states waits for the draw pass — see text and fonts.

There is one sort comparator, not three. GuiRenderer.ELEMENT_SORT_COMPARATOR is the whole key, and it is built from three, in order: GuiRenderer.SCISSOR_COMPARATOR first, then the pipeline’s own sort key, then GuiRenderer.TEXTURE_COMPARATOR. That is what turns a node’s element list into as few GuiRenderer.Draws as possible, because the mesh builder starts a new one only when the next element disagrees about one of those three, or the blur boundary falls between them. Both the sort and the coalescing happen inside GuiRenderer.prepare; GuiRenderer.draw replays the list they produced, splitting it at the blur, and GuiRenderer.render is the outer method that calls the two in turn.

Blur is a barrier, and it is fussy

GuiRenderState.blurBeforeThisStratum splits the draw list in two, and GuiRenderer.draw then draws everything before the boundary, clears the depth buffer, runs the blur chain over the result — world and GUI alike, which belongs to post-processing — and draws the rest crisp on top. What the tree decides is where the boundary goes and whether there is one at all.

Screen.extractBlurredBackground is the one thing that asks, and inside it there is exactly one condition: the menu-background blurriness option must be at least one, which is why a slider at zero costs nothing at all rather than blurring by a little. Two further facts are about reaching it. Screens that declare themselves in-game UI — container screens, sign editors, book screens — take the transparent-background path and never call it, which is why the pause menu blurs the world and a chest does not. And a second call in one frame is a bug rather than a second blur: GuiRenderState.blurBeforeThisStratum throws when the boundary has already been set. The darkening tint over a blurred menu is itself sharp, because the boundary falls before the whole stratum it is recorded in.

When the atlas costs something, and what the extractor keeps

The residency the opening leans on is not free, and its failures are worth knowing because they are what a slow inventory screen looks like. A slot that is stale or was never filled is redrawn with no invalidation involved, and animated models are exempt from residency altogether — they are redrawn every frame. Wholesale invalidation is the loud case: changing the GUI scale throws the atlas away, and so does growing it, after which every model the frame draws is rendered again; an atlas that cannot grow, already at the largest texture the device allows, logs that some items will be skipped rather than failing. No slot ages out: GuiRenderer.endFrame, which GameRenderer calls after the frame, frees only the animated models’ slots, and otherwise a resident model gives up its slot only when a later frame’s models do not fit beside it, and then DynamicAtlasAllocator, the grid of equal slots behind GuiItemAtlas, frees slots of models that frame does not draw to make room.

The extractor, for its part, is not quite the pure function its name promises. It keeps real state and makes one side effect of its own, and neither is a draw: the pose and scissor stacks are state, and the cursor shape requested during the record pass is applied to the window at the end of it. It also holds the deferred tooltip and the IMEPreeditOverlay until the screen has recorded everything else, then records each in a stratum of its own, which is how both land above everything the screen drew.

And the tree outlives all of it, because it is not Gui’s. Gui holds a reference to one that belongs to GameRenderState, so the tree is reachable from the frame rather than from the interface — which is also how two fields recorded here come to change the frame: the HUD owns that traffic and both of its directions. If you ever need to prove the batching sort is safe, two debug switches exist for it: one draws a translucent tint over every element’s box, in the layer above it, and the other shuffles each node’s element list and re-seeds the sort keys, to shake out accidental order dependence.

For a 1.21-era reader. There is no GuiGraphics. The class is GuiGraphicsExtractor, and the name is the whole design — it extracts, it does not paint. LayeredDraw is gone too: ordering is inferred from the boxes and the call order, plus explicit barriers. GuiGraphics.renderTooltip is gone, and so is PoseStack in 2D GUI code — the GUI transform is a 2D affine stack now, though a real PoseStack still lives inside the item atlas and the picture-in-picture renderers, where actual 3D models are drawn.

Where to look

GuiRenderState.nextStratum and the node-placement logic beside it — the layering rule is short and explains most of the UI’s behaviour. GuiGraphicsExtractor for what a screen is handed. GuiRenderer.prepare for where items, text and picture-in-picture content are resolved and the draws are batched, and GuiRenderer.draw for the blur split.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Text and fonts

Verified against Minecraft 26.3 · Part X · a chat line in a language whose characters are not in the default font: six stages from a Component to a quad, one of which uploads a texture while pretending to measure.

Ask the client how wide a piece of text is and it will stitch a glyph into a texture sheet and upload it to the GPU. Font.width resolves each codepoint to a baked glyph and reads its advance — so measuring a codepoint nobody has drawn yet is neither free nor read-only, and a layout pass over a never-before-seen script is a series of texture uploads wearing arithmetic’s clothes.

This page is the pipeline that ends in that glyph: flattening a component tree into styled runs, measuring and wrapping it, reordering it for bidirectional scripts, choosing a glyph for every codepoint from a chain of providers, baking it the first time anyone asks, and emitting one renderable per glyph for the draw pass to turn into vertices. What a Component is — its contents kinds, its Style, how it is serialised — is Part II’s text components, and how a chat message is signed is chat and signing. This page starts from “you have one”.

The cast

classwhat it decidesthread
StringDecomposerwhere one styled run ends and the next begins, and what a section sign meansRender thread
StringSplitterwidth, line breaks and word boundaries, given a width providerRender thread
FormattedBidiReordervisual order, by handing the flattened text to ICURender thread
FontManagerwhich GlyphSource a FontDescription resolves to — three different waysworker pool, then Render thread
FontSetthe provider chain for one font id, its codepoint cache, and the fishy splitRender thread
GlyphStitcherwhich FontTexture sheet a bitmap goes to — the sheet places and uploads itRender thread
Fontthe per-codepoint loop, and the Font.PreparedText everything else walksRender thread
GuiRendererexpanding prepared text into one GlyphRenderState per glyphRender thread

The six stages

flowchart TD
    C["a Component"]
    subgraph WHEN["when the text is laid out"]
        RUNS["1 · flatten — Component.visit, then Style.applyTo"]
        DEC["StringDecomposer, codepoint by codepoint"]
        WRAP["2 · measure and wrap — StringSplitter"]
        BIDI["3 · reorder — Language.getVisualOrder, ICU"]
        RUNS --> DEC --> WRAP --> BIDI
    end
    subgraph PREP["inside Font.prepareText"]
        RESOLVE["4 · resolve — FontManager, then the first provider"]
        BAKE["5 · bake — first sight only, into a FontTexture"]
        EMIT["6 · emit — a Font.PreparedText of TextRenderables"]
        RESOLVE --> BAKE --> EMIT
    end
    GUI["at draw: GuiRenderer expands it into GlyphRenderState, or TextFeatureRenderer walks it for the world"]
    WORLD["or SubmitNodeCollection.submitText, for the world"]
    C --> RUNS
    BIDI --> RESOLVE
    WRAP -. "measuring resolves and bakes it too" .-> RESOLVE
    EMIT --> GUI
    BIDI --> WORLD
    WORLD -. "TextFeatureRenderer runs four to six at draw" .-> RESOLVE

The numbers are the order the stages matter in, not one walk: for a chat line the top box runs when the line arrives and whenever the chat re-wraps, the bottom box on every record pass for the GUI and at draw time for text in the world, and the first dotted edge is why measuring a string you never draw still uploads its glyphs. In the GUI only what happens after stage six waits for the draw pass.

The numbering is the order the stages matter in, not a pipeline anything walks straight through. Stages one to three run when a chat line arrives (some callers run them on every frame), and four to six run inside Font.prepareText, which the GUI calls during the record pass — the render tree explains why, and why nothing is left for the draw pass but the expansion of the finished Font.PreparedText into one GlyphRenderState a glyph. But four and five are also reached from two: the width function Font hands its StringSplitter asks the glyph source for each codepoint, and that call resolves the provider and forces the bake. Measuring a string you never draw still uploads its glyphs.

All of it is on the Render thread, glyph baking and GPU uploads included. The only work that leaves the thread is loading, and it is an ordinary resource reload with one unusual step: FontManager’s prepare phase parses the font definitions, loads each provider, resolves references between them, and then pre-warms the chain by asking it for every codepoint but the space that any provider claims, on the worker pool, each answered by the first provider that has it — so a TrueType font’s glyphs are loaded off the Render thread before the apply phase asks for every one of them again to build the by-width table. Baking still waits for first use, except for the missing box and the white effect glyph, which every reload bakes at once. The apply phase — closing the old font sets and building new ones — is back on the Render thread.

1 · Flatten

Component and FormattedText provide the walk: Component.visit yields (style, string) runs in logical order, applying Style.applyTo down the sibling tree. StringDecomposer iterates those runs codepoint by codepoint and is where a legacy section-sign colour code is interpreted. FormattedCharSequence is the end product — a one-method interface that pushes (index, style, codepoint) triples at a FormattedCharSink — and ComponentCollector reassembles pieces back into formatted text.

2 · Measure and wrap

StringSplitter owns width and line breaking: StringSplitter.stringWidth, StringSplitter.splitLines, StringSplitter.headByWidth, StringSplitter.findLineBreak and StringSplitter.getWordPosition. It works against a StringSplitter.WidthProvider, and on the real Font that provider is what bakes. Font.width, Font.split, Font.splitIgnoringLanguage, Font.wordWrapHeight and Font.getSplitter are the public face of it.

Wrapping preserves styles rather than ignoring them. It cuts at a character offset, captures the style in force at the cut, and re-applies it to the continuation — which is why a colour code before a wrap point still colours the line after it. And Font.split reorders while Font.splitIgnoringLanguage does not — anything that will re-wrap must use the second.

3 · Reorder

Language.getVisualOrder is the entry point; on the client ClientLanguage implements it with FormattedBidiReorder, which builds a SubStringSource — the flattened plain text plus one Style per character — runs ICU’s Arabic shaping and bidi algorithm over it, and re-emits each run through SubStringSource.substring. MutableComponent.getVisualOrderText caches the result against the identity of the current Language. Font.bidirectionalShaping is a separate, much smaller thing: it shapes and reorders a bare string, and beside Font.prepareText’s own use of it the sign editor is the only caller. Those two are half of the game’s ICU surface; the other two are not text layout at all — CreateBuffetWorldScreen and the item property LocalTime each use it for collation and calendars.

4 · Resolve

FontManager is the reload listener and the resolver: Font.Provider asks it for a GlyphSource given a FontDescription, and it branches once per kind of description — three of them.

A FontDescription.Resource resolves to a FontSet — the per-font-id object holding the provider list, the codepoint cache (CodepointMap), a GlyphStitcher and the by-width table used for obfuscation. A FontDescription.AtlasSprite or FontDescription.PlayerSprite resolves instead to a SingleSpriteSource from AtlasGlyphProvider or PlayerGlyphProvider — a one-glyph font that returns the same sprite for every codepoint, with no texture sheet of its own; only an atlas id nobody knows falls back to the missing-font set.

Below that: GlyphProvider implementations chosen by GlyphProviderType, declared in font/ JSON as GlyphProviderDefinitions. Five kinds, and their definitions are worth naming because the differences between them are visible on screen: BitmapProvider slices a PNG on a grid, TrueTypeGlyphProviderDefinition runs FreeType over a real font file (FreeTypeUtil is the wrapper), UnihexProvider reads the compact hex format the fallback font ships in, SpaceProvider produces advance and no pixels, and ProviderReferenceDefinition splices another font’s providers into this one’s chain. Beside them SpecialGlyphs is not from a file at all: it is the hard-coded missing-glyph box and the white square everything else draws effects with.

5 · Bake

A provider returns an UnbakedGlyph, whose GlyphBitmap is handed to GlyphStitcher.stitch, placed into a FontTexture sheet and uploaded, producing a BakedGlyph. BakedGlyph is an interface; the sheet implementation is BakedSheetGlyph, and EffectGlyph covers the solid quads used for underlines and backgrounds.

There is one glyph atlas family per font id, each with its own stitcher, and colour and greyscale glyphs never share a sheet. The atlas is discarded, never compacted: a resource reload throws it away — and so does toggling the force-unicode or Japanese-variants option, which rebuilds every font set with no reload at all.

6 · Emit

What comes out is a TextRenderable per glyph inside a Font.PreparedText. In the GUI, GuiGraphicsExtractor.text and its siblings build a GuiTextRenderState that GuiRenderer later expands into GlyphRenderStates. In the world, SubmitNodeCollection.submitText and SubmitNodeCollection.submitNameTag both hand a FormattedCharSequence to TextFeatureRenderer, which runs Font.prepareText itself at draw, and GlyphRenderTypes.select picks between the normal, see-through and polygon-offset render types by Font.DisplayMode.

A chat line, through all six

sequenceDiagram
    participant ChatC as ChatComponent
    participant CRU as Component<br/>RenderUtils
    participant SSpl as StringSplitter
    participant FBR as Formatted<br/>BidiReorder

    ChatC->>CRU: wrapComponents — the chat width over the chat scale
    CRU->>SSpl: splitLines — over runs already translated
    SSpl->>SSpl: splitLines cuts at a char offset, each run keeping its Style
    CRU->>FBR: reorder, reached through Language.getVisualOrder
    FBR->>FBR: SubStringSource, ICU bidi, one substring per run

Stages one to three, when the message arrives: four objects, and the measuring has already baked the glyphs it measured (an Arabic line’s shaped forms wait for stage five). ComponentRenderUtils reaches FormattedBidiReorder through Language.getVisualOrder, which ClientLanguage implements.

When the frame is next recorded, the same line goes through four other objects.

sequenceDiagram
    participant ChatC as ChatComponent
    participant Font as Font
    participant FSet as FontSet
    participant GStit as GlyphStitcher
    participant GuiR as GuiRenderer

    rect rgba(0, 0, 0, 0.04)
    Note over ChatC,GuiR: recording the frame
    ChatC->>Font: prepareText, via GuiTextRenderState asking its bounds
    loop each codepoint
        Font->>FSet: getGlyph — the first provider that has it
        FSet->>GStit: first sight only: stitch it and upload
        Font->>Font: a TextRenderable, effects, advance the pen
    end
    end
    rect rgba(0, 0, 0, 0.04)
    Note over ChatC,GuiR: drawing the same frame
    GuiR->>GuiR: walks the Font.PreparedText with a Font.GlyphVisitor
    GuiR->>GuiR: one GlyphRenderState a glyph — shadow, bold and italic belong to BakedSheetGlyph.<br/>renderChar
    end

Stages four to six, then what happens to their result: the two bands are the record pass and the draw pass of one frame, and GlyphStitcher appears in the first only for a glyph the measuring did not bake, such as a shaped form, because a glyph is baked once per font set, until a reload or a font option starts it again.

Two moments beyond the baking are worth pausing on. Translation is lazy and cached on the TranslatableContents itself, so the first thing to visit a message is what translates it — for a chat line, usually the chat listener (a system message’s sender guess, a signed message’s trust check), otherwise the log line ChatComponent writes as it adds the message. And the continuation indent on a wrapped chat line is a literal space codepoint prepended by ComponentRenderUtils — one of the places the pipeline invents a character rather than deriving it, beside a clipped line’s ellipsis and an object component’s replacement character.

Style is not a font, and the box is four failures

Two things the pipeline does not do are worth stating before the page ends, because both are assumed the other way round by nearly everyone.

Bold is not a font. Bold draws the glyph twice with a small offset and thickens it; italic shears the top and bottom edges. Nothing anywhere in the six stages knows what a bold face is. Shadow is a colour rather than a boolean, and zero means none. Underlines, strikethroughs and text backgrounds always come from the default font whatever the style names, because the effect glyph is looked up separately from the text’s. Obfuscation is in the same family: the glyph is swapped for a random one of the same width, from a by-width table rebuilt with the font set, which is why obfuscated text does not shift a layout unless the chain has fractional advances or, in bold, mixes providers of different bold offsets — and the swap happens inside Font.prepareText, which already runs once per frame, though each glyph it swaps in is baked the first time it is chosen.

A sprite is a font. A block icon or a player head in a chat line is, mechanically, one character in a font with one character in it: an object component emits a single object replacement character with a synthetic font description, which resolves through stage four to a one-glyph sprite source. The plain-text walk of the same component yields a bracketed fallback string instead, which is what the narrator and Component.getString see. A data pack cannot reach this: the codec behind FontDescription encodes only the resource kind, so a style can never name a sprite font, and the sprite descriptions arise only from object contents.

And when the pipeline fails it fails one way, which makes the hollow box ambiguous. Four different causes produce it: no provider has the codepoint; the font id is unknown, so the whole font set is the missing-font set; the glyph’s advance is fishy — outside a sane range — and the caller asked for the filtered font; or the bitmap fit no sheet. The fishy case is the one with machinery behind it. Every font set stores two suppliers per codepoint because of it, and the game builds a second Font that filters the fishy ones out, with exactly one use in the entire client: the chat input box, which cannot be made to draw a character three screens wide.

What else walks a Font.PreparedText, and what the caches promise

The prepared text is walked twice by different things and that is deliberate. GuiRenderer walks it to emit glyphs, and ActiveTextCollector walks the very same object looking for the style under the cursor — which is how hovering a link works, and why preparation can be asked to record empty areas, so that hovering the space inside a hover-event run still finds the style. Glyph areas deliberately extend to the full advance, so there are no dead gaps between characters.

The caches down the pipeline are safe mostly by being single-threaded rather than by being locked: they are meant to be touched only on the Render thread. It is not a clean rule — some are keyed on identity and some on equality, and the glyph layer does use volatile fields and a Guava cache. A component’s cached visual order is invalidated by the Language object changing, and a translatable component’s decomposition likewise. Two places leak, and both are worth knowing: the sign block entity caches its rendered lines on a class the server also ships, and a text display entity keeps its laid-out lines the same way; neither cache notices a font reload.

For a 1.21-era reader. Font cannot draw. Every drawInBatch and drawString is gone; Font.prepareText returns a Font.PreparedText that somebody else walks later. Style.getFont no longer returns an identifier — it returns a FontDescription, which may not name a font file at all. Also gone: Font.StringRenderOutput; RawGlyph and SheetGlyphInfo (now UnbakedGlyph and GlyphBitmap); BakedGlyph as a class; GlyphProviderBuilder (now GlyphProviderDefinition); and FontSet.getGlyph as public API.

Where to look

Font.prepareText — the per-codepoint loop is the centre of the page. FontSet for the provider chain and the fishy-advance split, FontManager for how a FontDescription becomes a GlyphSource, and GlyphStitcher.stitch for the moment a glyph acquires a texture. StringSplitter.splitLines for how a line break preserves styles, and FormattedBidiReorder.reorder for the heaviest of the four places ICU is used.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The HUD

Verified against Minecraft 26.3 · Part X · press F1 and the interface goes away — but not the thing that can black out your whole screen.

Press F1 and the hearts go, the hotbar goes, the crosshair, chat and the tab list go. Get into bed with the interface hidden and the screen still fades to black. That exception is not a special case in the code: it is a consequence of where a single element sits. Hud.extractRenderState is one ordered method wrapped in two blocks gated on the hidden flag, and the sleep fade is recorded in the gap between them.

That is what this page is: not a tour of hearts and hotbars but a policy. What is drawn over the world, in what order, under exactly which conditions, and which of those conditions is a surprise. The full per-element table is what the HUD draws, and when in Reference; the machinery underneath every one of them is the GUI render tree, and what a screen is — the other thing recorded into the same tree — is GUI and screens.

The order, and the two blocks

flowchart TD
    PUB["GuiRenderState.isHudHidden published, before any check"]
    LLS{"is a LevelLoadingScreen up?"}
    STOP["record nothing"]
    H1{"hidden?"}
    A["camera overlays, crosshair, a new stratum, the hotbar block, effects, boss bars"]
    SLEEP["the sleep fade — ungated"]
    H2{"hidden?"}
    B["demo text, scoreboard sidebar, action bar, title, chat, tab list, subtitles"]
    B2["the subtitles only, and only with an in-game-UI screen up"]
    GUI["Gui continues: the screen if any, then saving indicator, toasts, debug overlay, the deferred subtitles"]
    PUB --> LLS
    LLS -- "yes" --> STOP --> GUI
    LLS -- "no" --> H1
    H1 -- "no" --> A --> SLEEP
    H1 -- "yes" --> SLEEP
    SLEEP --> H2
    H2 -- "no" --> B --> GUI
    H2 -- "yes" --> B2 --> GUI

Two identical hidden? gates with the sleep fade between them, which is the whole shape: the fade is the only thing Hud.extractRenderState records whatever you press. Both branches reach the subtitles: the visible one always, deferring them when there is no screen or an in-game-UI one, and the hidden one only with an in-game-UI screen up.

Above all of that sit gates that are not Hud’s at all, and a page about conditions has to name them: GameRenderer.extract combines whether resources are loaded, whether this frame advances game time (which it is handed) and whether there is a level, and Gui.extractRenderState applies them, calling Hud.extractRenderState only when they hold. That is why a HUD-less frame is the normal state of a loading screen shown before a level exists; the level-loading screen, which is up while one does, is the one Hud tests for by name.

Four more elements are recorded by Gui itself, after the screen rather than with the rest of the HUD, and they are gated at three different depths — which is the one thing about them worth memorising, because it decides what you see on a title screen, on a loading screen and under F1.

recorded after the screenwhen Gui records itunder F1
the saving indicatoronly on a frame that renders a leveldrawn — the one element that ignores the flag
the toastswhenever resources are loaded, level or nothidden
the debug overlaywhenever resources are loaded and some debug entry is on, unless the debug-options screen is the one openhidden, unless some screen is open
the deferred subtitlesalwayswhatever Hud’s second block decided

Two structural facts follow from the shape. The hidden flag is published before the loading-screen short-circuit, so the renderer’s copy is correct even on a frame where the HUD records nothing. And toasts and the debug overlay are always above a screen, because Gui records them after it — while the deferred subtitles are called from a screen’s background pass, so the deferral that fires after the screen finds nothing left to draw and the subtitles a screen drew for itself sit under that screen’s widgets.

Hud.tick runs from Gui.tick once per client tick and takes a pause flag: the autosave indicator animates either way, everything else only when the game is not paused.

The cast

classwhat it decidesthread
Hudthe order, the two hidden-gated blocks, and the health animation stateRender thread
Guithe four elements recorded after the screen, and their three different gatesRender thread
ContextualBarwhat can occupy the one slot above the hotbar — Hud picks whichRender thread
BossHealthOverlaythe bars, and three questions the world’s side asks itRender thread
ChatComponentthe message list, its wrapped lines, and what is faded outRender thread
DebugScreenEntriesthe F3 registry: what an entry is, and the presetsRender thread
DebugScreenEntryListthe per-entry status, the preset applied, and its own save fileRender thread
GuiGraphicsExtractoreverything the HUD records intoRender thread

The toast shelf

The toasts are the one element in that list with arbitration of its own, and it is a shelf rather than a queue. ToastManager holds five slots and a waiting deque; a toast declares how many consecutive slots it wants through Toast.occcupiedSlotCount — Mojang’s spelling — and is admitted only when that many free slots sit next to each other, so a wide toast can wait behind narrow ones that arrived after it. Toast.Visibility is the two-state animation, each state carrying its own sound, and a toast is asked its wanted visibility every frame rather than given a lifetime. Toast.getToken is what lets SystemToast.addOrUpdate reset a system toast with the same id instead of stacking a second, and ToastManager.getToast, which matches class and token, is how a newly unlocked recipe joins the recipe toast already up; advancement toasts always stack. The implementations are AdvancementToast, RecipeToast, TutorialToast, SystemToast, FriendToast and NowPlayingToast — the last of which is not on the shelf at all: it is a field of its own, drawn after the five and suppressed by the pause screen and by MusicToastDisplayState.

The hidden flag travels two ways

The interesting one is the smaller. GuiRenderState.isHudHidden is read by GameRenderer in three places, to suppress the held item, the three-dimensional crosshair and — the smallest of the three — the totem-pop animation. The block-in-eyes, water and fire overlays are drawn whatever F1 says. So a 2D flag does change how the world is drawn, in three narrow ways. But Hud.isHidden itself is read directly by six other places across the client, including two entity renderers that suppress name tags.

It is not the only field on the tree the frame reads back: a clear-colour override lives there too, and every site that reads either of them is GameRenderer’s rather than LevelRenderer’s — the 2D side never reaches into the world renderer, only into the thing that drives it.

This page’s own boss bar is the loudest case of the same traffic. Five places on the world’s side — the game renderer’s tick and its level pass, the lightmap, the fog environment and the music picker on Minecraft — ask BossHealthOverlay three questions: should the screen darken, should world fog be created, should the End music play. So the dragon’s fog and the Wither’s darkening come from the world asking a HUD element, which holds the flags the boss-bar packets set. The bar itself interpolates against wall-clock time inside LerpingBossEvent, which is what turns the discrete progress the server sends into a smooth bar; what is on the other end of those packets — the saved model, its members, and the execute store that writes it — is scores, teams and stored data.

Four states, one slot, and an asymmetric rule

Hud.contextualInfoBar holds one of four states — nothing, experience, the locator, a jumpable vehicle — and Hud.nextContextualInfoState re-decides which every frame. It is a rule, not a state machine, and the rule is not symmetric. With waypoints present, a jumping vehicle or a recently changed experience total beats the locator; with no waypoints, a jumpable vehicle beats experience unconditionally — so mounting a horse silently takes your XP bar away. The level number is recorded separately from the bar, so it survives whichever bar wins.

The three implementations are ExperienceBar, LocatorBar — backed by ClientWaypointManager and styled by Hud.waypointStyles, a WaypointStyleManager that is the HUD’s own reload listener — and JumpableVehicleBar. Hud.ContextualInfo is the enum of the four states.

The hearts, which are three numbers at once

sequenceDiagram
    participant CPL as ClientPacketListener
    participant LP as LocalPlayer
    participant Hud as Hud
    participant GGE as GuiGraphicsExtractor

    CPL->>LP: hurtTo — the health, on a change the cooldown, on damage hurtTime
    Note over CPL,GGE: the next frame
    Hud->>Hud: extractPlayerHealth, gated on the game mode
    Hud->>Hud: healthBlinkTime — 20 ticks, or 10 for a heal
    Hud->>Hud: displayHealth catches up after the second
    Hud->>Hud: Hud.random takes tickCount, so the jitter is per tick
    Hud->>Hud: extractHearts — one descending pass: container, absorption, ghost, truth
    Hud->>GGE: blitSprite a heart, chosen by Hud.HeartType.getSprite

One packet, one frame later: the packet’s handler, ClientPacketListener.handleSetHealth, does everything it does to the hearts by calling LocalPlayer.hurtTo, and the six steps after the note belong to Hud. None of them reacts to the packet — it is what the HUD does every frame, reading numbers the packet left behind.

The figure’s steps are Hud.extractPlayerHealth, which sets Hud.healthBlinkTime from Hud.tickCount when the health fell or rose while LivingEntity.damageCooldownTime runs; Hud.displayHealth, which lags; Hud.random, reseeded from that same tick counter; Hud.extractHearts, the one descending pass; and one GuiGraphicsExtractor.blitSprite per layer of each heart, the sprite chosen by Hud.HeartType.getSprite. LocalPlayer.hurtTo is what the packet touched: it sets the health, LivingEntity.damageCooldownTime when the health changed, LivingEntity.hurtTime too when it fell, and nothing on Hud at all.

The shake is seeded from the tick counter, so it jitters once per client tick and is identical across every frame of one tick — and the same seeded stream drives the hunger jitter and the air-bubble wobble, which is why each of them holds still within a tick. The blink is a square wave with a three-tick half-period, running for twenty ticks after damage and ten after a heal.

Three numbers and four layers is the arithmetic of a heart row, and they are not the same list. The three numbers are Hud.lastHealth, which is what the previous frame saw; Hud.displayHealth, a lagging value that catches up about once a second; and the truth, read off the player. The four layers the descending pass draws are the container, absorption, the ghost and the truth — and the ghost is Hud.displayHealth, the number that is out of date on purpose. That is what the blink shows you: not damage, but the gap between two of the three numbers.

Hud.HeartType is six constants, one of which Mojang spells Hud.HeartType.POISIONED, each with eight sprites.

One gate silences four elements at once: armour, hearts, food and air are all recorded inside Hud.extractPlayerHealth, which is gated on the game mode being able to hurt you — which is why creative has no armour bar either. Food and mount health share a slot, and the air bubbles sit a row above it, rising further for each extra row of mount hearts. And the air bubbles make a sound: Hud.playAirBubblePoppedSound ramps volume and pitch with how many bubbles are gone, and hands it to the sound engine like any other client-side sound. It is the only element inside Hud.extractRenderState that is also an audio event — the toasts have sounds too, but they arrive with the toast rather than out of a drawing pass.

Chat is one thing in two modes, and five collections

The chat you read and the chat you type into are the same code in different modes: the HUD bails out entirely when the chat screen is focused. Message age is measured in HUD ticks rather than timestamps, and a message the server asked to delete keeps its original timestamp when it is replaced by a marker — so the marker fades on the original message’s schedule.

ChatComponent holds four collections, not two: every message and every wrapped line are separate lists with separate caps, and the deletion queue and the recent-input history are the other two. A fifth, the delay-option queue, lives on ChatListener, and is the reason a chat-delay setting can hold a message that has already arrived.

What the message list holds is a GuiMessage — the time it arrived in HUD ticks, the Component, the signature if it had one, a GuiMessageSource saying whether a player, the server or this client produced it, and a nullable GuiMessageTag. The tag is the client’s own verdict, drawn as a two-pixel bar to the left of the line, outside the text entirely, with the reason as a tooltip when you hover it: system, system-in-singleplayer, not-secure, modified, error. Only modified also carries an icon, and that one goes after the text rather than beside the bar; each tag additionally carries a short GuiMessageTag.logTag string, which goes to the game’s log beside the message. No GuiMessageTag reaches ChatLog, the separate ring of LoggedChatEvents kept for the reporting screens; what it keeps of the verdict is a player message’s ChatTrustLevel, which the modified and not-secure tags are made from. Which verdict a message earned belongs to chat and signing; this page owns the bar.

What a debug line is, and who turns one on

The F3 overlay is a registry rather than a method, and it is the second of the two debug systems this book describes. DebugScreenEntries holds every entry by Identifier, each a DebugScreenEntry writing lines through a DebugScreenDisplayer. DebugScreenEntryList, reachable as Minecraft.debugEntries, stores a DebugScreenEntryStatus per entry, applies the DebugScreenProfile presets DebugScreenEntries defines and persists to its own file with its own data-fixer type — so an entry set to always-on renders with F3 never pressed, and the game remembers that you set it. The screen that edits it is suppressed by Gui rather than by the overlay, and the overlay’s own test asks about screens too: under F1 it still records, given an entry to show, whenever a screen is open.

The charts hanging off it are FpsDebugChart, TpsDebugChart, PingDebugChart and BandwidthDebugChart over AbstractDebugChart, plus ProfilerPieChart, which is not one of them.

Two seams run out of here. Two of the rebindable debug mappings toggle entries that print no line at all and exist only to carry a flag the world renderer reads — input and keybinds says which family a given shortcut belongs to. And the other debug system, the one that asks the server for a villager’s brain, is debugging the running game: the two meet because an F3 entry decides whether eleven of that page’s twenty-five renderers exist at all, and the FPS charts are what turn its tick-time subscription on.

Questions players ask

Why do subtitles appear under an open chest? They are deferred to the screen’s background pass, beneath its widgets, whereas the tooltip and the pre-edit overlay the extractor holds are deferred past the widgets. The deferral happens when there is no screen at all or the screen declares itself in-game UI — the common case, not the rare one — and with no screen Gui makes the deferred call itself — or, while the loading overlay fades in or out, LoadingOverlay does.

Is the pumpkin blur hardcoded? No. The camera overlay list is data-driven: every equipment slot is asked whether its item declares a camera overlay.

Why does everything on my HUD vanish at once when I disconnect? Because one method clears it as a whole. Hud.onDisconnected resets the tab list, the boss bars, the toasts, the debug overlay, chat and titles together — which is a better definition of “HUD state” than any list of fields.

For a 1.21-era reader. Gui is not the HUD any more. The class that drew the hotbar and hearts is Hud; the name Gui was reused for the screen and overlay manager that used to be fields on Minecraft. The canonical path is Gui.hud. Every render* method on Gui is now an extract* on Hud, and gone with them: LayeredDraw, Minecraft.screen, Minecraft.getToastManager, Minecraft.fpsString, Options.hideGui, and DebugScreenOverlay.render along with its two information-gathering methods — the line content moved out into the entry registry.

Where to look

Hud.extractRenderState — all but the last four of the HUD’s elements in one ordered method, and the two hidden-gated blocks are visible at a glance. Then Hud.extractPlayerHealth for the most-loved method in the client, Hud.nextContextualInfoState for the bar arbitration, Gui.extractRenderState for the four recorded after the screen, DebugScreenEntries for the F3 registry, and ChatComponent for the message list.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Sound: the engine

Verified against Minecraft 26.3 · Part X · a block placed near you: from a packet on the client’s Render thread to an OpenAL source, across four of the five threads that take part and one hop the sound cannot skip.

SoundEngine.play never starts a sound. It resolves the name, picks a variant, tells the subtitle overlay, computes the volume and asks for a channel — and leaves attach buffer, play to be posted as a task on another thread once the buffer is ready. Even when the decoded audio is already in the cache, a sound always starts at least one hop to the sound thread after the packet that asked for it, because the engine has no path that calls Channel.play itself. Preloading removes the decode from that latency; it does not remove the hop.

This page is the machine: the five threads that take part, the borrowed OpenAL source, the buffer that arrives afterwards, and the arithmetic that decides how loud it is. What decides that a sound should happen at all — and the fact that most world sounds are not named on the wire — is what makes a sound happen.

OpenAL is touched only inside com/mojang/blaze3d/audio, plus NativeLibrariesBootstrap, which loads the native library. Nothing in client/sounds makes an AL call itself: it calls into that wrapper, and nearly everything outside calls SoundManager.play and forgets — MusicManager alone reads what it returns.

The cast

classwhat it decidesthread
SoundManagerthe loaded sounds.json map, and the public front doorRender thread
SoundEnginewhich instances are playing, how loud, and what to dropRender thread
SoundInstanceone playing-or-wanting-to-play sound: position, pitch, looping, attenuationRender thread
SoundEngineExecutorone daemon thread with a task queue in front of it — where a per-source AL call is madeSound engine
ChannelAccessacquiring, configuring and releasing a channel, as tasksposts to Sound engine
Librarythe OpenAL device, context, listener and channel limitsRender thread opens it
SoundBufferLibrarydecoded .ogg data, cached per paththe caller’s; the decode on the Download pool
AbstractDeviceTrackernoticing that the device list or the default device changedRender thread, querying on IO-Worker

Five threads, and one the game does not own

The page is mostly about which thread does what, so it is worth having the list before the trace.

threadits part in a sound
Serverdecides a sound happens, computes who is in range, sends packets. Never audio.
Render (the client game thread)receives the packet, builds a SoundInstance, calls SoundManager.play. Also the device’s life: opening and closing the device and context, resetting the Listener, and deleting the cached static buffers.
Sound engineevery per-source AL call while the game is running: channel acquisition, parameter setting and release through ChannelAccess, plus the listener transform, which SoundEngine.updateSource posts to the executor directly. The two bulk teardowns are the exception, and they run off it.
Util.nonCriticalIoPool (the Download- threads)reads and decodes .ogg files with JOrbisAudioStream into a SoundBuffer, inside SoundBufferLibrary.getCompleteBuffer.
Util.ioPool (the IO-Worker- threads)device enumeration — AbstractDeviceTracker.tick dispatches DeviceList.query there, so the refresh of the ALC device list does not stall a frame. A forced refresh still queries on the Render thread.

And one the game does not own: OpenAL Soft’s own event-callback thread, which invokes the callback CallbackDeviceTracker installs to notice that a playback device was added, removed or made the default.

Four of those five are on the path of a single sound; Util.ioPool is the odd one, polling the device list beside the trace rather than inside it. Where each sits among the rest of the game’s is the threads.

The Render thread’s two cadences, both set by the client loop: once per client tick Minecraft.tick calls MusicManager.tick and then SoundManager.tick, which walks the ticking sounds, updates positions and volumes, releases finished channels and drains the delayed queue; once per frame Minecraft.runTick calls SoundManager.updateSource with the camera.

A block is placed near you

The packet has arrived and ClientLevel.playSeededSound has taken it — that half of the story is what makes a sound happen. What this figure shows is everything after: three objects on the Render thread, two on the sound thread, the buffer library whose decode runs on a download thread, and the boundary every per-source OpenAL call has to cross.

sequenceDiagram
    box transparent Render thread
    participant CL as ClientLevel
    participant SndE as SoundEngine
    participant ChanA as ChannelAccess
    end
    box transparent Sound engine thread
    participant SEE as SoundEngineExecutor
    participant Library as Library
    end
    box transparent Download pool
    participant SBL as SoundBufferLibrary
    end

    CL->>SndE: play, via SoundManager
    SndE->>SndE: resolve, weigh, tell the listeners, calculateVolume
    SndE->>ChanA: createHandle
    ChanA->>SEE: execute — the acquire
    SEE->>Library: acquireChannel
    SEE-->>SndE: join — a handle, or null
    SndE->>ChanA: ChannelAccess.<br/>ChannelHandle.execute
    ChanA->>SEE: execute — the parameters
    SndE->>SBL: getCompleteBuffer
    SBL-->>SndE: thenAccept — run by the thread that completes it
    SndE->>ChanA: ChannelAccess.<br/>ChannelHandle.execute
    ChanA->>SEE: execute — attachStaticBuffer, play
    loop per client tick
        SndE->>ChanA: scheduleTick, pumping streams
    end

Every arrow that crosses into the sound thread’s box is a task queued, not a call made: in this trace ChannelAccess never touches OpenAL itself. Of the two arrows coming back, only the join on ChannelAccess.createHandle’s future waits — the only place in this trace the Render thread does.

Read the three ChanA->>SEE arrows as one shape. The sound thread is reached three times for one sound, and the third of them — attach and play, posted from inside the buffer future’s continuation — is the hop this page opens with. The first two come from the Render thread; the third comes from it only when the buffer was already decoded, and otherwise from the download thread that finished decoding it.

The four beats worth narrating.

The name is resolved, in an order you can hear. The instance’s Identifier is looked up in the SoundManager registry for a WeighedSoundEvents, and WeighedSoundEvents.getSound rolls the weighted choice — following event-to-event redirects — to a concrete Sound. Then, in this order: the unknown-event and empty-sound cases return early; every registered SoundEventListener is told; the volume is computed; and only then is a zero-volume sound abandoned. So a sound whose category is muted still produces a subtitle, and a sound with no sounds.json entry does not. SubtitleOverlay is the only SoundEventListener in the game, and that ordering is what it is for.

A channel is borrowed, on the sound thread. ChannelAccess.createHandle posts a task to the SoundEngineExecutor; on that thread Library generates a new OpenAL source, provided the static or streaming limit — chosen by Sound.shouldStream — has room. The Render thread blocks on that future and gets a handle or null; null means the sound is silently dropped. This is one of exactly two places the Render thread ever waits on the sound thread; the other is teardown, four sections below.

Parameters go first, data arrives later. ChannelAccess.ChannelHandle.execute posts the pitch/volume/attenuation/position setup, while SoundBufferLibrary.getCompleteBuffer returns a future for the decoded buffer. When that completes, its continuation posts attach buffer, play to the sound thread. That post is the hop this page opens with. Sound.shouldPreload and SoundEngine.requestPreload remove the decode from the latency, not the hop.

Streams are pumped by the tick. Long sounds — music, records — are streamed: Channel.attachBufferStream queues Channel.QUEUED_BUFFER_COUNT buffers of Channel.BUFFER_DURATION_SECONDS each, four seconds in all, and ChannelAccess.scheduleTick, posted once per client tick from SoundEngine.tick, calls Channel.updateStream on each to refill. The same pass releases channels whose source reports stopped.

The channel limits are counters, not pools

Library asks the device how many mono sources it offers, falling back to Library.DEFAULT_CHANNEL_COUNT — thirty. The streaming limit is the square root of that, clamped between two and eight; the static limit is a clamped remainder, floored at eight and capped at 255, so it is not simply “the rest”. Sources are generated on acquire and deleted on release: one OpenAL source per playing sound, and when a limit is reached new sounds are dropped rather than queued. The game does not steal channels by priority.

Nor does muting free one. SoundEngine.refreshCategoryVolume pushes the new volume to every playing channel of that category and stops nothing, so a looping sound muted to zero holds its OpenAL source until it ends on its own. Muting suppresses new allocations; it does not reclaim old ones. The OpenAL source itself, though, goes back the moment the channel reports stopped: ChannelAccess.scheduleTick releases it with no lifetime gate at all. SoundEngine.MIN_SOURCE_LIFETIME holds something else — the engine’s bookkeeping entry for the instance, for at least twenty ticks from the play, so a sound shorter than a second keeps its entry for the rest of that second after its source has gone.

SoundEngine.queuedSounds is the one of its fields worth naming here, because it is what the trace has not shown: a sound can be delayed rather than played, and SoundEngine.tick drains that queue once per client tick. Three things put a sound in it — a distance delay, which is what makes a sound happen, the End flash’s fixed thirty-tick delay, and a manual loop, three paragraphs below. Beside it SoundEngine.instanceToChannel, SoundEngine.instanceBySource, SoundEngine.tickingSounds, SoundEngine.gainBySource and SoundEngine.soundBuffers are the bookkeeping. SoundEngine.play returns a SoundEngine.PlayResult — started, started silently, or not started — and SoundManager.play passes it through; MusicManager is the only caller that reads it.

Volume, looping, and the attenuation everyone explains wrongly

SoundEngine.calculateVolume multiplies the instance’s own volume, the options volume (Options.getFinalSoundSourceVolume, itself category times master) and the runtime gain in SoundEngine.gainBySource. The third exists so that MusicManager can fade a category without touching the player’s slider — which is why the music slider and the music fade are two different numbers.

A computed volume of zero is abandoned unless it is music: SoundEngine.play drops it only when the instance does not say SoundInstance.canStartSilent and the category is not SoundSource.MUSIC. Music always starts, silently if need be: under a music or master slider at zero a track plays unheard, and MusicManager withholds the now-playing toast until the slider is raised.

Looping happens three different ways. Static sounds loop in OpenAL, with Channel.setLooping. Streamed sounds loop by wrapping the decoder in a LoopingAudioStream, since the source only ever holds a few seconds. And a looping instance with a delay is looped manually — SoundEngine.shouldLoopManually — by re-queueing it into SoundEngine.queuedSounds when its channel stops.

The third thing in this section is attenuation, and the obvious explanation of it is wrong. UI sounds do not attenuate because of their attenuation, not their relativity: SimpleSoundInstance.forUI sets both SoundInstance.Attenuation.NONE and the relative flag, and it is the former that makes the engine call Channel.disableAttenuation. A relative sound offset from the listener would still fall off.

The instance family, and the decode stack

SoundInstance lives in client/resources/sounds and carries event, source, volume, pitch, position, looping, relative and attenuation. SimpleSoundInstance is a one-shot at a point; the TickableSoundInstance subclasses — AbstractTickableSoundInstance, EntityBoundSoundInstance following its entity, minecarts, elytra, bees, ambient loops — re-evaluate themselves every tick.

Below the engine, com/mojang/blaze3d/audio is the OpenAL wrapper: Library (device, context, listener, channel limits), Channel (one source), SoundBuffer (one buffer), Listener (the ear, set from a ListenerTransform), DeviceList, and the device-tracker family (AbstractDeviceTracker, CallbackDeviceTracker) that notices headphones being unplugged.

The decode stack is three interfaces deep: AudioStream, then FiniteAudioStream, then FloatSampleSource, with ChunkedSampleByteBuf assembling the samples and JOrbisAudioStream — JOrbis, a Java Vorbis decoder — as the one real implementation. LoopingAudioStream wraps any of them.

The sound thread is not the mixer, and the device is not its

Two things about that thread are assumed the wrong way round often enough to be worth stating flatly.

It does not mix. SoundEngineExecutor does nothing but run tasks; OpenAL, the native library, does the mixing on threads of its own that no Java code ever sees. The Java thread exists so that per-source AL calls are serialised — the tasks include decoding the next seconds of a stream — and almost all of them go through it. The exceptions are the two bulk teardowns, ChannelAccess.clear and Library.cleanup, which release handles directly on the Render thread: the first after the sound thread has been joined, the second after SoundEngine.stopAll has emptied every channel and started a fresh thread with nothing queued — or, in a crash, from SoundEngine.emergencyShutdown with the sound thread still running.

It does not own the device. Opening and closing the device and context, resetting the listener and deleting the cached static buffers all happen on the Render thread, and an orderly teardown (SoundEngine.destroy) releases the channels only after SoundEngineExecutor.shutDown has joined the sound thread. That join is the second of the two places the Render thread blocks on the sound thread, and SoundEngine.stopAll is where it happens; the channel acquisition four sections above is the first.

What a device has to offer before any of this starts is exactly three things, and Library.init throws rather than degrade if one is missing: an ALC of 1.1 or newer, the AL_EXT_source_distance_model extension and the AL_EXT_LINEAR_DISTANCE extension. Everything beyond those it can do without — HRTF is taken only when the device offers ALC_SOFT_HRTF and Options.directionalAudio is on, and the lost-device check only when the device has ALC_EXT_disconnect; without it the engine still reloads when the device list shows the current device gone, the default moved or the chosen device back.

Questions players ask

Why does sound cut out when I plug in headphones? Because reload is destroy-and-rebuild, and it arrives from three different doors. The resource reload arrives as SoundManager.apply, which ends by reloading the engine; SoundManager.reload is the options path, taken when the audio device is changed; and SoundEngine.tick reloads itself when the device is lost, or when the tracker’s list shows the current device gone, the default moved or the chosen device back. All three tear the OpenAL context down in Library.cleanup and call SoundEngine.loadLibrary again.

Why is there no sound for the first few frames of a world? SoundEngine.updateSource posts a ListenerTransform — position, forward, up — from the Camera every frame, and it no-ops until Camera.isInitialized. The same is true again after GameRenderer.resetData. And the ear is always one frame stale: Minecraft.runTick posts the transform before it renders, and the camera is only advanced inside the frame, so the ear is where the eye was last frame. At 60 fps nobody hears it — but it is worth knowing before blaming OpenAL.

Does pausing stop everything? No: SoundManager.pauseAllExcept leaves SoundSource.MUSIC and SoundSource.UI running, and SoundEngine.tickMusicWhenPaused is the pause-menu tick.

Where to look

SoundEngine.play — the whole resolution-and-dispatch order is one method, and the early returns in it are audible. ChannelAccess and ChannelAccess.ChannelHandle for how a per-source call becomes a task, and SoundEngineExecutor for the thread it becomes a task on. SoundEngine.calculateVolume for the three factors, Library.init for the limits and the device requirements, SoundBufferLibrary.getCompleteBuffer for the decode, and SoundEngine.tick for the once-a-tick sweep.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What makes a sound happen

Verified against Minecraft 26.3 · Part X · you break a block and you hear it: three doors a sound can come through, and only one of them says what the sound is.

Watch someone place a block and the server sends ClientboundSoundPacket, naming the sound. Watch them break the same block and it sends nothing of the kind: Block.playerWillDestroy fires a level event through Block.spawnDestroyByEntityParticles, and ClientboundLevelEventPacket carries an int and a block-state id. The client decides for itself what that int means — for a break, SoundType.getBreakSound off the block’s own SoundType, the five-sound group (break, step, place, hit, fall) every BlockBehaviour.Properties carries — and plays the result locally. So the two halves of one interaction reach you by different mechanisms, and the second one is not a sound at all until your client makes it one.

Then there is the third door, and it is the absence of a packet rather than a packet: some sounds never cross the wire at all. Two quite different things come through it. Biome loops, the cave mood, music, the underwater hum are decided by your client and nobody else’s. And so, more surprisingly, are your own place and break — neither of the calls above is sent to the player who caused the sound, because that player’s client has already played it locally. Who hears it below is the one rule that arranges both.

This page is the content model and those three doors, and it takes them first: the table below is what the page is for, and the cast comes after it, because half of the eight classes only make sense once you know which door they stand in. The machine that turns any of them into an OpenAL source is the sound engine.

The three doors

flowchart TD
    SERVER["something happens on the server"]
    NAMED["door 1 — Level.playSound names a SoundEvent"]
    NOWIRE["door 3 — nothing crosses the wire"]
    EVENT["door 2 — Level.levelEvent sends an int and a block-state id"]
    P1["ClientboundSoundPacket or ClientboundSoundEntityPacket"]
    P2["ClientboundLevelEventPacket"]
    LEH["LevelEventHandler reads the int against this client's block data"]
    LOCAL["ClientLevel.playSeededSound — the excluded entity is you, so the test passes"]
    MINE["your own place, through the shared Level.playSound"]
    MINEB["your own break, a level event run locally"]
    AMB["BiomeAmbientSoundsHandler, MusicManager, the underwater and bubble-column handlers"]
    SM["SoundManager.play"]
    SERVER --> NAMED --> P1 --> LOCAL
    NOWIRE --> MINE --> LOCAL
    NOWIRE --> AMB --> SM
    NOWIRE --> MINEB --> LEH
    LOCAL --> SM
    SERVER --> EVENT --> P2 --> LEH --> SM

Doors one and three meet, which is the point of the picture: a packet and your own placing hand both arrive at ClientLevel.playSeededSound, and both pass its one test because the excluded entity is you. Your own break runs door two’s handler locally instead, and door two never names a sound on the wire at all.

The three columns are how to reason about the wire, and the third is the one the usual summary leaves out entirely.

a named sounda level eventnothing on the wire
what crossesa SoundEvent holder, a position in eighths of a block, a seedan int and a block-state idnothing
who chooses the soundthe serverthis client, from its own resource pack and block datathis client
can it name a sound in no registryyes — SoundEvent.STREAM_CODEC sends either a registry id or an id plus a rangeonly a jukebox song’s, whose entry may carry one inlineyes, when the data behind it carries one inline: the synced ambience and music attributes, a goat horn’s instrument, an item’s consume sound
examplesa block placed, /playsound, a mob’s voicea block broken, a dispenser, fire extinguished, a ghast warning, the wither spawn, the dragon’s deathyour own place and break; biome loops, cave mood, music, underwater, bubble columns

That third row is a genuine hole in the usual summary. Data packs cannot register sound events — Registries.SOUND_EVENT is a static registry — but a packet may carry an inline SoundEvent, so a server can name a sound that is in no registry at all. The two statements are both true and are usually run together into a false one.

The other clientbound members of the family are ClientboundSoundEntityPacket, which follows a moving entity, ClientboundStopSoundPacket, which /stopsound sends, and ClientboundExplodePacket, which carries its own sound alongside everything else an explosion needs. There is no serverbound sound packet anywhere: the server infers what you did from other packets and tells everyone else about the sound.

The cast

classwhat it decidesthread
SoundEventa name and an optional fixed range — never a fileboth sides
SoundEventsthe static registry of every event the game definesboth sides
SoundSourcewhich volume slider appliesboth sides
SoundEventRegistrationwhat one sounds.json entry says, and whether it replaces or appendsworker pool
WeighedSoundEventsthe weighted list a name resolves to, and the redirects in itworker pool, then Render thread
LevelEventHandlerwhat an int from ClientboundLevelEventPacket meansRender thread
BiomeAmbientSoundsHandlerthe loop, the random additions and the cave moodRender thread
MusicManagerwhich track, how often, and the fade that is not the sliderRender thread

What a sound is, as data

SoundEvent — in net/minecraft/sounds, and therefore shared — is a record of an Identifier and an optional fixed range. SoundEvents is the static registry of every one the game defines. It is a name, not a file.

The file comes from sounds.json, one per namespace in every resource pack, which SoundEventRegistrationSerializer parses into a SoundEventRegistration per event name: a weighted list of Sound entries — a file, or a redirect to another event, per the Sound.Type enum — each with volume, pitch, weight, attenuation distance, and whether to stream rather than load whole. SoundManager owns the loaded form, a map of Identifier to WeighedSoundEvents, rebuilt on every resource reload.

Packs merge rather than replace, as tags, languages and atlas sources do among the lists the resource system merges instead of letting the top pack win, and, as a tag can, an entry may say otherwise. SoundEventRegistration carries a replace flag; without it a higher pack’s entries are appended to the lower pack’s list, so a pack that adds one variant gets a mix rather than an override. A redirect entry multiplies volume and pitch through and ORs the streaming flag.

And there are two kinds of silence, which is worth knowing because only one of them is a mistake. The identifier SoundManager.INTENTIONALLY_EMPTY_SOUND_LOCATION is short-circuited by name in AbstractSoundInstance.getOrResolve before the registry is consulted at all, so anything asking for it is silenced with no log warning. An event that simply does not resolve is the other kind, and it logs — including a pack that empties an event’s list, which gets the warning rather than the quiet.

One development flag makes sounds visible: SharedConstants.DEBUG_SUBTITLES gives every sound that plays a subtitle, its path, whether or not its event declares one. Neither kind of silence gets one, because neither reaches the listeners.

SoundSource is the volume category and each one is an options slider: SoundSource.MASTER, SoundSource.MUSIC, SoundSource.RECORDS, SoundSource.WEATHER, SoundSource.BLOCKS, SoundSource.HOSTILE, SoundSource.NEUTRAL, SoundSource.PLAYERS, SoundSource.AMBIENT, SoundSource.VOICE, SoundSource.UI. Two of those carry more than the options screen says: SoundSource.RECORDS, Jukebox/Note Blocks, also carries the goat horn, and SoundSource.WEATHER carries thunder and the End flash as well as rain. Neither is SoundSource.AMBIENT.

Who hears it

ServerLevel.playSeededSound asks SoundEvent.getRange for the audible radius — a fixed range if the event declares one, otherwise sixteen blocks scaled up by volumes above one — and PlayerList.broadcast sends the packet to every player in that dimension within range, skipping the excluded player. The seed travels in the packet so that every client picks the same variant and samples the same pitch multiplier off the chosen Sound, which is why a sound that is one of eight variants sounds the same to two players standing together.

And the excluded player is not left out; they are served first. The two sides read that one word oppositely. ServerLevel.playSeededSound broadcasts to everyone in range but the excluded entity; ClientLevel.playSeededSound — the same method name, the client’s override, in both its positional and its entity-bound form — plays a sound only when the excluded entity is the local player.

That inversion is what makes the client override usable for two jobs at once, and it is worth being explicit about which is which. When a packet arrives, ClientPacketListener.handleSoundEvent hands the override the local player as the excluded entity, so the test passes and a bystander hears the packet normally. When shared block or item code calls Level.playSound while running on your own client, the excluded entity is you for the same reason it was you on the server — and the test passes again, locally. So the one method plays the sounds sent to you and the sounds never sent to anyone, and each machine hears exactly one copy: yours locally, theirs by packet. That is how your own place comes through the third door, with every other sound shared code plays with you excluded. Your break comes through it another way, and never reaches this method: the same shared code fires a level event on your client and on the server, ClientLevel.levelEvent plays it with no test at all, and ServerLevel.levelEvent sends it to everyone in range but the player who broke the block.

Two qualifications keep it from being a law. The rule needs an exclusion to read: Player.playServerSideSound, which plays the six attack sounds, excludes nobody, so your own critical hit does travel the whole way out and back. And your local copy is genuinely a different sound from the one your neighbour hears — your client drew its own seed (from Level.soundSeedGenerator for a place or a door, from the level’s own random for a break, which carries no seed on the wire, so every client that hears it rolls its own) rather than reading one off a packet, so the variant and the pitch are rolled twice (block interaction follows a door through both rolls).

The position is quantised on the way: ClientboundSoundPacket.LOCATION_ACCURACY is eight, so the wire carries three ints in eighths of a block.

And sound has a speed, for the few callers that ask for it. ClientLevel.playLocalSound takes a distance-delay flag, and when it is set and the source is more than ten blocks off the sound is deferred by its distance over a fixed rate, through SoundManager.playDelayed into SoundEngine.queuedSounds. Firework explosions and a handful of level events set it. Thunder, the sound everyone assumes is the reason it exists, does not: LightningBolt passes the flag as false and the crack is instant. LocalPlayer.playSound goes the other way and calls ClientLevel.playLocalSound directly, skipping even the exclusion check.

Music and ambience are environment attributes

BiomeSpecialEffects carries block tint only. Music and the ambient loop, additions and mood are carried by EnvironmentAttributes (see environment attributes and timelines): EnvironmentAttributes.BACKGROUND_MUSIC and EnvironmentAttributes.MUSIC_VOLUME for the music, EnvironmentAttributes.AMBIENT_SOUNDS for the other three together, beside EnvironmentAttributes.FIREFLY_BUSH_SOUNDS, a gate on the firefly bush’s own sound — all syncable, all resolved through the same dimension-then-biome-then-timeline-then-weather layer stack as fog and sky colour.

Minecraft.getSituationalMusic reads EnvironmentAttributes.BACKGROUND_MUSIC off the camera’s attribute probe and asks BackgroundMusic.select for the creative or underwater variant; the End boss fight overrides it directly. Minecraft.getMusicVolume reads EnvironmentAttributes.MUSIC_VOLUME the same way, which is how a biome dims its own music without touching the slider.

BiomeAmbientSoundsHandler reads EnvironmentAttributes.AMBIENT_SOUNDS from Level.environmentAttributes and drives three things from it: a cross-faded loop, random additions on a per-tick chance (AmbientAdditionsSettings), and the cave “mood” that accumulates in darkness (AmbientMoodSettings). UnderwaterAmbientSoundHandler and BubbleColumnAmbientSoundHandler are the two that remain plain client-side handlers with no attribute behind them.

MusicManager owns the rest: a MusicManager.MusicFrequency setting that caps the gap between tracks, a fade that drives SoundManager.updateCategoryVolume rather than the player’s slider (the third of the engine’s three volume factors), and the now-playing toast — a NowPlayingToast shown or withheld on the SoundEngine.PlayResult the engine returned, and suppressed again by the pause screen and by MusicToastDisplayState.

The remaining callers are worth naming because they are the ones that are neither the world nor the wire: the ambient handlers in client/resources/sounds for loops that exist only on the client, the GUI’s clicks and toast chimes (AbstractWidget.playButtonClickSound, ToastManager), and SoundPreviewHandler, which previews a representative sound for most categories while a volume slider is dragged outside a world.

Where to look

LevelEventHandler — the whole second door is two switches in one class, LevelEventHandler.levelEvent and LevelEventHandler.globalLevelEvent, and reading them is the fastest way to see how much of the game’s audio is not named on the wire. ServerLevel.playSeededSound and PlayerList.broadcast for who is told; ClientLevel.playSeededSound for the local-player branch that closes the loop. SoundEventRegistration and WeighedSoundEvents for the pack model, and BiomeAmbientSoundsHandler for the three ambience mechanisms in one class.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Debugging the running game

Verified against Minecraft 26.3 · Part X · a villager’s brain drawn over its head: one subscription mechanism, sixteen instances, all of them in the jar you downloaded and fifteen of them unreachable without a JVM flag.

Every one of these sixteen debug subscriptions is compiled into the shipped client and the shipped dedicated server. Every subscription is in the registry, every packet is in the protocol, every producer call site is there. Nothing is stripped. The client simply never asks — fifteen of the sixteen are behind a JVM system property read at startup, and the server has to agree besides. The idle cost is small but it is not nothing: the producers check for a subscriber before they work, and the server sweeps every online player’s permissions once a tick whether or not anyone has asked for anything.

That is the pattern the page is about: a registry of subscription kinds, a per-level engine that sleeps until somebody asks, a poll-and-diff sender, and the renderers — thirteen of the twenty-five — that turn the results into floating text and boxes. It is only half a client system — the machinery ships on the dedicated server — but the client is the only thing that ever asks and the only thing that draws, and the trace ends in a renderer.

The cast

classwhat it decidesthread
DebugSubscriptionwhat one kind of debug value is: a stream codec, and an expiryboth sides
DebugSubscriptionsthe sixteen kindsboth sides
DebugValueSourcewhich objects can answer which subscriptionsServer thread
ServerDebugSubscriberswho is subscribed, rebuilt every tick, and the permission ruleServer thread
LevelDebugSynchronizersper level, a source synchronizer for each of the fifteen subscriptions with a value codec, the event-driven point-of-interest and village-section synchronizers, and the sleep flagServer thread
TrackingDebugSynchronizerthe engine: registration, the diff, and the tracking filterServer thread
ClientDebugSubscriberwhat to ask for, and the maps holding what came backRender thread
DebugRendererthe renderer list, rebuilt when the enabled entries changeRender thread

The idea

A DebugSubscription is a registry object — held in BuiltInRegistries.DEBUG_SUBSCRIPTION under Registries.DEBUG_SUBSCRIPTION — carrying exactly two things: a nullable stream codec for its value type, and an expiry in ticks, which is DebugSubscription.DOES_NOT_EXPIRE for most. Its payload wrappers are the records DebugSubscription.Update — a subscription plus an optional value, so absence is expressible — and DebugSubscription.Event, a subscription plus a value. Both are serialised by dispatching on the registry id onto the subscription’s own codec.

That is the whole abstraction, and it replaces what used to be a fixed packet type per kind of information.

DebugValueSource is the supply side, implemented by Entity, Mob, Bee, Breeze, BlockEntity, BeehiveBlockEntity and LevelChunk; DebugValueSource.registerDebugValues hands back one DebugValueSource.ValueGetter per subscription the object can answer. On the client, ClientDebugSubscriber keeps what came back, keyed by chunk position, block position or entity UUID, plus a list of expiring events, and ClientDebugSubscriber.createDebugValueAccess hands renderers a read-only DebugValueAccess view. DebugRenderer is a plain list of DebugRenderer.SimpleDebugRenderers, rebuilt by DebugRenderer.refreshRendererList. Neither the synchronizer that produced the value nor the renderer that consumes it draws anything: the renderer appends, which is the next section.

A renderer does not draw a gizmo; it appends one

The renderers at the end of this trace emit through Gizmos, a debug-drawing API in the game-engine sense: the immediate-mode draw me a box in the world for one frame facility most engines have and Minecraft did not. Gizmos is a static façade over a thread-local GizmoCollector, and Gizmos.addGizmo throws when no collector is installed on the calling thread — so where you may draw is decided by where you are running, not by what you are allowed to see. The shapes are small records — CuboidGizmo, LineGizmo, ArrowGizmo, CircleGizmo, RectGizmo, PointGizmo, TextGizmo — a GizmoStyle is a stroke and a fill, and the GizmoProperties handle a call returns can pin the shape on top, keep it for a number of milliseconds, or fade it.

Five places in the game install a collector; four of them keep what they are given, and those four are the table. The fifth is GameTestServer, which installs GizmoCollector.NOOP, so code that draws does not throw in a headless test run. The fourth row is the one worth knowing, though nothing the game ships draws into it.

collectorinstalled bywhat it collects
per tickMinecraft.collectPerTickGizmosanything the client tick draws, cleared each tick
per frame, during extractionLevelExtractor.collectPerFrameMainThreadGizmosthe debug renderers of this page, inside the extract pass
per frame, during renderingLevelRenderer.collectPerFrameRenderThreadGizmoswhat the world renderer itself wants outlined
the integrated serverIntegratedServer, around its whole packet-and-tick stepserver tick code drawing into a singleplayer world, published for the client to drain

So server-side game logic can draw, and only in singleplayer: a dedicated server installs no collector at all, so the identical call there would throw. The two renderers below that want server state are singleplayer-only as well: they reach for the singleplayer server directly rather than asking over the wire, and no subscription carries what they draw.

The sixteen instances

Fourteen rows, sixteen subscriptions: two of the rows carry a matched pair.

subscriptioncarriesfed by
DebugSubscriptions.BRAINSDebugBrainDumpMob.registerDebugValues
DebugSubscriptions.GOAL_SELECTORSDebugGoalInfoMob.registerDebugValues
DebugSubscriptions.ENTITY_PATHSDebugPathInfoMob.registerDebugValues, from the navigator’s current Path
DebugSubscriptions.BEES / DebugSubscriptions.BEE_HIVESDebugBeeInfo / DebugHiveInfoBee and BeehiveBlockEntity
DebugSubscriptions.BREEZESDebugBreezeInfoBreeze
DebugSubscriptions.POISDebugPoiInfoTrackingDebugSynchronizer.PoiSynchronizer, event-driven
DebugSubscriptions.VILLAGE_SECTIONSnothing but presenceTrackingDebugSynchronizer.VillageSectionSynchronizer
DebugSubscriptions.RAIDS / DebugSubscriptions.STRUCTURESpositions / DebugStructureInfoLevelChunk.registerDebugValues
DebugSubscriptions.GAME_EVENT_LISTENERSDebugGameEventListenerInfothe listener registry
DebugSubscriptions.GAME_EVENTSDebugGameEventInfothe dispatcher — an event, expiring after 60 ticks
DebugSubscriptions.NEIGHBOR_UPDATESa positiona listener installed on the neighbour updater — an event, 200 ticks
DebugSubscriptions.ENTITY_BLOCK_INTERSECTIONSDebugEntityBlockIntersectionEntity, pushed directly, 100 ticks
DebugSubscriptions.REDSTONE_WIRE_ORIENTATIONSan Orientationthe experimental wire evaluator, 200 ticks
DebugSubscriptions.DEDICATED_SERVER_TICK_TIMEno value at allsee the sample path below

Four of the sixteen declare an expiry, and “expires after n ticks” means two different things among them. DebugSubscriptions.GAME_EVENTS and DebugSubscriptions.NEIGHBOR_UPDATES are events, and the number is how long the event stays on screen. DebugSubscriptions.ENTITY_BLOCK_INTERSECTIONS and DebugSubscriptions.REDSTONE_WIRE_ORIENTATIONS are values pushed directly, and the number is a time-to-live on a stored value. Only the client purges, and only for those four.

One instance traced: a villager’s brain

sequenceDiagram
    box transparent the client
    participant CDS as ClientDebug<br/>Subscriber
    participant BDR as BrainDebugRenderer
    end
    box transparent the server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant SDS as ServerDebug<br/>Subscribers
    participant LDS as LevelDebug<br/>Synchronizers
    participant TDSS as TrackingDebug<br/>Synchronizer.<br/>SourceSynchronizer
    end

    CDS->>CDS: requestedSubscriptions — the JVM has the brain flag
    CDS->>SGPL: ServerboundDebugSubscriptionRequestPacket, BRAINS
    SGPL->>SGPL: handleDebug<br/>SubscriptionRequest — stored, unhonoured
    Note over SGPL,TDSS: the end of the same server tick
    SDS->>SDS: tick — op, or the owner of an IDE world?
    Note over SGPL,TDSS: the next server tick
    LDS->>LDS: tick — subscribers exist, so wake up
    LDS->>TDSS: registerChunk and registerEntity for everything already tracked
    TDSS->>TDSS: the Mob offers a DebugValueSource.<br/>ValueGetter
    loop each server tick
        TDSS->>TDSS: pollAndSendUpdates — takeBrainDump, compare with the last sent
        TDSS->>CDS: ClientboundDebugEntityValuePacket — only if it differs
    end
    CDS->>CDS: updateEntity — stored under the villager's UUID
    Note over CDS,BDR: the next client frame
    BDR->>BDR: emitGizmos — reads through DebugValueAccess
    BDR->>BDR: Gizmos.<br/>billboardTextOverMob — appended now, drawn later

Two sides and, from a sleeping level, one tick of lag before anything is sent: the request is stored at the head of a server tick and the subscriber set read at its end, and the synchronizers wake, and first send, in the next. The packet in the loop reaches ClientDebugSubscriber through ClientPacketListener like any other, which is why no listener lane is drawn.

The figure’s own names, in its order: ClientDebugSubscriber.requestedSubscriptions is what the JVM flag filled; ServerGamePacketListenerImpl.handleDebugSubscriptionRequest stores the set on the ServerPlayer; LevelDebugSynchronizers calls TrackingDebugSynchronizer.SourceSynchronizer.registerChunk and TrackingDebugSynchronizer.SourceSynchronizer.registerEntity for everything already tracked; TrackingDebugSynchronizer.SourceSynchronizer.pollAndSendUpdates is the per-tick compare, and for a villager the value it asks for is a brain dump; ClientDebugSubscriber.updateEntity files the answer under the entity’s uuid; and BrainDebugRenderer reads it back next frame.

The engine is the synchronizers’ part of that, and it has three properties worth naming. Nothing exists until somebody asks: the level’s synchronizers start asleep, and the first non-empty subscriber set wakes them and retroactively registers every ready chunk and every tracked entity. A polled value is sent again only when it changes: each value source keeps the last value it sent and compares, and a player who starts tracking is sent that stored value once. The point-of-interest synchronizers and the pushed kinds send on each event instead. And nothing reaches a player who cannot see it: sending is filtered by subscription and by whether that player is tracking the chunk or entity. When the last subscriber goes away the value sources are cleared and the level goes back to sleep.

The three cadences: ClientDebugSubscriber.tick runs from ClientPacketListener.tick, once per client tick, and sends only when the wanted set differs from the last one sent. ServerDebugSubscribers.tick runs from MinecraftServer.tickChildren after the levels have ticked, while each LevelDebugSynchronizers.tick runs inside its level’s tick — so every level acts on the previous tick’s subscriber snapshot, a built-in one-tick lag. And DebugRenderer.emitGizmos runs inside LevelExtractor.extract — the snapshot step of the frame — after entities, block entities, particles, sky and clouds, fetching one DebugValueAccess for the whole pass.

The exceptions

Every pattern page’s real content.

Two gates, and the second is not a flag. Fifteen of the sixteen kinds are behind SharedConstants.DEBUG_ENABLED and one of a set of constants beside it — SharedConstants.DEBUG_BRAIN, SharedConstants.DEBUG_POI, SharedConstants.DEBUG_BEES and their siblings, thirteen for the fifteen kinds, all read from JVM system properties at startup, so turning one on means launching the game differently rather than pressing anything. The only subscription an F3 key can reach is the dedicated server’s tick time, through the FPS charts. And the server still has to agree: ServerPlayer.debugSubscriptions returns nothing unless ServerDebugSubscribers.hasRequiredPermissions passes, which means op on the player list, or the owner of a singleplayer world run from an IDE. On a normal singleplayer world that means cheats must be on.

Producers check before they work. Path finding only records its open and closed node sets when somebody wants paths; entities only collect block intersections when somebody wants them; the neighbour updater’s debug listener is only installed while someone is subscribed. There are two gates of that name and the producers do not agree on which to use: path finding and block intersections ask ServerDebugSubscribers.hasAnySubscriberFor, the live map, while the neighbour updater asks LevelDebugSynchronizers.hasAnySubscriberFor, which reads the level’s snapshot from the previous tick. The change detection, by contrast, is record equality: a brain dump is rebuilt every tick per villager and compared with the last one sent — so the saving is in bandwidth, not in server time.

The other twelve renderers do not use this system at all. The chunk debug renderer reaches directly into Minecraft.getSingleplayerServer and shows nothing in multiplayer; the entity hitbox renderer reaches for it too, but only to check the server has the entity, for its flag-gated server hitbox; the ordinary hitbox it draws itself, switched by an F3 entry rather than a flag, as entity rendering says. And a whole family of them — chunk borders, light, collision boxes, height maps, the section octree — are purely client-side views that need no server.

One flag combination under-delivers, and says so. The POI renderer’s ticket-holder rows are behind an explicit SharedConstants.DEBUG_BRAIN test, so running with the POI flag alone gives POI boxes with their own two labels and none of the brain ones. The bee flag avoids needing the test at all by explicitly also requesting goal selectors.

Subscriptions survive a dimension change and a death, but not a reconnect. A dimension change keeps the same ServerPlayer, so the set is simply never touched; a respawn builds a new one and ServerPlayer.restoreFrom copies the requested set across. A fresh login starts empty, and the client re-sends on its next tick because ClientDebugSubscriber was cleared at login.

The other query, which is one transaction deep

Not everything the client asks the server for is a subscription. Press the copy-recreate-command key — Options.keyDebugCopyRecreateCommand, an ordinary rebindable mapping — while looking at a block or an entity and the client builds a /setblock or /summon for it and puts it on the clipboard. Whether the NBT in that command is yours or the server’s is decided by two things. Holding shift takes the client’s own copy, straight out of the block entity or the entity in front of you. Not holding it goes to the server, through DebugQueryHandler: ServerboundBlockEntityTagQueryPacket or ServerboundEntityTagQueryPacket with a transaction id, a callback parked against that id, and the command assembled when the reply comes back. The handler holds one callback and one id, so a second query cancels the first by overwriting it, and a reply whose id does not match is dropped on the floor. Including the NBT at all needs the gamemaster permission, and the whole key is dead while the server has reduced debug info on.

The sample path, which shares only a subscription

The performance charts are a separate and much simpler system, and what they have in common with everything above is the subscriber side: one of the sixteen subscriptions gates the remote half of them, asked for in the same request and granted by the same permission rule. There is no shared engine, no diff and no tracking filter. A SampleLogger takes a vector of longs; a tick logs its parts as it goes and a final call flushes the whole vector. There are two implementations and the difference is the whole story: LocalSampleLogger is the storage — a SampleStorage ring buffer the charts read directly — while RemoteSampleLogger stores nothing and broadcasts a ClientboundDebugSamplePacket if anyone is subscribed.

So a dedicated server measures its tick with a remote logger and sends, while IntegratedServer.getTickTimeLogger hands back the client’s own local logger and reports logging as unconditionally enabled — singleplayer TPS never touches the network. Client-side, DebugScreenOverlay owns four local loggers with four different feeders: the frame time from the loop, the tick time from either of the two paths above, the ping from PingDebugMonitor (which sends its own ping requests, and only while the network charts are shown), and the bandwidth from a BandwidthDebugMonitor that counts bytes on the Netty thread and is drained by Connection.tick.

Six packets carry the subscriptions and the samples between them: ServerboundDebugSubscriptionRequestPacket outbound, and ClientboundDebugChunkValuePacket, ClientboundDebugBlockValuePacket, ClientboundDebugEntityValuePacket, ClientboundDebugEventPacket and ClientboundDebugSamplePacket inbound — for a system that used to have one packet per subject. The two tag-query packets of the section before are not among them, and are not part of this machinery at all.

For a 1.21-era reader. The fixed set of debug packets is gone. Instead of one packet type per kind of debug information there is one DebugSubscription registry and three generic value packets that dispatch on the registry id. The debug screen is a different system again — the F3 entry registry described in the HUD — and it is not a light touch: the F3 entries decide whether eleven of the twenty-five renderers exist at all when the list is rebuilt, and the FPS charts gate the tick-time subscription.

Where to look

DebugSubscriptions for the catalogue and DebugSubscription for how little a subscription is. TrackingDebugSynchronizer for the engine — the tracking diff and the equality check are in that one class, and the wake-up’s back-fill is LevelDebugSynchronizers’. LevelDebugSynchronizers.tick for the sleep flag, ClientDebugSubscriber for both ends of the client’s half, and DebugRenderer.refreshRendererList for which renderers exist and why.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

XI · Rendering

Verified against Minecraft 26.3 · Part XI · One thread, a hundred-odd times a second, turning a world nobody can see into a picture — and two layers of machinery underneath that never touch the world at all.

The renderer is not allowed to look at the world. A frame is one call to Minecraft.renderFrame, on the same thread that ticked the world a moment earlier, and it runs twice over: once to copy the live game into a pile of value objects, and once to draw those objects with the live game held at arm’s length. Everything in this part is either that wall, or something feeding it, or something on the far side of it working from a copy. And the claim the part makes is that nothing you see on screen is the world; it is a copy of the world, and every seam a player notices is two copies disagreeing. A mob pins in place under /tick freeze while the item in your hand keeps swaying, because each was copied at a partial tick of its own. A chest’s lid stops and the chest in your hand does not, for the same reason one step further out. And terrain fills in outward as you fly because the copy that decides what is visible can only reach as far as the copies that already exist — which is the same disagreement again, now between the world and the meshes that stand in for it.

Besides the renderer’s own per-tick half, three things escape the frame, and each of them is a page: particles are stepped from the tick, most sections are meshed on a background pool, and the atlases are built by a resource reload and animated before each frame.

It is also the largest thing on the client. client/renderer, client/model, client/particle, com/mojang/blaze3d and com/mojang/renderpearl together come to 1,291 files and 98,283 lines — counted the way the atlas counts a part, leaving out the hatched packages — against 358 files and 48,834 lines for net/minecraft/server counted the same way. net/minecraft/server has a little over a quarter as many files as the client’s renderer, and about half its lines.

The shape of the part

Part XI is a substrate under a pipeline. Two of its pages — the window and Blaze3D — are what the renderer stands on: neither has a trace through the world, and Blaze3D is cited far more than it cites. The rest is a pipeline. The frame opens the part ahead of the substrate it stands on, because it is the shortest way to see the whole shape at once and because a reader who has watched one frame end to end has a reason to care what a GpuDevice is.

flowchart TD
    FRAME["1 · The frame"]
    subgraph SUB["the substrate"]
        WIN["2 · The window"]
        B3D["3 · Blaze3D"]
    end
    subgraph PIPE["the pipeline"]
        VIS["4 · Visibility and the frame graph"]
        MESH["5 · Section meshing"]
        MOD["6 · Models and atlases"]
        ENT["7 · Entity rendering"]
        BEN["8 · Block-entity rendering"]
        SKY["9 · Lightmap, fog and sky"]
        PART["10 · Particles"]
        POST["11 · Post-processing"]
        VIS --> MESH --> MOD --> ENT --> BEN --> SKY --> PART --> POST
    end
    FRAME --> SUB --> PIPE

The part as a substrate and a pipeline, numbered to the watch order, the frame first: an arrow is the next lecture, which is not the order a frame runs in.

The arrows in that figure are the order to watch in, which is not the order a frame runs in. Visibility hands section meshing the sections to compile, and a frame’s terrain is drawn from meshes an earlier frame made, so the fourth and fifth lectures feed each other. Inside a frame the order is different — the sky pass is declared before the main one, the lightmap is built before the world is drawn at all, and one of the five post chains runs as passes of the world’s own graph while the other four build one and throw it away.

Before you start

The client loop from Part X, and not optionally: it is the page that says when a frame happens and how many ticks ran before it, and this part begins exactly where that page’s frame zone opens. It is one of the two longest pages in Part X, so budget for it.

The resource system from Part II before models and atlases, which is a reload listener and leans on the barrier semantics taught there rather than restating them.

Environment attributes and timelines from Part IV before lightmap, fog and sky. Part IV owns that system; Part XI is one of its client-side consumers and does not re-teach it.

The client level, for what the thing being drawn is — a Level with its authority removed — and for the two ways LevelExtractor is reached, pushed and pulled.

Watch in this order

  1. The frame — one method, two halves, and a wall between them. Fifteen profiler zones, six clocks disagreeing on purpose, and a failed surface acquisition that costs you the picture but not the work.
  2. The window — the substrate nothing else admits to needing. A retry loop that settles the graphics backend before the window exists, nineteen kinds of window event and the six the game is never told about, and NativeImage, the seam between a file and a texture.
  3. Blaze3D — the game’s own graphics API, and the part’s vocabulary page. Four validating façades over two real backends, one of which is Vulkan and is the larger of the two.
  4. Visibility and the frame graph — what the frame decides to draw, and in what order. A reachability walk whose asymmetry — uncompiled sections stop it, empty ones do not — is why terrain reveals itself outward.
  5. Section meshing — where the triangles came from. A block is placed, a halo of positions goes dirty, the sections it touches are rebuilt from a snapshot — by default inline for your own block, on a worker for the server’s — and the new mesh swaps in all at once.
  6. Models and atlases — the reload pipeline behind block, item and particle quads. Twelve atlases stitched in parallel, one barrier, and a quad whose chunk layer is read out of its sprite’s pixels.
  7. Entity rendering — every entity in the world, in four stages, none of which is called render. The zombie is animated at least twice per frame.
  8. Block-entity rendering — the same four stages with three differences that show. A chest’s block model is empty, a block entity is never frustum-tested on its own, and the chest in your hand is drawn by a different renderer at a different partial tick.
  9. Lightmap, fog and sky — what colour all of it is. One question asked by four of the five renderers, most of which no longer know what time it is.
  10. Particles — the part’s policy page: three distance rules for a particle or a level event, enforced in three places and three readers of one setting who disagree about what its values mean, with a break puff that answers to almost none of them.
  11. Post-processing — the closer. Five JSON-declared shader chains, which is how the pause-menu blur and the creeper spectator shader turn out to be the same machine — and a resource pack can rewrite all five and add its own.

Four and five are a pair — one journey seen from its two ends — and so are seven and eight, the second of which is written as the differences from the first. One to three can be watched in order or in the order one, three, two; the window is the page a viewer is most likely to skip and least likely to regret.

Where the part stops

What draws over the world rather than in it — screens, the HUD, the render tree they record into and the text inside them — is Part X, from the GUI render tree onward; what the server chose to tell this client is Part IX. The packages themselves cross the boundary in both directions: the GUI’s render states and the debug renderers under client/renderer count against Part XI while being Part X’s subjects, and client/resources/model counts against Part X while being the subject of models and atlases.

Everything else left out is the families — one class per mob model, one per renderer, one per particle, one file per GL or Vulkan call site — which is why 41% of the part’s lines are named on no page in the book, the second-highest figure in the book after Part V’s. Teaching each family’s shape and declining its instances leaves a great many lines untouched, and that is the trade, made on purpose.

Reference this part uses

Submit phases and feature renderers is the catalogue behind entity rendering and block-entity rendering: the fifteen phases a submitted feature can land in, and the twelve renderers that write the vertices. The threads for the two that matter here — the Render thread most of the part runs on, and the background pool that meshes sections — and diagram lanes for the abbreviations the figures use. Naming drift is worth having open throughout: the world-facing half of the old LevelRenderer is now LevelExtractor, and many of the names a 1.21-era reader reaches for moved with it. The glossary for extract, render state, frame graph, partial tick, atlas, special model renderer and built-in block model.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The frame

Verified against Minecraft 26.3 · Part XI · one frame: from acquiring a surface texture to handing it back, and the wall in the middle that the drawing half is not allowed to look past.

The number in the top-left says 143 fps, and each of those 143 is one call to Minecraft.renderFrame — one method with two halves. The first walks the live game and copies everything drawable into a GameRenderState; the second draws that state, and reads the live game only where the wall below leaks. Before either half runs, the frame asks the surface the window was made with for somewhere to put the picture, and the surprising thing is what happens when that request fails. It does not skip the frame. It skips the picture. The world still renders in full into GameRenderer.mainRenderTarget, the GUI still goes on top, the framerate limiter still parks; only the blit and the present are guarded on a surface having been acquired, and both are quietly skipped. A minimized window is the same story — a client drawing complete frames that nobody will ever see.

All of this is one thread — the same one that ticked the world a moment earlier. How many ticks ran before the frame, and what paced it afterwards, is the client loop; this page starts where that page’s frame zone opens.

The cast

classwhat it decidesthread
Minecraftthe zones, the acquire guard, and whether this frame advances game time at allRender thread
GameRendererthe frame’s owner of everything drawn — the snapshot target, the main render target, the camera, the lightmapsRender thread
GameRenderStatewhat the drawing half is allowed to read, and therefore where the wall iswritten in extract, read in render
Camerawhere the eye is, how wide the view is, and how wide the cull frustum is — which is not the same numberRender thread, but ticked from GameRenderer.tick
LevelExtractorwhich of the live world becomes drawable state, at one partial tick per entityRender thread
LevelRendererthe world half — its render handed a CameraRenderState, never a CameraRender thread
GuiRendererthe GUI half, with its own StagedVertexBufferRender thread
GpuSurfacewhether there is anywhere to put the picture: acquire, blit, presentRender thread

The zones a frame is made of

The profiler zones in order are the shortest honest description of what a frame is, and the second column is the part a name does not tell you: who pushes it. Eleven of the fifteen are Minecraft.renderFrame’s own. render belongs to GameRenderer.render, which is why the F3 pie chart puts the whole of the drawing half under one slice that Minecraft never named.

zonepushed bywhat happens in it
update windowMinecraft.renderFramethe window’s one per-frame call, a surface reconfigure if one is due, then GpuSurface.acquireNextTexture
updateMinecraft.renderFramethe real-time clock, the GPU timer query when something wants it, Minecraft.pauseIfInactive, the GUI, the client light engine on a ticking frame, then Minecraft.pick
cameraGameRenderer.update, inside updateCamera.update alone
extractMinecraft.renderFramethe wall: the window and the options, then on a frame with a world to draw the lightmap, the camera and the level, then the GUI, copied into GameRenderState
gpuAsyncMinecraft.renderFrameRenderSystem.executePendingTasks drains signalled fences, and nothing else; it is closed before the drawing starts
renderGameRenderer.renderthe resize, the clear, on a frame with a world to draw the lightmap, then the two zones below
worldGameRenderer.render, inside renderonly on a frame with a level: matrices, fog, level, hand, screenEffects, then the outline composite and the post-effect chains
guiGameRenderer.render, inside renderthe GUI, drawn under the one-pixel lightmap
swapchainBlitMinecraft.renderFramethe blit from GameRenderer.mainRenderTarget into the surface texture
tracyCaptureMinecraft.renderFrameonly with Tracy frame capture on: the main target copied out for Tracy
submitMinecraft.renderFrameCommandEncoder.submit, surface or no surface
presentMinecraft.renderFrameGpuSurface.present
endFrameMinecraft.renderFrameTracy’s end of frame if it is capturing, the dynamic uniforms reset, LevelRenderer.endFrame
frameLimiterMinecraft.renderFramethe parking, spending the limit extract snapshotted
fpsUpdateMinecraft.renderFramethe counter behind the number in the top-left

Drawn as calls rather than zones, the same frame shows who calls whom: Minecraft.renderFrame hands GameRenderer its update, its extract and its render, and only the two ends of the frame touch the surface’s texture.

sequenceDiagram
    participant MC as Minecraft
    participant GpuS as GpuSurface
    participant GR as GameRenderer
    participant LX as LevelExtractor
    participant LR as LevelRenderer
    participant GuiR as GuiRenderer

    MC->>GpuS: GpuSurface.acquireNextTexture, if the surface is valid — a throw invalidates it
    MC->>MC: update — clock, pause check, GUI, ClientLevel.update on a ticking frame
    MC->>GR: GameRenderer.update — the camera zone, then the frame's post chains listed
    MC->>MC: Minecraft.pick writes Minecraft.hitResult
    MC->>GR: GameRenderer.extract — the window and the options
    opt there is a world to draw
        GR->>GR: the lightmap and the camera
        GR->>LX: LevelExtractor.extract — the level
    end
    GR->>GR: the GUI
    Note over MC,GuiR: the wall — nearly sealed at LevelRenderer.render, leaky one level up
    MC->>MC: gpuAsync — the signalled fences drained
    MC->>GR: GameRenderer.render — resize, clear
    opt there is a world to draw
        GR->>GR: the lightmap, then the world zone
        GR->>LR: LevelRenderer.render with a CameraRenderState
        GR->>GR: the hand and the screen effects
        GR->>LR: LevelRenderer.blitEntityOutline
        GR->>GR: the post chains
    end
    GR->>GuiR: GuiRenderer.render, then GuiRenderer.endFrame
    opt GpuSurface.isAcquired
        MC->>GpuS: GpuSurface.blitFromTexture from the main target
    end
    MC->>MC: submit — the frame's GPU work, surface or none
    opt GpuSurface.isAcquired
        MC->>GpuS: GpuSurface.present
    end
    Note over MC: the zones endFrame, frameLimiter, fpsUpdate

One frame as the calls Minecraft.renderFrame makes, with GameRenderer’s work under the three it hands over. The surface’s texture is touched only at the two ends, and only the blit and the present are guarded on having one.

Read it as acquire, snapshot, draw, present: everything but the blit and the present is paid for whether or not a surface was acquired.

Acquire, and the frame that carries on without one

The update window zone does three things, and the first belongs to somebody else: Window.updateFullscreenIfChanged at the very top of it — the one per-frame call of the window — then a reconfigure of the surface if it needs one, then GpuSurface.acquireNextTexture. Vsync is a GpuSurface.PresentMode in that configuration, which the OpenGL surface turns into a swap interval, so toggling it in the options calls Minecraft.invalidateSurfaceConfiguration, and the next frame’s update window reconfigures the surface.

When the acquisition throws, the surface is marked invalid, a reconfigure is scheduled, and the frame goes on as if nothing had happened but for a line in the log. Exactly two later statements re-test whether a surface is held, and both are in Minecraft.renderFrame itself: the blit and the present are each guarded on GpuSurface.isAcquired, with nothing on the other branch. The tolerance is the caller’s, not the surface’s — GpuSurface.blitFromTexture and GpuSurface.present both throw, in the façade the game holds, if you reach them without one. Everything before those two guards — the extract, the world, the GUI — is paid in full.

There is one guard on the whole method, and it is about re-entry rather than failure: if the surface is already acquired when Minecraft.renderFrame is called, the call is a silent no-op.

What a minimized client actually stops doing

On the OpenGL backend, a minimized window is the same story after one refused acquire. Minecraft.renderFrame holds any surface reconfigure back until the window is restored, and GlSurface.acquireNextTexture refuses an iconified window outright, so the first minimized frame fails to acquire, the surface is marked invalid, and no acquire is tried again until the window comes back. The three statements that drop out are the acquire and the two guarded ones — which is nothing next to what the frame still pays for. The saving comes from somewhere else entirely. FramerateLimitTracker.getThrottleReason tests iconification first, ahead of idleness and the menu, and FramerateLimitTracker.getFramerateLimit turns that answer into a limit of ten — so frameLimiter parks the thread for most of every hundred milliseconds and the client draws its unseen frames about ten times a second instead of at the player’s setting. On top of that, losing focus for half a second, with no screen up and pause on lost focus on, opens the pause menu through Minecraft.pauseIfInactive, and that menu’s vote stops a singleplayer world not opened to LAN, so every frame after that draws a world that has stopped.

Update and extract: six clocks in one frame

The update zone advances the real-time clock, reads Minecraft.timerQuery — a TimerQuery, the GPU-side stopwatch behind the F3 utilisation figure. It brackets almost the whole frame, from here through the blit, runs only while the debug screen’s GPU line is shown or a metrics recording is running, and is only started when the last one has been collected, so not every frame is measured — runs Minecraft.pauseIfInactive and updates the GUI. Then GameRenderer.update calls Camera.update — that one call is the camera zone — and Minecraft.renderFrame follows it with Minecraft.pick, a private method of Minecraft and not the renderer’s, which writes Minecraft.hitResult for the crosshair and the block outline to find later.

Behind all of this sits the flag the rest of the page keeps leaning on. Minecraft.renderFrame is passed a boolean saying whether this frame advances game time, and a frame passed false is a non-ticking frame: no ticks ran before it, ClientLevel.update does not run the client’s own light engine, and the whole world half of the drawing is skipped, leaving a GUI-only picture — the half GUI and screens owns. Four call sites always pass false: the Minecraft constructor, once, as it finishes; the two loops that wait for the integrated server to start and to stop; and Minecraft.setScreenAndShow, which forces a single frame so that a screen appears during blocking work. And the loop’s own call passes false for every frame after the client has recovered from running out of memory. Every other frame is a ticking one, and the term means only that.

Camera is split three ways across the client, and two of the three are here. Camera.tick — driven from GameRenderer.tick, not from the frame — smooths the eye height and the field-of-view modifier and advances the camera’s EnvironmentAttributeProbe. Camera.update does the frame’s work: Camera.alignWithEntity, Camera.calculateFov, Camera.prepareCullFrustum, Camera.setupPerspective. Camera.extractRenderState then copies the result across the wall.

Six partial ticks, and none of them is the one

Extract opens by snapshotting the framerate limit into GameRenderState.framerateLimit and goes on to copy the window and the options, and on a frame with a world to draw the lightmap, the camera and the level. It is here that the frame’s oddest number appears: there is no such thing as the partial tick of a frame. There are six, they disagree on purpose, and two of them are not partial ticks at all.

who is interpolatedwhich valuewhat it ignores
the worldDeltaTracker.getGameTimeDeltaPartialTicknothing — frozen time is honoured
the camera, and the held item as it is posedCamera.getCameraEntityPartialTicksfreezing, unless the camera entity itself is frozen, which a player never is
the lightmapa literal oneeverything — it is extracted fully advanced
screens and overlaysDeltaTracker.getGameTimeDeltaTicksthe fraction — this is the whole delta since the last frame, not a position inside a tick
the autosave indicator and the menus’ panoramaDeltaTracker.getRealtimeDeltaTicksthe game clock, and anything past seven ticks, where it clamps to a half
each entity, separatelyits own frozen-honouring value, asked for by LevelExtractorthe world’s single answer

The last row is the one you can see. Under /tick freeze a mob pins at the end of its last tick while players go on interpolating — in the same frame, from the same extract. The exception is worth knowing, because it is the one a player rides: TickRateManager.isEntityFrozen excludes anything with a player aboard, so the horse under you keeps moving smoothly while the horse beside you is a statue.

The wall, and the one level at which it is real

LevelRenderer.render is handed state and reads no live game state: no ClientLevel, and nothing from Minecraft but the level renderer itself, which it reaches through Minecraft.getInstance. That is the wall, and one level down it holds but for a few reaches by reference: a moving block’s render state carries the level’s light engine and is lit from it at prepare, a sign’s carries the block entity’s own text (block-entity rendering), and a section compiled on the spot reads tints and light live through its region (section meshing). Beyond those it reaches back only into live renderer objects, for the main target and the shader manager.

At the top it leaks, and the leak is sharper than the naming suggests. GameRenderer.render takes whether there is a world to draw from the snapshot, as GameRenderState.shouldRenderLevel, but inside the world half GameRenderer.shouldRenderBlockOutline reads the live camera entity during rendering, and in adventure or spectator mode goes further: for a player who may not build it reads Minecraft.hitResult, asks the game mode what it is and looks a BlockState up in the level, all mid-draw. It is not the only reach: the boss bar’s world fog, the hand’s check for spectator mode and the world-icon screenshot read live state inside GameRenderer.render too. The interesting fact is not that a wall exists but that it is drawn one level below where extract then render implies it is.

The resize the snapshot has already decided

A resize is handled inside the render half rather than before it: GameRenderer.render opens by comparing GameRenderState.windowRenderState against the main target’s size and resizing the renderer inline when they differ. The snapshot is what the frame believes the window size to be, even if the window has changed since.

What the drawing half holds, which is four buffers and a storage per drawer

The buffers are few. RenderBuffers holds RenderBuffers.fixedBufferPack and the section-meshing scratch RenderBuffers.sectionBufferPool — capped by processor count and again by a memory budget — which only section meshing uses, and a single RenderBuffers.stagedVertexBuffer released by RenderBuffers.endFrame, which the feature dispatcher writes every prepared feature into, with the GUI keeping a second staged buffer of its own inside GuiRenderer. Geometry submission goes through SubmitNodeCollector into a SubmitNodeStorage, one per drawer (entity rendering owns what a submission is), drawn either inside the frame graph’s main pass in visibility and the frame graph or by FeatureRenderDispatcher.renderAllFeatures — which, despite the name, is not how the level is drawn. It has four call sites: the held item, the screen effects, the GUI’s item atlas and picture-in-picture. The last two are why the GUI needs submit storage at all. The world’s submitted features are prepared into the frame graph and drawn inside its main pass.

The hand and the screen effects, in a storage the level never sees

The first two of those four call sites share a storage of their own. GameRenderer.handAndScreenSubmitNodeStorage collects both GameRenderer.renderItemInHand — which hands the hand to FirstPersonHandsAndItemsRenderer, drawn under its own projection after a depth clear so a held sword never intersects the world — and, in the screenEffects zone that follows it, ScreenEffectRenderer: the underwater overlay, the fire quad when you are burning, the sprite of whatever block your head is inside, and the item-activation flourish a totem plays. Each is submitted, prepared and drawn inside its own zone, and neither ever reaches the level’s frame graph.

Blit, submit and present

The picture leaves the frame in three zones. swapchainBlit does the blit from GameRenderer.mainRenderTarget to the acquired surface texture; submit closes the frame’s GPU work with CommandEncoder.submit, surface or no surface; and present hands the texture back with GpuSurface.present, which on the OpenGL backend is a single call to SDL’s SDL_GL_SwapWindow. With Tracy frame capture on, a tracyCapture zone between the blit and the submit copies the main target out for Tracy.

The last three zones are bookkeeping. endFrame ends Tracy’s frame if Tracy is capturing, resets the dynamic uniforms and calls LevelRenderer.endFrame. frameLimiter spends the limit that extract snapshotted into GameRenderState.framerateLimit, parking only below a threshold, so the top slider position parks only when FramerateLimitTracker has capped or replaced the limit. What that limit is — and the four cases in which FramerateLimitTracker has already capped or replaced the player’s option — belongs to the client loop; this page only spends it. Then fpsUpdate, and the frame is over.

Questions players ask

Why does lowering a spyglass never reveal a hole in the world? Because the cull frustum is deliberately wider than the camera. Camera.createProjectionMatrixForCulling builds its matrix from the larger of the current and the configured field of view; Camera.prepareCullFrustum turns that into Camera.cullFrustum, and Camera.extractRenderState copies it across the wall into CameraRenderState.cullFrustum. Sprinting and flying raise the live FOV above the option, so for them the maximum does nothing. It bites when something narrows the view — a spyglass, a drawn bow, the dying-camera effect — where culling against the configured FOV keeps the geometry a narrowed view would have thrown away.

Why is the HUD not shaded by the light the player is standing in? Because for the whole of the gui zone GameRenderer.lightmap hands out a one-pixel white texture instead of the real one. GameRenderer.useUiLightmap is set on either side of exactly that zone and nowhere else; which two textures it is switching between belongs to lightmap, fog and sky.

Why does the world go strange when spectating a creeper? Because the post-effect chain is chosen by what you are spectating rather than by an option, at the end of the world block and before any GUI. Which chain, through which door, and what each does to the picture belong to post-processing.

Where does the main menu’s panorama come from? From six images loaded as one cube-map texture and drawn as a turning cube, and the game can take the six itself on the same two halves, though only with a debug flag set: Minecraft.grabPanoramixScreenshot runs GameRenderer.update, GameRenderer.extract and GameRenderer.renderLevel six times at 4096×4096, with a fixed delta of one and the camera in panoramic mode — which is what CameraRenderState.isPanoramicMode exists for. A second, silent screenshot path inside the world half writes the world icon, singleplayer only, and only once enough sections have been rendered.

For a 1.21-era reader. The frame is now extract then render: GameRenderer.extract copies the live game into a GameRenderState, and LevelRenderer.render is handed a CameraRenderState rather than a Camera. Names to stop hunting for: Minecraft.getMainRenderTarget (now GameRenderer.mainRenderTarget), Camera.setup (now Camera.update plus Camera.extractRenderState), GameRenderer.getProjectionMatrix, Window.updateDisplay, LightTexture (now Lightmap), ItemInHandRenderer (now FirstPersonHandsAndItemsRenderer) and MultiBufferSource.

Where to look

Minecraft.renderFrame — the frame is one method, and its profiler zones are its table of contents. Then GameRenderer.extract and GameRenderer.render for the wall between live objects and drawing, GameRenderer.tick for the per-tick half nobody expects a renderer to have, Camera.update for how the view is decided, and GpuSurface.present for where a frame ends. The GPU abstraction underneath all of it has its own page, blaze3d.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The window

Verified against Minecraft 26.3 · Part XI · before the first frame: the game finds out which graphics backend it has by which device survives, and only then asks the operating system for a window.

The process is a second old. There is no world, no renderer, no resource pack and nothing to draw. RenderSystem.initBackendSystem has just brought SDL up, NativeLibrariesBootstrap has probed for the GL and Vulkan loaders and MonitorManager has enumerated the monitors and the video modes each of them offers. Now Minecraft wants a window — and it cannot ask for one without already having decided how the pixels will be drawn, because an OpenGL window and a Vulkan window are asked for with different flags. So the device comes first and the window second: each candidate backend in turn loads its library and tries to build a whole device before the game has any window at all — OpenGL’s device makes a hidden window of its own to hold its context — and only the backend that survives is asked for the window, once. A window that fails at that point is not handed to the next candidate; it ends the game with a crash report.

That loop is the first half of the page. The second is what the window turns into once it exists: this is Part XI’s platform layer, not just its window — the nineteen kinds of event the window answers, the three sizes every GUI element is placed against, and NativeImage, the CPU image type every texture, screenshot and skin, and every glyph but the unihex font’s, passes through on its way to or from a file. They share a package and a role rather than a scenario, and the role is everything between the game and the machine it is running on.

The frame is the lecture you watch first, and it opens on a surface that has already been acquired. This page is what created it. Input and keybinds opens on an event that has already arrived, and blaze3d on a GpuDevice that already exists. All three of them start here.

The cast

classwhat it decidesthread
Minecraftwhich backends to try, in which order, and when to give upRender thread
Windowthe SDL window handle, the three sizes, and every fullscreen transitionRender thread
GpuBackendits library, its device, and the flags a window for it is made withRender thread
MonitorManagerwhich monitors the game knows about, kept current by SDL’s display eventsRender thread
Monitorwhich VideoMode an exclusive fullscreen switch asks forRender thread
WindowEventHandlerwhat the window tells the game: size, cursor and fullscreenRender thread
FramerateLimitTrackerwhat an iconified, idle or menu-bound window is allowed to costRender thread
NativeImagethe CPU-side pixels between a file and a texturenative memory, closed by its owner

Two of those rows live outside com/mojang/blaze3d/platform and the other six in it: Minecraft is the game’s own, and GpuBackend sits in renderpearl/api with the façades. None of the package exists on the server — server-classes.txt has no entry under com/mojang/blaze3d at all — and most of it runs on the Render thread, which is one of the four: images are decoded and mipmapped on reload workers (a downloaded skin on the download pool) and written on the IO pool, and the shutdown watchdog waits on a thread of its own.

Trying backends until one of them makes a device

The startup path is a retry loop, and it is drawn as a flowchart rather than a conversation because the shape is the fact: the loop encloses the device and leaves the window outside it. A backend that cannot load its library and a backend that cannot make a device land in the same handler, which unloads the library only in the second case, and both hand the next candidate a clean slate.

flowchart TD
    START["RenderSystem.initBackendSystem starts SDL, MonitorManager lists the displays"]
    subgraph LOOP["each candidate in turn"]
        NEXT{"a candidate left?"}
        LIB["GpuBackend.loadLibrary"]
        Q1{"did it load?"}
        DEV["GpuBackend.createDevice, with no window yet"]
        Q2{"a device?"}
        UNL["GpuBackend.unloadLibrary"]
    end
    BOX["MessageBox.error, and the game never starts"]
    RS["RenderSystem.initRenderer with that device"]
    WIN["the one Window, from GpuBackend.createWindow"]
    Q3{"a window?"}
    CRASH["a crash report, and no next candidate"]
    DONE["Window.setIcon, then GpuDevice.createSurface on its handle"]
    START --> NEXT
    NEXT -- "yes" --> LIB --> Q1
    Q1 -- "yes" --> DEV --> Q2
    Q1 -- "no" --> NEXT
    Q2 -- "no" --> UNL --> NEXT
    NEXT -- "no" --> BOX
    Q2 -- "yes" --> RS --> WIN --> Q3
    Q3 -- "yes" --> DONE
    Q3 -- "no" --> CRASH

The startup retry loop, the box being the loop: a library or a device that fails sends the next candidate in, and the loop is left by a device that survives or by running out of candidates. The window is made once, outside the loop, and a window that fails is a crash rather than a retry.

The list is never one candidate long. PreferredGraphicsApi.getBackendsToTry returns an ordered pair: every setting has the other API behind it as a fallback, the default is OpenGL-first, and GlBackend and VulkanBackend are the two things the loop above is choosing between. A start that ended before it finished loading, by a crash or otherwise, downgrades the preference one step on the next start — a Vulkan preference becomes the default, and the default becomes OpenGL — so a client that keeps crashing on boot comes down to the safest option it has, and stays there until you set it again. That is why asking for Vulkan does not always get it.

What the window is asked for is a DisplayData: a size, an optional fullscreen size and a fullscreen flag, with DisplayData.withSize and DisplayData.withFullscreen applying the saved size and fullscreen options, or, after a start that never finished loading, forcing windowed, before the window is made. What comes back is a Window holding a Window.handle — and neither a GpuDevice nor the backend that made it. The window uses the backend once, to be created, and the device meets the window only afterwards, when GpuDevice.createSurface is handed its handle.

SDL, STB and FreeType, reached through LWJGL, are most of what the package sits on, and it calls almost nothing else in the game on its way down. The exceptions are about passing something upward, an event or a failure: SDLEventHandler hands the input up, and a failure goes to Minecraft, CrashReport, and the one piece of the dedicated server the client borrows — ClientShutdownWatchdog builds its report with ServerWatchdog.createWatchdogCrashReport, which is the only net.minecraft.server.dedicated name anything here touches. Above the package, Minecraft drives startup and the two per-frame calls below, KeyboardHandler and MouseHandler take the input events and the clipboard, VideoSettingsScreen drives the fullscreen and video-mode controls, and Screenshot and TextureManager want NativeImage. What the player’s saved choices reach is Options: an override width and height, the fullscreen flag and video-mode string, exclusive fullscreen, the GUI scale, and the graphics-API preference that ordered the loop above. The window’s position is not among them — it is a field the move event keeps and nobody saves.

How the loop knows why a device did not appear

Minecraft has to be able to say why every candidate failed, because it may have to show the player. A failed attempt arrives as a BackendCreationException, carrying a message and a BackendCreationException.Reason, and where the failure is SDL’s — a library that would not load, a context or a window that would not appear — the backend reads SDL_GetError on the spot and writes it into the message. The loop appends each attempt’s name and message to the text MessageBox.error shows if nobody survives, and keeps one exception in Minecraft.backendCreationException for the crash report and the telemetry, where an OpenGL-missing failure never displaces an earlier one.

Errors are read where they happen: where the game checks an SDL call’s result it mostly reads SDL_GetError there and then, or once in the method that made the call, and what SDL logs of its own accord goes to the game’s log through SdlDebug, which RenderSystem.initBackendSystem installs before SDL starts. Window.setErrorSection writes a label for the crash report — Startup from the constructor, then Pre render, Render and Post render on every pass of the loop — which is why a crash report’s Window category names the phase the game was in.

Nineteen events, and the six the game is never told about

The window registers nothing with the operating system. At the top of every pass of the client loop, RenderSystem.pollEvents runs SDLEventHandler.pollEvents, which drains SDL’s queue: the keyboard, mouse and drop events it deals with itself, and everything else goes to Window.handleEvent, which answers nineteen kinds of event and ignores the rest. WindowEventHandler — a four-method interface that Minecraft implements — is nearly the whole of what a window says back to the game, and the window only ever reaches for three of those four methods.

flowchart LR
    SEH["SDLEventHandler.pollEvents, once per loop pass"]
    IN["keys, text, mouse, dropped files"]
    KMH["KeyboardHandler and MouseHandler"]
    WHE["Window.handleEvent, nineteen kinds"]
    TOLD["pixel size, display or mode, cursor, fullscreen — seven"]
    WEH["WindowEventHandler, implemented by Minecraft"]
    IC["minimised, maximised, restored — three"]
    MC["Minecraft.invalidateSurfaceConfiguration"]
    QT["quit, close, terminate — three"]
    CB["Window.shouldClose, and the close callback"]
    LOOK["moved, resized, focus in or out — four"]
    W["a Window field, looked up later"]
    DS["display added or removed — two"]
    MM["the monitors MonitorManager keeps, looked up later"]
    SEH --> IN --> KMH
    SEH --> WHE
    WHE --> TOLD --> WEH
    WHE --> IC --> MC
    IC --> W
    WHE --> QT --> CB
    WHE --> LOOK --> W
    WHE --> DS --> MM

The poll’s one fork and the window’s five groups of event, with how many of the nineteen kinds each group holds. Input never reaches the window, and the moves, resizes, focus changes and displays arriving or leaving are the six the game is never told.

WindowEventHandler.framebufferSizeChanged, WindowEventHandler.cursorEntered and WindowEventHandler.fullscreenStateChanged are the three. A new pixel size, a move to another display and a change of the display’s mode all end in the first; the cursor crossing the window’s edge, in either direction, in the second; and the operating system taking the window into or out of fullscreen in the third, which writes the new state back into the fullscreen option. A move, a resize, focus gained or lost and a display arriving or leaving end in a field or in MonitorManager — Window.getX, Window.getY, Window.getScreenWidth, Window.isFocused and MonitorManager.getMonitor are what anyone asks instead, whenever they get round to it — so six of the nineteen are things the game is never told, only things it can look up. Minimising reaches the game outside the interface: Window.isIconified reads a field like the others, but Window also calls Minecraft.invalidateSurfaceConfiguration itself, on the way down and on the way back. While the flag is set the frame will not reconfigure its surface, and the surface was handed the same flag when GpuDevice.createSurface made it, which the OpenGL surface uses to refuse an acquire; otherwise the frame runs in full, and where the real saving comes from is told in the frame.

The fourth method on the interface is the odd one. WindowEventHandler.resizeGui is never called by Window at all: its callers are Minecraft and Options, which is to say the game calling itself — at startup, after every new framebuffer size, and when the GUI scale or a font option changes. And WindowEventHandler.framebufferSizeChanged is not only an event’s answer — Window.updateFullscreenIfChanged, Window.changeFullscreenVideoMode and Window.setExclusiveFullscreen all raise it directly, which is how F11 and a video-mode switch reach the renderer by the same route a dragged window corner does.

Notice what is not among the nineteen: keys, characters, mouse buttons, cursor motion, scrolling and dropped files. SDLEventHandler.pollEvents takes all of those off the queue before the window sees any and hands them to KeyboardHandler and MouseHandler — see input and keybinds.

The one callback, which is not the constructor’s

The window does hold one callback, and Minecraft sets it after the constructor has run: the window-close callback, the one that runs when you click the X rather than quitting from the menu — the close request is one of the nineteen events, and Window answers it by setting Window.shouldClose and running the callback. It is registered late because what it does is not the window’s business at all — it is one of the two places ClientShutdownWatchdog is armed, and it is the one live through the teardown that follows a close from the window, which the other arming, after Minecraft.exitWorldAndClose returns, never sees. That is one reason the game sometimes leaves a crash report behind after you close it. The two armings and what each may do are the client loop’s.

Three sizes, and every misplaced GUI element is a confusion between them

the sizehow it is asked forwhat it is
framebufferWindow.getWidth, Window.getHeightthe pixels the renderer targets
screenWindow.getScreenWidth, Window.getScreenHeightthe window as the operating system reports it, which under DPI scaling is not the framebuffer
GUI-scaledWindow.getGuiScaledWidth, Window.getGuiScaledHeightthe framebuffer divided by an integer scale

The integer scale is the part with a policy in it, and what the option asks for is a ceiling the game is allowed to miss in both directions. Window.calculateScale counts upward from one for as long as the framebuffer divided by the next scale would still be at least Window.BASE_WIDTH by Window.BASE_HEIGHT — 320 by 240 — so on a small window it stops below what you asked for. Then, if the font needs unicode and the answer came out odd, it adds one, which is the one case where the result comes back above the ceiling. Window.setGuiScale takes whatever that was and stores it, deriving the two scaled sizes by dividing and rounding up. A high-DPI display is what makes the first two rows diverge, and a GUI element that lands in the wrong place is nearly always code that read one of the three and meant another.

What the window does per frame, which is almost nothing

Two calls, both inside Minecraft.renderFrame, both in the update window profiler zone: Window.updateFullscreenIfChanged at the very top of it, and immediately after it a reconfigure-and-acquire of the GpuSurface — which is renderpearl’s object, not the window’s (see Blaze3D), and is the whole of the window’s involvement in getting a picture onto the screen. Everything else the window does is answering an event.

Window.updateFullscreenIfChanged is where F11 lands. Window.setFullscreen, which the fullscreen option calls, sets the request, Window.setWindowed applies one at once, WindowEventHandler.fullscreenStateChanged reports the outcome back to the option, and Window.changeFullscreenVideoMode with Window.getPreferredFullscreenVideoMode and Window.setPreferredFullscreenVideoMode negotiate what exclusive fullscreen turns into. Dragging the window to the other monitor is the same machinery approached from the other end: MonitorManager.findBestMonitor asks SDL which display the window is on, and Monitor.getPreferredVideoMode looks for the saved preference among the modes this monitor offers, taking the monitor’s current mode when there is no exact match — the monitor never approximates; SDL does, afterwards, when Window asks it for the closest mode it can set, and an exclusive switch that finds none falls back to borderless. A Monitor is a record — a name, an SDL display id, its list of VideoModes, the current one and its bounds — and Window.getActiveVideoMode, the mode the window is showing, is where the debug screen’s refresh rate comes from.

The one policy on this page that runs continuously is FramerateLimitTracker, and what it watches is the window’s iconification, and its focus only in exclusive fullscreen, where an unfocused window is throttled as if it were iconified — anywhere else, losing focus is a different mechanism with a different effect. Beside the window it watches the idle time since the last input, when the inactivity option asks it to, and whether a menu is up with no world. What it does with that, and the four limits it caps or substitutes, belongs to the client loop; the frame is where the limit gets spent.

NativeImage, the seam between a file and a texture

NativeImage is a NativeImage.Format, a width, a height and a pointer into native memory. It is where every image the game reads from or writes to a file briefly is, the unihex font’s glyphs aside, and it is not only textures. NativeImage.read is an STB decode from a stream, a byte array or an NIO buffer — that is the PNG path. NativeImage.copyFromFont receives a rasterised FreeType glyph. NativeImage.copyRect and NativeImage.fillRect are how one image is cut out of or patched into another — an Unstitcher source slicing a sheet apart before it is ever stitched, or SkinTextureDownloader folding a legacy 64×32 skin into the modern layout — while NativeImage.resizeSubRectTo has exactly one caller in the game, the world icon being scaled down. NativeImage.mappedCopy, NativeImage.getPixel and NativeImage.setPixel are the rest of the vocabulary. A downloaded skin sits in one while it is being validated, and a screenshot arrives in one read back off the GPU on its way to NativeImage.writeToFile. What it is not is where an atlas is built: an atlas is assembled on the GPU, sprite by sprite, as models and atlases explains.

NativeImage.computeTransparency is what a stitched sprite’s contents are scanned with, and it is the reason a quad’s chunk layer is read out of its sprite’s pixels — a rendering decision made by looking at the pixels of a file.

Because the memory is native, ownership is explicit: NativeImage.close frees it, and NativeImage.untrack only takes an image out of LWJGL’s leak tracking, for the few special glyphs kept for the life of the process.

The corners the story does not pass through

Two of them are worth a sentence each because a player meets them. The cursor stops changing shape when Window.setAllowCursorChanges is clear — it is driven by a player option on the mouse-settings screen and nothing else, and with it clear every request is answered with the default arrow; the other half of that problem is the platform’s, since CursorType.createStandardCursor takes a fallback for the shapes a given system does not provide. And TextureUtil, the odd one out because nothing about it is a window, is why a mipmapped texture’s edges do not bleed: before MipmapGenerator builds a sprite’s mip chain it runs one of two repairs over it, when the sprite is not an item’s and its mipmap strategy asks for one: TextureUtil.solidify floods the nearest opaque colour outward into every fully transparent pixel, and TextureUtil.fillEmptyAreasWithDarkColor fills them with a darkened copy of the image’s darkest colour instead. The first is the one that stops the bleed, since a plain average of four texels down a mip level would otherwise pull in whatever colour the transparent ones held.

The rest of the package is genuinely a list, and the class index is where a list belongs: clipboard and IME text, the cursor shapes, the window icon set, the macOS and memory-tracking helpers, Lighting’s buffer of diffuse-light directions, and InputConstants, the key and mouse-button vocabulary every KeyMapping is written in. Transparency, which sits here too, is the answer NativeImage.computeTransparency gives, not pipeline state — that belongs to Blaze3D, in renderpearl.

For a 1.21-era reader. GLFW is gone; SDL3 does its work, and GLX went with it. The window no longer presents anything: Window.updateDisplay and Window.updateVsync are gone, presentation is the GpuSurface protocol (blaze3d) and vsync is a GpuSurface.PresentMode. ScreenManager is now MonitorManager, in the same package (naming drift). The constructor now takes the GpuBackend that makes the window.

Where to look

Minecraft’s constructor for the candidate loop, what happens when it runs out of candidates, and the one window made after it. SDLEventHandler.pollEvents and Window.handleEvent for where each event goes, then Window.updateFullscreenIfChanged for the one call the window gets on every frame. MonitorManager.findBestMonitor and Monitor.getPreferredVideoMode for the fullscreen negotiation. NativeImage.read and NativeImage.computeTransparency for the image type the rest of Part XI is built on.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Blaze3D

Verified against Minecraft 26.3 · Part XI · one draw call, from a declared pipeline to a triangle — and the two backends that can serve it.

Open Video Settings, change Graphics API, restart, and the game comes back looking exactly as it did. Nothing in the net.minecraft packages noticed, because nothing in them talks to a driver: they talk to GpuDevice, CommandEncoder and RenderPass, and a backend under com/mojang/renderpearl/backend/opengl or com/mojang/renderpearl/backend/vulkan turns that into real calls. What made the swap possible is that the state machine left the game. Blend mode, depth test, cull and polygon mode are fields of a RenderPipeline, declared once and applied by the backend (by OpenGL at the first draw that uses the pipeline, by Vulkan when it is bound), and RenderSystem — the class that used to be the state machine — contains no GL call at all. It did not vanish, though. It moved behind the backend boundary, where GlStateManager still shadows the enable-and-disable toggles and still elides the redundant ones.

Nothing in this page chooses which backend that is. The choice is made in Minecraft, before Blaze3D exists, and which candidates it tries and in what order belongs to the window — which is also why the option is a preference rather than an instruction. This page is the vocabulary of the boundary once one of them has won; the frame that drives it is the frame.

The cast

classwhat it decidesthread
RenderSystemthe static holder: the device, the Render thread, the frame’s shared uniforms and buffersRender thread
GpuBackendwhich API is alive — its library, the device, creation errors, and then the windowRender thread
GpuDevicewhat exists, and what the hardware will allowRender thread
CommandEncoderwhether a pass may open, and with which attachmentsRender thread
RenderPassthat the pipeline matches the attachments before a draw is allowedRender thread
GpuSurfacehow a finished frame reaches the screen, and what vsync meansRender thread
RenderPipelinehow to rasterise: shaders, blend, depth, cull, topology — declared, never calleddeclaration only, compiled on workers, bound on the Render thread
BufferBuildervertex data — on the Render thread, except the builders chunk meshing runs on workersRender thread, and the meshing workers

Four objects the game only touches through a façade

The four are interfaces in renderpearl/api, and behind each sits one concrete, validating class in renderpearl/frontend over one thin per-backend interface. The figure draws those three rungs for one of the four, and the table after it names them for all four: the game holds the left column, never the other two.

classDiagram
    class RenderPass {
        <<interface>>
        what the game holds
        setPipeline()
        drawIndexed()
    }
    class FrontendRenderPass {
        checks the API contract
        RenderPassBackend backend
    }
    class RenderPassBackend {
        <<interface>>
        one implementation per API
        setPipeline()
        drawIndexed()
    }
    RenderPass <|.. FrontendRenderPass
    FrontendRenderPass *-- RenderPassBackend
    RenderPassBackend <|.. GlRenderPass
    RenderPassBackend <|.. VulkanRenderPass

One of the four façades as the objects behind it: the game holds a RenderPass, which is a FrontendRenderPass wrapping one backend’s RenderPassBackend. The API’s contract is checked in FrontendRenderPass, and only the bottom row, GlRenderPass or VulkanRenderPass, knows which API is running.

the game holdswhich checksthe backend implements
GpuDeviceFrontendGpuDeviceGpuDeviceBackend
CommandEncoderFrontendCommandEncoderCommandEncoderBackend
RenderPassFrontendRenderPassRenderPassBackend
GpuSurfaceFrontendGpuSurfaceGpuSurfaceBackend

GpuBackend is the entry point and the exception: no façade, because it is what exists before a device does, and it makes the device and the window. Everything else the device creates, or its encoder does, and the device answers for the hardware too — as one record graph, not a pile of getters. GpuDevice.getDeviceInfo returns a DeviceInfo whose fields include DeviceInfo.name, DeviceInfo.backendName, DeviceInfo.isZZeroToOne, a DeviceFeatures of eight booleans, a HintsAndWorkarounds, a DeviceType and a DeviceLimits whose DeviceLimits.maxTextureSize, and what its DeviceLimits.maxMemoryAllocationSize can hold, cap the window size.

That record is sniffed as well as queried, which is why the game cares which GPU you have when it could simply ask the driver. GlHeuristics reads the renderer and vendor strings to guess the device type, flags GL-over-D3D12 — assumed on Windows-on-ARM whatever the string says — and flags AMD for anisotropy problems: the first changes how the backend maps buffers and stages chunk uploads, the second which filtering a graphics preset picks. The backend probes the reported maximum texture size rather than trusting it too, halving a proxy allocation until the driver accepts one.

Who checks what

The façade owns the API-contract checks and the backend owns the state of its own resources. FrontendCommandEncoder.createRenderPass validates the attachment count against DeviceLimits.maxColorAttachments, each attachment’s GpuTexture.USAGE_RENDER_ATTACHMENT bit, that the attachments are all one size, that the render area fits, and that no pass is already open. FrontendRenderPass.setPipeline checks the pipeline’s ColorTargetState list against the pass’s attachments in both count and GpuFormat. The GpuBufferSlice overload of FrontendRenderPass.setUniform checks the offset against DeviceLimits.minUniformOffsetAlignment — the plain GpuBuffer overload feeds it the whole buffer at offset zero, which cannot fail.

Underneath, the backend throws on its own account: GlBuffer for a buffer mapped without persistent-mapping support, unreadably, unwritably or over two gigabytes, and GlDevice GpuOutOfMemoryException on a failed allocation. The development-only checks are the façade’s too, behind one flag: the only validation in this whole tree gated on running from an IDE is FrontendRenderPass’s draw-time check, behind FrontendGpuDevice.STRICT_VALIDATION. Everything else the façade asserts, it asserts in a shipped game, and the gated half is the one that bites: a draw that breaks one of its rules — a missing uniform, an index or uniform buffer used against its usage bits, a pipeline that wants a depth texture the pass lacks — goes to the backend unchecked in a shipped game.

The thread assertions are not where a reader expects them either. RenderSystem.assertOnRenderThread is called from nine classes, eight of those sites inside RenderSystem itself: it guards RenderSystem’s own mutable statics, the GL- and SDL-facing classes, the render targets and one texture helper, while FrontendGpuDevice, FrontendCommandEncoder and FrontendRenderPass assert nothing at all. And GpuDevice.createCommandEncoder does not create an encoder — FrontendGpuDevice hands back the one FrontendCommandEncoder it built over the backend’s one encoder, so the game can call it fresh at every use site and the is a pass open guard still sees every pass.

How tight the boundary is

OpenGL is imported by exactly fifteen files — fourteen in com/mojang/renderpearl/backend/opengl, the fifteenth the native-library bootstrap — and nothing else in the game references LWJGL’s OpenGL bindings. Vulkan gets nearly the same treatment: outside com/mojang/renderpearl/backend/vulkan, exactly two files import its bindings — the same native-library bootstrap, and FrontendRenderPass, which borrows two Vulkan indirect-command structs for their size when it validates an indirect buffer, a small leak OpenGL has no counterpart to. The boundary is thinner still than that suggests in one direction and thicker in the other: BackendCreationException, in renderpearl/api where the game can see it, names eleven ways a backend can fail to be created and eight of the eleven are Vulkan’s, so the neutral façade layer knows rather a lot about one of the two APIs it is meant not to. Graphics is not all of it either, and neither is the Render thread: com/mojang/blaze3d/audio is the OpenAL wrapper, whose per-source calls run on the sound engine’s own thread — see the sound engine.

Vulkan is not a stub

7,387 lines against 5,815 — the Vulkan backend against the OpenGL one, thirty-three files against twenty-nine.

It is the larger of the two trees: a real swapchain, pipelines built straight from the SPIR-V the shared compiler produces, five required device extensions, nine required features, and vendor-specific GPU crash breadcrumbs in vulkan/checkpoints that OpenGL has no answer to. VulkanBackend.checkBackendAvailable says why it is unavailable, though only the default preference consults it.

The backends differ in all eight DeviceFeatures flags, and only one difference is symmetric: Vulkan hardcodes five flags true that OpenGL derives from extensions, OpenGL hardcodes DeviceFeatures.wireframeFillMode true where Vulkan asks the device for fillModeNonSolid, and the mirrored pair is the two direct multi-draw flavours, where OpenGL has the separate one and never the interleaved one and Vulkan the interleaved one only if the driver offers VK_EXT_multi_draw — so a Vulkan device can support neither. Not every draw consults those flags: the six multi-draw and indirect entry points gate unconditionally, RenderPass.draw and RenderPass.drawIndexed only when a non-zero first instance is asked for, and RenderPass.drawMultipleIndexed — the batched chunk path wherever terrain is not drawn indirect — not at all.

A pipeline is a record, not a sequence of calls

RenderPipeline is declarative and effectively immutable: a RenderPipeline.getLocation name, two shader Identifiers, a ShaderDefines, a list of BindGroupLayout, up to eight ColorTargetState, an optional DepthStencilState, vertex bindings, a PolygonMode, a cull flag and a PrimitiveTopology. RenderPipeline.Builder assembles one, and composition is the static RenderPipeline.builder taking RenderPipeline.Snippets, which RenderPipeline.Builder.buildSnippet produces rather than consumes.

Blending is a BlendFunction record, most often a named one (BlendFunction.TRANSLUCENT, BlendFunction.ADDITIVE…), rather than factors set on the device, and RenderPipeline.Builder.build refuses a pipeline whose blended colour targets do not all share one blend function, or that binds more than sixteen vertex attributes. Depth is reversed-Z throughout: DepthStencilState.DEFAULT compares greater-or-equal and RenderSystem.DEFAULT_DEPTH_CLEAR_VALUE is zero.

The catalogue lives on the game side: RenderPipelines registers the static pipelines — RenderPipelines.GUI, RenderPipelines.LIGHTMAP, RenderPipelines.SKY and a hundred and ninety-two more, ninety-three of them the three stages of thirty-one OitPipelineSets — from a shallow tree of snippets and the shared uniform-name sets in BindGroupLayouts, and ShaderManager compiles what RenderPipelines.requiredPipelines and RenderPipelines.optionalPipelines list.

What a pipeline does not say

A RenderPipeline says how to rasterise. It does not say which textures to bind or which target to draw into. The textures are client/renderer/rendertype’s: RenderType wraps a RenderPipeline with its texture bindings, a TextureTransform, a LayeringTransform, an outline variant and the batching predicates RenderType.canConsolidateConsecutiveGeometry and RenderType.sortOnUpload. RenderTypes is the static catalogue, RenderSetup builds the entries, and RenderType.prepare resolves one into a PreparedRenderType — pipeline, texture bindings, uniform slice — at draw time, and that draws into whatever RenderPass its caller has opened.

Buffers, uniforms, and the ring that resets every frame

The resource vocabulary is small: GpuBuffer with usage bits (GpuBuffer.USAGE_VERTEX, GpuBuffer.USAGE_UNIFORM, GpuBuffer.USAGE_MAP_WRITE…) and GpuBufferSlice over it, GpuTexture with its own and GpuTextureView over it, GpuSampler, GpuFence, GpuFormat, IndexType, PrimitiveTopology. Two of those are where old habits break. Sampler state left the texture — GpuTexture has no filter or wrap setters, filtering is an immutable GpuSampler bound per draw, and SamplerCache eagerly creates all thirty-two combinations at startup and throws if either enum ever gains a constant. And there are three shared index buffers, not one: RenderSystem.getSequentialBuffer switches between a quad buffer, a line buffer with different winding, and a one-to-one buffer.

The ring, and the blocks a shader gets for free

Per-draw uniform data does not come from per-draw uniform calls; it is carved out of ring buffers, and the ring is what makes it safe: a slice handed out this frame is still being read while the next frame is being built, so the buffer is rotated rather than overwritten. DynamicGpuData and DynamicGpuDataStorageMapped hand out GpuBufferSlices of a MappableRingBuffer, and DynamicGpuDataStorageMapped.endFrame rotates it, resets the write cursor and closes any buffer a growth left behind. FogRenderer.regularBuffer is a second ring, rotated the same way every frame, between the world and the GUI, while GlobalSettingsUniform and ProjectionMatrixBuffer hold one buffer apiece and rewrite it in place, which is all a frame-wide value needs. The first of those is the Globals block, one of the four RenderSystem.bindDefaultUniforms puts in front of every draw but the two depth-integrating passes — beside the projection, the fog and the lighting — and its seven members are fixed in Java: the camera position as an integer block position and the fraction left over, the screen size, the glint strength, the time of day as a fraction of twenty-four thousand ticks, the menu blur radius, and a flag for the supersampled texture-filtering mode. Those four, and a post pass’s own sizes, are the whole of what a shader may know without being told, and a post pipeline declares only the first of the four, which is why a post chain that wants something to vary per frame has to find it in Globals or in its own pass’s sizes. Per-frame scratch comes from TransientMemory — one interface over the shared TransientBlockAllocator, with GlTransientMemory and VulkanTransientMemory behind it, each among the four longest classes of its backend tree. Blocks are packed by hand with Std140Builder, sized by Std140SizeCalculator.

Vertices, and the builders that are not on the Render thread

Vertex data is described by VertexFormat and VertexFormatElement (a plain record of name, offset and GpuFormat) with the standard layouts in DefaultVertexFormat, and built with ByteBufferBuilder and BufferBuilder into a MeshData. Most BufferBuilders are on the Render thread like everything else here; the exception is the use that matters most for throughput, because chunk meshing builds its sections with BufferBuilders on worker threads, a synchronous compile’s aside, and stages the result through UberGpuBuffer into a StagingBuffer, which is why SectionRenderDispatcher has a spin-wait guarded by RenderSystem.isOnRenderThread.

Targets, and the pass a draw is handed

Render targets are RenderTarget, TextureTarget and MainTarget, the transient ones allocated through GraphicsResourceAllocator — CrossFrameResourcePool implements it — and declared in the FrameGraphBuilder of visibility and the frame graph. Which target a draw lands on is never in doubt: it is the one its RenderPass was opened on, and PreparedRenderType.drawFromBuffer draws into the pass its caller hands it. The GUI’s item atlas and its picture-in-picture renderers open that pass on a texture of their own and hand it to FeatureRenderDispatcher.renderAllFeatures, which is how an entity that turns to follow the mouse in an inventory screen is drawn by the world’s machinery into a texture instead of onto the screen.

Shaders, and the reflection that checks them

ShaderManager loads shader sources and hands them to the device through ShaderSource. Preprocessing happens once, in one place, and knowing where is the whole of it: ShaderManager only reads every source and every shaders/include file, on a worker, during a reload’s prepare; the #include directives and the ShaderDefines are both resolved later, by shaderc, when GlslCompiler.compileToSpv compiles a stage to SPIR-V — the defines as macros, the includes through a callback that answers from ShaderSource.getInclude. Both backends start from that SPIR-V, and the checking happens before either sees it: PipelineBuilder reflects each module with spirv-cross through SpvModule.reflect — which is what lets a declared BindGroupLayout be checked against what the shader declares, and a vertex format against the vertex shader’s inputs. Vulkan then takes the SPIR-V as it is, and OpenGL’s GlPipelineRecompiler translates it back into GLSL for the driver. It is all data on disk, alongside the chains in post-processing.

One draw

Every drawing class comes through this one shape — LevelRenderer, GuiRenderer, Lightmap and TextureAtlas among them — or joins it halfway, handed a pass someone else opened, as FeatureRenderDispatcher is.

sequenceDiagram
    participant Game as the game's own code
    participant GD as GpuDevice
    participant CE as CommandEncoder
    participant RP as RenderPass
    participant GlRP as GlRenderPass
    participant GlCE as GlCommandEncoder

    Game->>GD: createCommandEncoder — the same façade every time
    Game->>CE: createRenderPass with a RenderPassDescriptor
    CE->>CE: validate attachments, sizes, usage bits, render area, no pass open
    CE->>GlCE: createRenderPass — a cached FBO bound, scissor, clear, viewport
    CE-->>Game: the RenderPass, wrapping a GlRenderPass, to be closed
    Game->>RP: setPipeline, from RenderSystem.getCompiledPipeline — formats must match
    Game->>RP: RenderSystem.bindDefaultUniforms — Projection, Fog, Globals, Lighting
    Game->>RP: setVertexBuffer, setIndexBuffer, setUniform per texture
    Game->>RP: drawIndexed
    RP->>GlRP: drawIndexed
    GlRP->>GlCE: executeDraw — the VAO, the pipeline, the dirty uniforms
    GlCE->>GlCE: the GL draw call itself
    Game->>RP: close — debug groups must balance
    RP->>CE: FrontendCommandEncoder.<br/>submitRenderPass
    CE->>GlCE: submitRenderPass

One draw on the OpenGL backend: the game talks only to the three façade lanes beside it, which check what can break the API’s contract and pass the work to the two on the right. A single RenderPass.drawIndexed becomes a vertex array, a pipeline, the dirty uniforms and the driver’s draw.

What the game and the three façades do is declaration and the contract’s checks. In the two lanes on the right, one call is not one call: pass setup alone binds a framebuffer, sets the scissor, clears and sets the viewport, and the first RenderPass.drawIndexed of a pass binds a vertex array and an index buffer, binds a program and applies its depth, cull, colour mask, blend and polygon mode, walks the uniform and sampler bindings and finally draws. The point is not that a draw is cheap. It is that the game never sees any of it.

A pipeline reaches RenderPass.setPipeline already compiled: the game asks RenderSystem.getCompiledPipeline for it, and the current PipelineCache answers by identity, compiling on a miss while the Render thread waits. The reload is what keeps that off the frame: in the prepare half of every resource reload ShaderManager compiles the static catalogue through GpuDevice.compilePipeline — the SPIR-V and its reflection on workers, each pipeline finished on the Render thread — and in the apply half it fills a fresh PipelineCache and swaps it in with RenderSystem.setCurrentPipelineCache only if every required pipeline compiled, so a pack that breaks one leaves the old cache standing rather than a half-built one. The lazy path is left for pipelines the reload did not compile, which in a stock game means the post chains and three item pipelines, RenderPipelines.ITEM_CUTOUT among them, that the location-keyed required list drops because another pipeline reuses their location. Run the trace on Vulkan and the game code is unchanged — dynamic rendering replaces the framebuffer bind, and push descriptors the uniform binding.

How a frame reaches the screen

Presentation is a four-step protocol, not a swap: GpuSurface.configure, then GpuSurface.acquireNextTexture, then GpuSurface.blitFromTexture, then GpuSurface.present. Vsync is not a toggle in that sequence but a GpuSurface.PresentMode in the configuration: OpenGL offers a fixed pair of modes, Vulkan whatever the driver enumerates for the surface VulkanGpuSurface owns beside its swapchain, mailbox and relaxed FIFO included.

What stops the CPU running a hundred frames ahead of the GPU

Not the present. A two-deep submit fence is the pacing: GlCommandEncoder rotates its transient memory and a small fence ring every time it submits, and a submit that would outrun the ring waits. Results that must come back from the GPU use GpuFence instead — a callback registered with RenderSystem.queueFencedTask, run by RenderSystem.executePendingTasks once a frame in the gpuAsync zone, stopping at the first fence that has not signalled. Its one registration site in the game is GlCommandEncoder’s texture readback, and Vulkan routes the same callbacks through its own destruction queue.

For a 1.21-era reader. Tesselator is gone: vertices go through a BufferBuilder into a MeshData, then a GpuBuffer. VertexFormat.Mode, VertexFormat.IndexType and TextureFormat are now PrimitiveTopology, a top-level IndexType and GpuFormat; GpuDevice.getRenderer and its siblings are the DeviceInfo record; and Window.updateDisplay is GpuSurface.present, with vsync a GpuSurface.PresentMode. PoseStack did not move.

Where to look

RenderSystem for what the game holds, then GpuDevice, CommandEncoder and RenderPass for the façade, and FrontendCommandEncoder and FrontendRenderPass for its checks. RenderPipeline.Builder and RenderPipelines for how a draw is declared, RenderTypes for how one is dressed. GlCommandEncoder for an OpenGL draw, VulkanCommandEncoder for the other answer, GpuSurface for where a frame ends.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Visibility and the frame graph

Verified against Minecraft 26.3 · Part XI · you fly forward in creative and the world opens out in front of you, one section at a time.

You fly forward in creative and the world arrives. It does not resolve as a wall of fog lifting all at once — it grows outward from where you already are, section by section, at exactly the rate the mesher can keep up with. That order is not the download order and it is not the mesher’s queue order. It falls out of an asymmetry inside the walk that decides what is visible at all: a section that has not been meshed yet is opaque to the walk and stops it dead, a section known to be empty is transparent and lets it straight through, and a section that meshed to nothing is transparent too unless it holds a block entity, which keeps it a real compiled mesh that happens to have no draws in it. Terrain reveals itself outward because the walk can only reach as far as the meshes that already exist.

The frame ends where this page begins, and the page runs in the order the sections below do. The first stage is the odd one, because it is on the far side of the wall: LevelExtractor.applyFrustum runs near the top of extract, so the two things the extractor later takes from that list — the dirty sections and the block entities those sections hold — are already reading what this stage decided. Entities are not among them: they are tested one by one. Everything after it is LevelRenderer.render, which gathers what was submitted, declares the passes of a frame, draws the terrain and schedules translucency work for a later frame — then re-runs the walk on its way out, for the frame after this one.

The cast

classwhat it decidesthread
LevelRendererwhich passes the frame declares, and how terrain is bucketed and finally drawn — it holds the visible list the extractor fillsRender thread
SectionOcclusionGraphwhich sections the walk can reach from the camera at allfull walk on Util.backgroundExecutor, partial walk here
SectionOcclusionGraph.GraphStatethe published result of a walk — swapped whole on a rebuild, extended in place by a partial walkrebuilt by a worker, extended and read here
LevelExtractorwhether the frustum is re-applied this frame, and so whether the visible list is rebuilt or reusedRender thread
Frustumwhich of the reached sections survive into LevelRenderer.visibleSectionsRender thread
FrameGraphBuilderwhich passes exist, what each reads and writes, and what order they execute inRender thread
LevelTargetBundlethe named render targets the passes hand between themRender thread
ChunkSectionsToRenderhow the bucketed terrain is issued: one indirect draw per bucket, split at the device’s limit, or one multi-draw per layerRender thread

Only one of those reads the world, and it is the one on the extract side: LevelExtractor holds the ClientLevel and is the class that carries it across the wall. What the renderer still owns, on this side of it, is geometry, targets and order.

The order a frame does this in, and why the figure loops

flowchart TD
    subgraph EXT["extract, before the wall"]
        S1["1 · visible: LevelExtractor.applyFrustum trims the graph into LevelRenderer.visibleSections"]
    end
    subgraph REN["LevelRenderer.render"]
        S2["2 · submitted: LevelRenderer.submitFeatures, then FeatureRenderDispatcher.prepareFrame"]
        S3["3 · the first passes declared: clear, and the sky if drawn"]
        S4["4 · the visible sections bucketed into ChunkSectionsToRender"]
        S3B["3 · the main pass and any outline chain declared, then FrameGraphBuilder.execute"]
        S5["5 · translucent re-sorts, scheduled for a worker at the end of LevelRenderer.compileSections"]
        S6["the walk that feeds stage one: SectionOcclusionGraph.update, partial"]
    end
    BG["a full walk, on Util.backgroundExecutor"]
    S1 --> S2 --> S3 --> S4 --> S3B --> S5 --> S6
    S6 -. "when one is due" .-> BG
    S6 -. "raises the flag" .-> S1
    BG -. "publishes, raises the flag" .-> S1

One frame’s five stages in the order they run, the first on the far side of the wall and the other four inside LevelRenderer.render, which ends by re-running the walk. The dotted arrows are a full rebuild handed to a worker and the flag either walk can raise: stage one re-applies the frustum only when that flag is up or the camera has turned.

Stage one has already happened when LevelRenderer.render is entered, and the box at the end of LevelRenderer.render is the walk that feeds it being re-run for the frame after this one, which is why the figure loops rather than ending. Stages three and four interleave: the terrain is bucketed after the first passes are declared and before the main pass is, and drawn when FrameGraphBuilder.execute runs that pass. The two bookkeeping stages are the first and the fifth, and they are incomplete in two different ways: the first is a cache, thrown away and rebuilt only when something invalidates it, and the fifth is a budget, doing a fixed slice of the work each frame and leaving the rest stale. Both are why a frame’s terrain cost does not track what is on the screen.

The walk that decides what exists, and the frustum that only trims it

Visibility here is reachability, not a frustum test. SectionOcclusionGraph starts at the camera’s own section, or with the camera above or below the world the nearest layer of sections, and walks outward one neighbour at a time, and — with smart cull on, which is the default — it may step from a section into a neighbour only if the two faces involved can see each other through that section’s geometry. That per-section answer is a VisibilitySet, computed by VisGraph when the section was meshed — so the question can you see through this section is decided once at compile time and then read for free by every walk that passes through it. A wall of stone does not hide the world behind it because a frustum test rejected it. It hides it because the walk cannot get past.

The reached sections live in an Octree inside SectionOcclusionGraph.GraphState, alongside the queue of sections whose neighbours still need visiting. A rebuilt state is never edited into the old one: the whole of it is published through an AtomicReference, because SectionOcclusionGraph.scheduleFullUpdate runs the complete rebuild on Util.backgroundExecutor and the Render thread has to keep reading the old graph until the new one is ready. The rest of the walk happens on the Render thread, including SectionOcclusionGraph.runPartialUpdate, which does edit the published state in place — it walks outward again from the sections that SectionOcclusionGraph.schedulePropagationFrom flagged (a call a worker also makes when it compiles a section to nothing), typically because a new mesh landed for them and their neighbours are worth trying again — which is a real walk, not a drain, and it is where the outward reveal actually advances. SectionOcclusionGraph.update, at the very end of LevelRenderer.render, is what runs both.

The frustum arrives after all of this, and only trims. SectionOcclusionGraph.addSectionsInFrustum visits the octree, keeps what a Frustum admits, and fills LevelRenderer.visibleSections plus the small short-radius subset LevelRenderer.nearbyVisibleSections. Looking a section up by position goes through LevelRenderer.viewArea.

Beyond sixty blocks the walk gets harder — and sixty blocks and three sections are one number written twice. SectionOcclusionGraph.MINIMUM_ADVANCED_CULLING_DISTANCE is sixty, and SectionOcclusionGraph.MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE is that same distance converted to section coordinates, which comes out at three; the test that uses it compares section coordinates on each axis separately. Out past it, smart cull adds a ray march back toward the camera from the section being stepped out of, and rejects the neighbour if any section along the line has not itself been reached by this walk. Inside it, nothing marches — nearby geometry is cheap enough not to argue about.

Which is why the reveal is outward

The three states a section can be in are the whole trick. An uncompiled section is opaque: the walk stops there and everything behind it stays unreached. An empty section is transparent: the walk passes through without needing a mesh at all. A section that compiled to nothing is transparent too, unless it holds a block entity: then it keeps a real compiled mesh with no draws in it and answers from its own VisibilitySet like any other section. So as meshes land, the frontier of the walk moves outward one shell at a time, and each newly meshed section re-arms its neighbours through SectionOcclusionGraph.schedulePropagationFrom.

Streaming is a separate handshake laid over the same walk. A section whose chunk has not arrived yet is treated as neither opaque nor transparent — it is parked, filed against the chunk it is waiting for, and resumed when ClientChunkCache reports that chunk loaded. The walk then continues from where it stopped rather than starting over.

The gate this stage controls is not only what gets drawn. Only visible sections are re-meshed, so a block that changes behind you, farther back than the few blocks the frustum is pulled back to cover, costs nothing until the frustum step lets its section back into the list, usually when you turn; how a section becomes triangles once it has been chosen — the dirty flags, the snapshot a worker reads, the compiler, the three chunk layers and the buffer arenas they upload into — is section meshing.

The visible list is a cache, not a per-frame computation

LevelExtractor.applyFrustum does not run every frame, and two different clocks decide when it does. They are easy to run together and they are not the same thing.

The walk is thrown away and redone when the camera crosses an eight-block cell on any axis, when the field of view changes, or when the smart-cull toggle changes. That is SectionOcclusionGraph.invalidateIfNeeded, and what it asks for is the full off-thread rebuild, which SectionOcclusionGraph.update then schedules.

The frustum step asks a different question — the walk’s result may still be good while the set of it you can see is not — and it re-runs when either of two things happens: the graph raises its own frustum-update flag, which a completed full walk always does and a partial walk does whenever it added a section inside the offset frustum; or the camera’s pitch or yaw crosses a two-degree step. Turning your head therefore re-applies the frustum without disturbing the walk at all, and between these events LevelRenderer.visibleSections is simply the list from last time. Stand still and stare, and the frame’s terrain cost does not move.

Everything is submitted before there is anywhere to put it

Entities and block entities do not draw themselves inside a pass. They are gathered first, before a single pass has been declared. LevelRenderer.submitFeatures runs LevelRenderer.submitEntities and LevelRenderer.submitBlockEntities into LevelRenderer.submitNodeStorage, and FeatureRenderDispatcher.prepareFrame groups everything submitted into a FeatureRenderDispatcher.PreparedFrame. The passes declared in the next stage capture that prepared frame and call into it; they never walk an entity list themselves. What a submission contains, and how an entity produces one, is entity rendering.

The ordering matters for a reason that only shows up in the next stage: the frame graph needs to know, while it is being declared, whether anything in this frame wants an outline. It can know that because the submission has already happened.

Declaring the passes, and why none of them is ever culled

LevelRenderer.render builds a graph and then executes it. Building it means declaring resources and passes. FrameGraphBuilder.importExternal brings in the targets that already exist outside the frame — the main render target and the entity-outline target — and LevelRenderer declares, through FrameGraphBuilder.createInternal, up to eight that exist only for the duration of this frame: seven for improved transparency, when that option is on, and one depth target when an always-on-top gizmo is drawn while a post effect runs; the outline chain, in a frame that has one, declares its own swap target the same way. For the duration of this frame is not the same as allocated this frame: every internal target in the part comes out of one CrossFrameResourcePool, which keeps a released target for three frames in case something asks again for that size and format, so a target that is conceptually thrown away at the end of a frame is usually the same GPU memory next frame. Each pass comes from FrameGraphBuilder.addPass, states its dependencies with FramePass.reads and FramePass.readsAndWrites, and states its body with FramePass.executes. LevelTargetBundle is where the handles live under names — LevelTargetBundle.main, LevelTargetBundle.entityOutline, LevelTargetBundle.alwaysOnTopDepth, and the improved-transparency targets from LevelTargetBundle.depthBounds to LevelTargetBundle.oitTerrainWithWaterPatchDepth — and LevelRenderer.targets is the bundle the frame threads through every declaration.

flowchart TD
    CLEAR["clear — wipes the main target's colour and depth"]
    SKY["sky — LevelRenderer.addSkyPass, if drawn"]
    subgraph MAIN ["the main pass"]
        direction TB
        T1["opaque terrain — the OPAQUE draw group"]
        T2["FeatureRenderDispatcher.PreparedFrame.<br/>executeSolid"]
        D{"improved transparency?"}
        T3["LevelRenderer.<br/>executeClassicTransparency"]
        T4["LevelRenderer.executeOit, once per OitStage"]
        T5["LevelRenderer.executeOutline, only with outlines: into the outline target"]
        T6["LevelRenderer.executeSeeThrough, only if anything sees through"]
        T7["LevelRenderer.executeAlwaysOnTop, only with always-on-top gizmos: depth cleared"]
        T1 --> T2 --> D
        D -->|"no"| T3
        D -->|"yes"| T4
        T3 --> T5
        T4 --> T5
        T5 --> T6 --> T7
    end
    OUT["entity outline post chain — only with outlines"]
    CLEAR --> SKY
    SKY --> MAIN
    MAIN --> OUT

The passes LevelRenderer.render declares, with the one LevelRenderer.addMainPass declares opened up. The diamond picks how everything translucent is drawn, and the last three steps are skipped in a frame with nothing of their kind.

The post chain in that figure is declared here and explained in post-processing, and it is why the entity-outline target is imported at all. Everything else after the sky draws inside the main pass, which opens render passes of its own as it goes. With improved transparency off, which is the default, LevelRenderer.executeClassicTransparency draws translucent features, translucent terrain, the features that follow terrain, then clouds, weather and the world border, all into the render pass the opaque terrain went into. With it on, LevelRenderer.executeOit draws the same things once per OitStage through the seven targets declared for it and composites the result onto the main target, which is the only reason those seven are ever created. LevelRenderer.executeOutline, LevelRenderer.executeSeeThrough and LevelRenderer.executeAlwaysOnTop follow, each only in a frame with something of its kind.

FrameGraphBuilder.execute is also where the profiler learns about any of this: the level’s graph is executed with an inspector that pushes a zone per pass, named after the pass, which is why the F3 pie chart can tell you the sky cost you something. A graph executed without one — and there is another kind — reports nothing.

The graph culls passes, and culls none of these. FrameGraphBuilder.execute keeps only the passes that transitively feed an imported external resource and drops the rest before it orders anything. Note the plural: it seeds from every imported resource, not from the main target alone, which is what saves the entity-outline chain — none of its four passes ever writes to main, and all four survive because the glow target is imported too. With that seeding, no pass LevelRenderer declares is ever dropped in a stock game. The outline chain is absent from a frame with no outline in it for a different and much cheaper reason: LevelRenderer.render never adds it. The declaration is the branch; the culling machinery is insurance against a declaration that has become pointless, not the mechanism the game uses to turn features off.

One bucket per buffer set, and what bucketing actually buys

LevelRenderer.prepareChunkRendersIndirect runs before the main pass is declared, and its output is what that pass will execute — or LevelRenderer.prepareChunkRenders does, on a device that cannot multi-draw indirect or with Minecraft.multiDrawIndirect switched off. Either walks LevelRenderer.visibleSections and, for each layer a section has geometry in, computes a hash of the buffers that geometry lives in and files the draw under that hash. Sections sharing buffers land in the same bucket. The indirect path writes every bucket’s draws into a buffer of commands the GPU reads, and each section’s origin into per-instance vertex data, and ChunkSectionsToRender.DrawIndirect then issues one RenderPass.drawIndexedIndirect per bucket, split only where a bucket holds more draws than the device takes in one call. There the saving is the draw calls. The fallback’s ChunkSectionsToRender.DrawSeparate issues one RenderPass.drawMultipleIndexed per layer instead, over the draws in bucket order, with each section’s origin arriving as a slice of a uniform buffer. That still loops and issues one GPU draw per section, and what the bucket order saves there is rebinding: the index buffer changes only between buckets, and on OpenGL the vertex buffer does too.

Two draw groups over three layers, and why translucent inverts the rule

The main pass renders two groups, not three layers. ChunkSectionLayerGroup is the coarser partition the draw side works in: ChunkSectionLayerGroup.OPAQUE covers the solid and cutout layers together, because neither blends and both can be drawn in any order, and ChunkSectionLayerGroup.TRANSLUCENT is the third layer alone, which is why the next paragraph is only ever about that one. Turning a position back into the section it names is LevelRenderer.viewArea, a ViewArea — and it holds its SectionRenderDispatcher.RenderSections in a RotatingSectionStorage, the same kind of ring the dirty flags live in, recentred by ViewArea.repositionCamera as you move.

The translucent layer inverts the rule whenever the order matters, which is whenever improved transparency is off: there the visit order decides the bucket, and the buffers only decide where a bucket ends. Its hash still covers the buffers, but a translucent draw can join only the bucket of the draw just before it, so each bucket is a run of neighbours sharing buffers, and a section whose buffers differ from the one before it opens a new bucket even when an older bucket has the same ones. That is what preserves the visit order across the buckets, and the visit order is what the draw list is then reversed against — the buckets and the draws inside each — so the far sections blend before the near ones. Correct blending is bought here by refusing to regroup the draws, not by refusing to share buffers. With improved transparency on, the order stops mattering and translucent sections bucket exactly as the opaque ones do.

One ordering here catches everyone out, and it is worth stating from this side because it is a fact about where LevelRenderer.compileSections sits: it runs after FrameGraphBuilder.execute, so terrain is drawn before the sections queued this frame are compiled. What that costs a player, and why the option that promises otherwise cannot deliver it, belong to section meshing.

Translucency, re-sorted on a budget it never finishes

With improved transparency off, translucent quads inside a section have to be sorted back to front from where you are standing, and where you are standing changes constantly. Re-sorting every visible section every frame is not affordable, so the client re-sorts a slice of them each frame and lets the rest be slightly stale.

Two groups are considered. Everything in LevelRenderer.nearbyVisibleSections — the short-radius set the frustum step filled alongside the main list — and then a round-robin slice of LevelRenderer.visibleSections, an eighth of it or fifteen sections, whichever is larger, walked from LevelRenderer.translucencyResortIterationIndex so that successive frames continue where the last one stopped.

What actually counts as having moved

Being considered is not being re-sorted. A section is scheduled if its TranslucencyPointOfView actually changed — and that is three integers, each clamped to −1, 0 or +1, saying whether the camera’s section is before, level with or past this one on that axis. Twenty-seven possible values in all, which is why “changed” is a cheap test that is usually false: crossing a section boundary changes it only for the sections in the two slabs that boundary separates, and wandering about inside one changes it for none. TranslucencyPointOfView.isAxisAligned is the same object answering whether any axis reads zero — or the section is scheduled anyway if the camera’s block position moved since LevelRenderer.lastTranslucentSortBlockPos and the section is either axis-aligned like that or one of the nearby ones. It is then skipped anyway if a re-sort is already scheduled for it, if it has no translucent geometry at all, or if improved transparency is on. What runs when one is scheduled is SectionRenderDispatcher.RenderSection.resortTransparency, which schedules a worker task that reorders the existing mesh rather than recompiling it, and which gives up without re-sorting if, when it runs, the section’s view has not changed and no axis reads zero — the cheap path section meshing hands here. So standing still costs nothing, walking costs a bounded amount, and a fast enough sideways move can leave a distant pane of glass sorted for a viewpoint you have already left.

For a 1.21-era reader. LevelRenderer.renderLevel is now LevelRenderer.render, handed render state rather than a level. LevelRenderer.cullTerrain and LevelRenderer.applyFrustum are gone: SectionOcclusionGraph and LevelExtractor.applyFrustum do their work. LevelRenderer.addCloudsPass, LevelRenderer.addWeatherPass and the transparency post chain are gone too: LevelRenderer.addMainPass draws clouds and weather, and improved transparency is LevelRenderer.executeOit. Every dirty method on LevelRenderer moved to LevelExtractor.

Where to look

LevelExtractor.applyFrustum first, because stage one is the one that is not in LevelRenderer.render; then LevelRenderer.render, which is stages two to five in the order the first figure draws them. SectionOcclusionGraph.update for the walk, and SectionOcclusionGraph.runPartialUpdate for the only part of it on the Render thread. LevelExtractor.extract, which decides when to call it, for why the visible list is usually a cache. FrameGraphBuilder.execute for how declared passes are ordered and which are dropped. LevelRenderer.prepareChunkRendersIndirect, LevelRenderer.prepareChunkRenders and ChunkSectionsToRender for how terrain finally reaches the GPU, with blaze3d underneath it. The models and the atlas the terrain is textured from are models and atlases; the block changes that make sections dirty arrive as the packets in what the client is told and become dirty sections in the client level.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Section meshing

Verified against Minecraft 26.3 · Part XI · a block is placed, and the section it lives in is re-meshed, uploaded and drawn.

You right-click a block into place and within a frame or two it is simply there, with no shimmer and no fade. Behind that wait, on a client left at its default graphics preset, the Render thread itself rebuilds a cube of world sixteen blocks on a side from scratch, from a snapshot of twenty-seven sections, just after the next frame has drawn its terrain from the old mesh, and swaps the new one in before that frame ends. A block the server changes takes the slower road: a worker thread, a scratch buffer it has to queue for, and a swap that can wait a frame or more until every layer has been uploaded. All of which is why the other half of the story is the surprising one: let a block change behind you, farther back than the few blocks the visible list reaches behind the camera, and none of it happens at all. Only sections the frame is already drawing are swept for dirtiness, so the flag on a section at your back is set and then simply waits — for a second, for an hour, for as long as you keep your back turned.

The cast

classwhat it decidesthread
LevelExtractorwhich block changes dirty which sections, and which dirty sections are worth compiling this frameRender thread
SectionUpdateTrackerwhere dirtiness lives, and whether a never-compiled section is allowed to compile yetRender thread
RotatingSectionStoragewhich slot in a fixed ring a section’s flag belongs to, and when that slot is re-homedRender thread
RenderRegionCachethe snapshot a mesher is allowed to read, and how much of it is sharedRender thread
SectionRenderDispatcherqueue order, scratch buffers, the GPU arenas, and the moment a new mesh becomes the drawn oneRender thread, workers
SectionTaskDynamicQueuewhich section a free worker takes nextany
SectionCompilerthe block-by-block walk that turns a snapshot into verticesthe worker pool, or the Render thread for a synchronous compile
BlockModelLightersmooth lighting and ambient occlusion, per quad, as the walk goesthe worker pool, or the Render thread for a synchronous compile

The whole trip, in one figure

sequenceDiagram
    participant CL as ClientLevel
    participant LX as LevelExtractor
    participant SUT as SectionUpdateTracker
    participant LR as LevelRenderer
    participant SRD as SectionRender<br/>Dispatcher
    participant SectC as SectionCompiler

    rect rgba(0, 0, 0, 0.04)
    Note over CL,SectC: the client tick that handles the click
    CL->>CL: setBlock, from BlockItem.placeBlock in the placement prediction
    CL->>LX: setBlockDirty, which asks whether a model cares
    CL->>LX: blockChanged, the player-changed bit read off the flags
    LX->>SUT: setDirty over a 3x3x3 block halo, one to eight sections
    end
    rect rgba(0, 0, 0, 0.04)
    Note over CL,SectC: the frame after that tick
    LX->>LR: visibleSections, the only ones the sweep asks about
    LX->>LX: RenderRegionCache takes a visible dirty section's 27-section snapshot
    LX->>SUT: that section's flag cleared
    LX->>LR: the swept sections, in the level's render state
    alt player-changed or nearby, as PrioritizeChunkUpdates says
        LR->>SRD: compileSync
        SRD->>SectC: compile inline, on the Render thread
    else otherwise
        LR->>SRD: compileAsync
        SRD->>SectC: on a worker, nearest-first, once a buffer pack is free
    end
    SectC-->>SRD: at most three layers, into the staging buffer
    LR->>SRD: uploadTerrainBuffersToGpu, whose callback swaps in each fully uploaded mesh
    end

The trip from a placed block to a drawn mesh: a tick sets flags, and the frame after it sweeps only visible sections and compiles or queues the dirty ones after its own terrain is drawn. A synchronous compile is swapped in at the end of that frame, an asynchronous one at the end of whichever frame uploads the last of it.

Read it in three beats: a change makes a flag, a frame turns some flags into work, and the end of that frame or a later one publishes the result. The middle beat is the one that leaves the Render thread, and it does not always — a synchronous rebuild compiles inline where it stands, and an empty mesh is published by whichever thread compiled it.

A click, and the flag it leaves behind

The first arrow hides who starts it. MultiPlayerGameMode starts the prediction — the client places the block itself, and its level remembers the state the server last confirmed, to put back if the server disagrees — but the Level.setBlock that actually changes the world happens down inside BlockItem.placeBlock. From the renderer’s side the one difference is a flag: your own write carries the player-changed bit and the server’s does not. What the client is told is the other door into the same call, by way of ClientLevel.setServerVerifiedBlockState.

Dirtiness is a small API on LevelExtractor, and the calls differ mostly in how much of the world they condemn.

callwhat it marks
LevelExtractor.blockChangedone changed block, with the player-changed bit read off the update flags
LevelExtractor.setBlockDirtyone changed block, and only if ModelManager.requiresRender says a model cares
LevelExtractor.setBlocksDirtya box of block positions
LevelExtractor.setSectionDirtyone section, by section coordinate
LevelExtractor.setSectionDirtyWithNeighborsthat section and the ones touching it
LevelExtractor.setSectionRangeDirtya range of sections
LevelExtractor.allChangedeverything at once

Twenty-seven block positions make the halo a single block change marks, and they are positions, not sections. This is the number to keep straight. A 3×3×3 neighbourhood of blocks maps to exactly one section for any block that is not on a section boundary, and to at most eight when it is — a corner block touching seven neighbours plus its own. Only the mesher’s read region, much later on, is genuinely twenty-seven sections. There is a gate on one of the two doors — ModelManager.requiresRender guards LevelExtractor.setBlockDirty, so a state change no model reacts to marks nothing through that route — but it cannot stop a placed block. Level.setBlock goes through both doors, and the second, LevelExtractor.blockChanged, marks the halo whatever the models say.

The flag belongs to a slot, not to a section

SectionUpdateTracker holds a SectionUpdateTracker.SectionDirtyState per section — with SectionUpdateTracker.SectionDirtyState.isDirty and SectionUpdateTracker.SectionDirtyState.isDirtyFromPlayer, the second being what the prioritise chunk updates setting keys off further down this page — and the tracker is built on RotatingSectionStorage, a fixed ring of slots that RotatingSectionStorage.repositionCenter re-homes as the camera moves. A dirty flag therefore belongs to a slot, not to a section: walk far enough away and the slot is re-homed and the flag is gone. Nothing is lost by that, because a newly homed slot starts dirty.

The sweep that only looks at what you can see

Once a frame, the extract pass walks the sections the renderer considers visible and collects the dirty ones. That is the whole mechanism behind the hook: a section that is not in the visible set is never even asked whether it is dirty, so its flag sits there, indefinitely, and the section rebuilds the instant it comes back into view. Which sections count as visible, and the reachability walk that decides it, belong to visibility and the frame graph.

One extra gate applies, and only to sections that have never been compiled before. SectionUpdateTracker.hasAllNeighbors requires the eight surrounding chunk columns — horizontally only, never the section’s own column — to be loaded and lit before a first compile is allowed. A mesher decides whether a block’s face is worth drawing by looking at the block on the other side of it, so a section built without its neighbours would be a section built against nothing. A recompile skips that check entirely: a section that has a mesh already may always build a new one.

What a mesher is allowed to read

A compile on a worker runs for an unbounded time while the Render thread keeps applying block updates, so it cannot be allowed near the live world. RenderRegionCache builds it a RenderSectionRegion instead: a 3×3×3 grid of SectionCopy, each holding a genuine copy of one section’s PalettedContainer together with an immutable map of that chunk’s block entities. Inside the compile, a block state read is a read of that copy, and it will answer the same way from the first quad to the last however much the world has moved on.

The cache is what stops this being ruinous. Twenty-seven copies per dirty section would mean 27n copies for n sections, and neighbouring dirty sections share almost all of their neighbourhood — so the regions built in one extract share their SectionCopy instances, and the real cost is far closer to n.

What is not copied is as informative as what is. Tints and light are read live, through the region’s references to ClientLevel and the light engine — so for those two the mesher is looking at the world as it is when it asks, not at the world as it was when the snapshot was taken.

The queue, and the scratch buffer every compile must hold

SectionRenderDispatcher takes the sections the extract collected and either queues them (SectionRenderDispatcher.RenderSection.compileAsync) or compiles them on the spot (SectionRenderDispatcher.RenderSection.compileSync). The dispatcher does not decide which: LevelRenderer.compileSections does, from the snapshotted prioritise chunk updates option, and this is where SectionUpdateTracker.SectionDirtyState.isDirtyFromPlayer is finally spent. PrioritizeChunkUpdates.NONE — the option’s own default, which the fancy preset a new client starts on replaces — never compiles inline; PrioritizeChunkUpdates.PLAYER_AFFECTED does so for a section the player changed; PrioritizeChunkUpdates.NEARBY does so for those and for anything close to the camera. SectionTaskDynamicQueue decides the order and it is nearest-first, with one guard: a recompile only beats a first-time compile while a small quota lasts, and only if it is also nearer. With no first-time compile queued at all, a recompile wins outright. So terrain you have never seen is never starved by a stream of rebuilds to terrain you have.

A thread is not enough. Each task-runner must also acquire a SectionBufferBuilderPack — the scratch vertex buffers a compile writes into — from SectionBufferBuilderPool before it can do anything, and the pool is sized to the processor count or to a share of the heap, whichever is smaller, degrading further if it hits an out-of-memory error while allocating. A worker that cannot get a pack puts its task back on the queue and gives up its turn — the task is requeued, not the flag, which was cleared when the work was taken, so the only symptom of an exhausted pool is terrain arriving more slowly.

That requeue is a null check, and it is worth knowing how wide it is: the catch that implements it covers the whole compile. A failure while one block is being tesselated is reported, as a crash; a null-pointer failure anywhere else in the compile quietly requeues the section, and the pack it had acquired is never given back. A section that fails that way every time takes one more pack from the pool each time a worker tries it, until the pool is empty and no compile on a worker ever finishes again.

The ceiling on how many compiles run at once is therefore the pool plus one: the synchronous path uses RenderBuffers.fixedBufferPack, which is not in the pool at all. And because the pool is usually larger than the background pool, the constraint you actually hit is normally the thread count after all.

What the compiler makes

SectionCompiler.compile walks every block in the section, asks the models for its quads and sorts them into layers. The asking is ModelBlockRenderer.tesselateBlock, one instance built per compile from the ambient-occlusion option and BlockColors, and the quads come back through a BlockQuadOutput — one of two callbacks the compiler builds per compile and picks between per block, so the renderer never knows which layer’s buffer it is writing into. Its product is SectionCompiler.Results, four things at once: SectionCompiler.Results.renderedLayers (the geometry, per layer), the SectionCompiler.Results.blockEntities it found on the way, a SectionCompiler.Results.visibilitySet for the reachability walk on the other page, and SectionCompiler.Results.transparencyState, the sort state for the translucent layer. Those become the section’s CompiledSectionMesh. Lighting is not a separate stage: BlockModelLighter computes smooth lighting and ambient occlusion as the walk goes, with a thread-local cache bracketed around each compile.

Three chunk section layers exist, and only three: ChunkSectionLayer.SOLID, ChunkSectionLayer.CUTOUT and ChunkSectionLayer.TRANSLUCENT. A quad’s layer is normally decided at bake time and simply carried into the compile — see models and atlases for the BlockStateModelSet the compiler reads and the reload that invalidates every mesh in the world. But the mesher overrules it in one place, and fluids take a road of their own. Every leaf quad is redirected to ChunkSectionLayer.SOLID when the cutout leaves option is off — that is ModelBlockRenderer.forceOpaque, tested per block, and it picks which of the compiler’s two callbacks the renderer is handed — so the setting is baked into the mesh rather than applied at draw time. And fluids never consult a baked quad at all: FluidRenderer.tesselate is a separate call with its own callback, and the layer comes from the FluidModel.

Why “prioritise chunk updates” still costs you a frame

The figure’s caption says it and it is worth stating plainly, because it is the one ordering fact on this page a player can feel: LevelRenderer.compileSections runs after FrameGraphBuilder.execute, so terrain is drawn before the sections queued this frame are compiled. The strongest promise the setting can therefore make is that the mesh exists by the end of frame N; it appears in frame N+1. The setting buys you the compile, not the draw — and it buys it by doing the work on the Render thread, which is why it can cost you that frame outright.

Onto the GPU, and a swap that is late on purpose

The worker does not touch the GPU. It appends its vertices to a StagingBuffer under a lock, spinning if the buffer is full — a real back-pressure point, where a worker waits on the Render thread rather than the buffer growing to fit it. The Render thread drains it in SectionRenderDispatcher.uploadTerrainBuffersToGpu, and each completed upload fires the callback that publishes the new mesh. The result of a compile therefore arrives back on the Render thread, always, with exactly one exception: a section that compiled to no geometry is published directly by the thread that compiled it, because there is nothing to upload.

The destination is not one buffer per layer but an arena per layer. Each ChunkSectionLayer has an UberGpuBuffer pair owning a growing list of fixed-size heaps — 128 MiB for vertices, 32 MiB for indices — and each heap is a real GPU buffer sub-allocated by a TlsfAllocator, freed again when it empties. Sections are tenants in a shared allocation, not owners of buffers; blaze3d is where those buffers come from.

The swap is atomic and late, on purpose

And now the second fact this page exists to place. SectionRenderDispatcher.RenderSection.sectionMesh keeps pointing at the old mesh until every layer’s vertex and index buffer has reported uploaded. There is no frame in which a rebuilt section is missing, no flicker and no hole — the price being that the section you can see is, for a frame or more, deliberately out of date. SectionRenderDispatcher.RenderSection.reset is the other end of that lifecycle, and SectionRenderDispatcher.RenderSection.getVisibility is not part of it at all despite the name — it is a fade, an alpha that climbs from nothing to one over the fade duration. That fade is why arriving terrain appears softly and a block you place never does: every section fades in over the one Options.chunkSectionFadeInTime setting, near or far, and the clock starts at a section’s first upload — so a recompile of terrain you have been staring at, which is what placing a block is, has no fade left to spend. SectionRenderDispatcher.RenderSection.resortTransparency is the cheap path that reorders an existing translucent mesh without recompiling anything; visibility and the frame graph owns the budget that decides when it runs.

For a 1.21-era reader. The whole dirty API moved: every setBlockDirty-shaped method on LevelRenderer is now on LevelExtractor, in client/renderer/extract, and the flags live in SectionUpdateTracker. LiquidBlockRenderer is now FluidRenderer; BlockRenderDispatcher is gone and ModelBlockRenderer does its tesselating; BlockAndTintGetter.getShade is gone, and BlockAndTintGetter.cardinalLighting answers with a CardinalLighting. There are three chunk section layers, not four: ChunkSectionLayer.TRIPWIRE is gone.

Where to look

LevelExtractor.blockChanged and LevelExtractor.setBlockDirty for where a change becomes a flag, and SectionUpdateTracker for where the flag lives. SectionUpdateTracker.hasAllNeighbors for the gate on a first compile. RenderRegionCache and SectionCopy for what a mesher may read. SectionRenderDispatcher.RenderSection.compileAsync and SectionTaskDynamicQueue for what gets built and in what order. SectionCompiler.compile for the walk itself, and BlockModelLighter for where ambient occlusion comes from. SectionRenderDispatcher.uploadTerrainBuffersToGpu for the upload and the swap. Then visibility and the frame graph for who decided the section was visible in the first place, and the client level for the world all of this is reading.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Models and atlases

Verified against Minecraft 26.3 · Part XI · a resource pack changes one texture, and every stone block in the world redraws.

You drop a pack into the folder, move it above the default, and the screen goes to the loading overlay for a second. The pack replaced exactly one file, a single stone texture, and when the overlay clears every block you can see has been thrown away and rebuilt — not just the stone. The models for stone were byte-identical before and after, and were still re-listed, re-parsed, re-resolved and re-baked: nothing in the reload asks what changed, and the sprite they point at is now a different object in a different image. Nothing here is incremental. The unit of change is the model layer.

Every block face, every item sprite and every particle texture starts as a PNG in a pack, and most of them as JSON too, and ends as four vertices pointing at a rectangle of a stitched atlas. The stages below are that journey, and they happen inside a resource reload — the resource system owns ReloadableResourceManager and the barrier this rides on — and at no other time.

The cast

classwhat it decidesthread
ModelManagerthe reload’s spine, and what the finished lookup sets containthe Render thread to start and to apply, workers between
AtlasManagerwhich atlases exist, and it publishes their stitches for others to awaitRender thread for the handshake, workers for the work
SpriteLoaderhow one atlas is decoded, packed and mipmappedworkers
ModelDiscoverywhich file an Identifier means, and what its parent chain resolved toone worker task
ModelBakerywhich unbaked model each BlockState and each item file bakes toworkers, in batches
FaceBakerya quad’s positions and UVs, both rotated, and its chunk layerworkers
TextureManagerwho owns each AbstractTexture, and when an animation advancesRender thread
ItemModelResolverwhich ItemModel a given ItemStack draws withRender thread, every frame

The shape of the work: seventeen fans, one barrier

This is the clearest fan-out-and-barrier in the client. Seventeen independent pieces of work start on worker threads at once, and the figure below draws all of them, each set in two boxes: the twelve atlas stitches AtlasManager.reload schedules, and the five roots ModelManager.reload opens. Three of those five are directory listings that are each themselves a fan of one task per file. The bake waits on only two of the twelve stitches, and everything meets at the barrier.

flowchart TD
    RL["a reload starts: F3+T, a pack change, or the game booting"]
    HS["Render thread: AtlasManager.prepareSharedState, then every listener's reload called"]
    subgraph W["on worker threads"]
        subgraph AT["the atlas stitches"]
            S1["the blocks and items atlases"]
            S2["the other ten atlases"]
        end
        subgraph MR["the five model roots"]
            LST["three listings, models/, blockstates/ and items/, one task per file"]
            ONE["EntityModelSet.vanilla and BuiltInBlockModels.createBlockModels"]
        end
        RES["ModelDiscovery resolves what the three listings name"]
        BAKE["the bakes: ModelBakery.bakeModels, then the BlockModel layer and fluids"]
    end
    BAR["the barrier"]
    UP["Render thread: TextureAtlas.upload, then ModelManager.apply"]
    INV["LevelExtractor.allChanged raises a flag the next frame reads"]
    RL --> HS
    HS --> AT
    HS --> MR
    LST --> RES
    S1 & RES & ONE --> BAKE
    BAKE & S2 --> BAR
    BAR --> UP --> INV

The reload as a fan and its joins: the Render thread publishes the pending stitches and calls each listener’s reload, and what those schedule runs on workers, the bake waiting on two atlases and all five roots. Everything meets at the barrier, and what follows it runs on the Render thread in one order.

Read it as spread, converge, upload, invalidate: inside the outer box, worker threads in whatever order the joins allow; below the barrier, the Render thread in exactly one.

Twelve atlases and three listings, all at once

In: every enabled pack. Out: twelve stitched images, three parsed maps.

AtlasManager owns AtlasManager.KNOWN_ATLASES — twelve of them, named in AtlasIds — and each is a definition file in atlases/, read from every pack that ships one, not a folder scan. SpriteSourceList runs that file’s five kinds of source in order — SingleFile, DirectoryLister, SourceFilter, Unstitcher and PalettedPermutations, registered in SpriteSources — and a later source overwrites an earlier one by id, which is how a pack’s own definition file redirects a sprite; replacing a vanilla texture needs none of that, since the pack’s PNG shadows vanilla’s under the same name. SpriteLoader.loadAndStitch decodes and reads metadata one task per sprite, hands the results to Stitcher — which sorts by height and grows by powers of two — and returns a SpriteLoader.Preparations, after which MipmapGenerator builds the mip chain under whatever MipmapStrategy the texture’s own metadata asks for.

Two properties of that packing surprise people, and a third setting rides on both of them. The mip level is clamped by the lowest set bit of each sprite’s width and height, with a warning, so one undersized or oddly sized texture degrades mipmapping for every sprite in the atlas — and only the block atlas asks for mipmaps, the other eleven stitch flat. And sprite padding derives from the mip level and the anisotropic filtering setting, with the UVs computed inside the padded box, so anisotropy changes the layout and every UV in the game. The mipmap slider is blunter still: it writes the new level onto AtlasManager and asks for a full resource reload when the video settings close, so every sprite in the game is decoded and stitched again.

Meanwhile BlockStateModelLoader parses blockstates/ into a BlockStateModel.UnbakedRoot per BlockState, through BlockStateModelDispatcher and VariantSelector, and ClientItemInfoLoader parses items/ into ClientItem.

The handshake that lets listeners share a future

Reload listeners are not supposed to reach into each other, and these have to: baking cannot resolve a texture slot without knowing where the sprite landed. The channel that lets them is the resource system’s shared-state pass, and AtlasManager.PENDING_STITCH is the only key the game declares. What matters here is which end of it this page is: AtlasManager publishes a pending stitch for all twelve atlases, and ModelManager awaits exactly the two it bakes against — the blocks atlas and the items atlas — from inside its own prepare, which lets loading and resolving the models overlap the stitching even though the bake waits for it. ParticleResources awaits the particles atlas the same way.

Interning the models, and dropping the ones that loop

In: the raw models/ map. Out: a ResolvedModel per Identifier.

ModelDiscovery is a single worker task and the narrow waist of the pipeline. Every Identifier becomes one ModelDiscovery.ModelWrapper, caching its resolved texture slots and its baked geometry per ModelState, and ModelDiscovery.resolve returns the map of ResolvedModel, whose helpers walk the parent chain for geometry, slots, ambient occlusion and transforms. A model whose parent chain loops is logged and excluded, one whose parent is missing inherits from the missing model, and a model nothing references is parsed and never baked. In parallel, ModelGroupCollector gives each block state a visual-equality group, which is what lets ModelManager.requiresRender later say that change is invisible: two states sharing a group id cost nothing to draw when one becomes the other, unless their fluid state differs.

The JSON model itself is CuboidModel, made of CuboidModelElement, CuboidFace and CuboidRotation, with UnbakedGeometry and UnbakedCuboidGeometry between a resolved model and its quads, and Material, SpriteId, TextureSlots and MaterialBaker are the indirection from a model’s slot reference to a real sprite.

One bake per block state, and why that is affordable

In: resolved models and the stitched sprites. Out: a ModelBakery.BakingResult.

ModelBakery.bakeModels bakes once per block state and once per item model file, sharing one baker whose caches are concurrent maps, and the bakes are batched rather than scheduled individually, so this is at most sixteen tasks per worker thread for each of the two maps, and not tens of thousands. ModelBakery.MissingModels — a block part, a block, an item and a fluid — is baked first of all, so there is always something to substitute. Two further bakes follow: the BlockModel display layer, including the hard-coded BuiltInBlockModels, and the FluidStateModelSet.

That name is the trap this page owes you, and it is worth taking now rather than later. BlockStateModel and BlockModel are different interfaces in different packages. BlockStateModel is the quad source, the thing a mesher reads. BlockModel is the display model, the thing an entity renderer reads, and the tints and the transform a reader expects on the interface belong to its usual implementation, BlockStateModelWrapper, rather than to the interface itself. Two block tables come out of a bake because there are two interfaces, and block-entity rendering explains which is which.

Dedup is what makes per-state baking cheap. Every state sharing an unbaked variant gets the same baked object, geometry is cached per ModelState, and vertex positions and material infos are interned. Multipart is the exception that proves it: each state gets its own thin MultiPartModel over a shared MultiPartModel.SharedBakedState, so every fence, wall, pane and redstone-wire state really is a distinct object.

The output is a QuadCollection of BakedQuad — a ten-component record of four positions, four packed UVs, a Direction recomputed from the baked vertices, and a BakedQuad.MaterialInfo holding the sprite, the ChunkSectionLayer, the item RenderType and its two glint variants, the tint index, shade and light emission. Three things are decided here, and one of them is to put a thing off. Cull faces are rotated at bake time — a rotated variant’s north-culled quad is filed under the rotated direction, in UnbakedCuboidGeometry, while FaceBakery rotates the quad’s vertices and UVs and applies the uvlock — so the mesher never thinks about it. Tint is not resolved: the quad carries an index, and the colour arrives later from BlockColors or an ItemTintSource. And QuadCollection carries translucent and animated flags, OR-ed up through every model wrapper, so the code that handles a block’s model outside the terrain mesh — the block outline, moving blocks, the breaking overlay, the display and item models — can decide whether they need sorting or re-uploading without reading a quad.

A quad’s chunk layer is read out of the sprite’s pixels

The most surprising decision in the pipeline is the one nobody has to make. FaceBakery.bakeQuad does not take the render layer from the block or from any per-face declaration: it asks SpriteContents what transparency exists inside that quad’s UV rectangle. The answer is a Transparency, two booleans wide — are there fully transparent pixels in there, and are there partly transparent ones — and ChunkSectionLayer.byTransparency reads them the other way round: any partial alpha makes it translucent, otherwise any full transparency makes it cutout, otherwise solid. The item render type is picked here too, out of Sheets, by the partial alpha alone and by whether the sprite is on the blocks atlas.

One pixel — enough alpha inside a quad’s UV rectangle to move that face out of the solid layer (FaceBakery, asking SpriteContents).

A pack author who softens one edge of a texture has changed which chunk layer that face draws in, and so when it is sorted and how it blends with everything behind it. There is no warning, and only one way out: a Material may set Material.forceTranslucent, from a force_translucent key beside the sprite name, which skips the scan and takes the translucent layer whatever the pixels say. A hundred and ten of the shipped models use it — the stained glass, mostly — and it is the exception that shows the rule, because it exists for textures whose alpha the scan would read correctly and whose author wants them sorted anyway.

Every layer fails soft, and the two that do not

Missing and malformed input is a warning and a substitution at every layer of the pipeline above, which is why a broken pack usually produces checkerboards rather than a crash report.

what goes wrongwhat you get instead
a model file will not parsethat model alone is dropped
a blockstate file will not parsethat pack’s copy is skipped and a lower pack’s used; with none left, the block’s states are dropped
one variant selector is malformedthat selector alone is dropped
a model’s parent is missingthe missing model stands in as its parent
the parent chain is a cyclethe model is logged and excluded
a bake throwsModelBakery.MissingModels, baked first for exactly this
a sprite id belongs to no atlasthe MissingTextureAtlasSprite checkerboard
a texture slot is unbound, or its chain does not resolvethe same
a block model draws from outside the block atlasthe model is refused and the missing model substituted
a block state has no entry at allthe missing model — the broadest of them, and the last

The atlas-membership case is caught only at the moment a model is baked, and quietly. Minecraft.selfTest, the startup sweep that throws rather than warns, only runs in a development environment, and it looks for states that got the missing model, not at atlases.

Two places have no soft path. If Stitcher cannot grow an atlas within the device’s maximum texture size it raises StitcherException, which becomes a crash report listing every sprite, and mip generation is the second hard crash site. A pack with too many textures does not degrade — it crashes.

The barrier, and how a sprite reaches the GPU

In: everything above. Out: live lookup sets and live GPU textures.

The Render thread started the reload — every listener’s shared state, AtlasManager.prepareSharedState among them, then every listener’s reload, which scheduled the work — and now it does the rest. TextureAtlas.upload builds the new texture and closes the old sprites, and ModelManager.apply assigns the new lookup sets in one go. Underneath sits TextureManager, a PreparableReloadListener owning every AbstractTexture by Identifier — each atlas included — plus the TickableTextures it advances, and it registers a checkerboard texture at MissingTextureAtlasSprite’s location at construction. The GPU side belongs to blaze3d.

How a sprite’s pixels reach the atlas depends on whether it moves. A static sprite goes into a throwaway scratch texture, is blitted into every mip level by a render pass, and the scratch texture is closed. An animated sprite instead keeps one persistent texture per unique frame for the life of the atlas and is redrawn only when it has something new to show, through a second pipeline entirely if the animation interpolates — SpriteContents.AnimationState drives it and TextureAtlas.cycleAnimationFrames steps it.

The flag that rebuilds the world

In: a successful reload. Out: every visible section re-meshed, a frame later.

Nothing rebuilds during the reload. LevelExtractor.allChanged raises a flag, and on the next frame’s extract LevelRenderer.invalidateCompiledGeometry builds a new SectionCompiler from the new model sets and replaces every section with a fresh, uncompiled one, each re-meshed as it is found visible — see section meshing. SectionCompiler is the biggest consumer of BlockStateModelSet.get but far from the only one: the block-breaking overlay, moving blocks, the in-wall screen effect, the nether-portal sprite on the loading screen, TerrainParticle and BlockMarker all read it too, with the HUD, LevelExtractor, FeatureRenderDispatcher and the display table’s own fallback covering the rest, bar the development-only Minecraft.selfTest. The path from a set to triangles is BlockStateModel.collectParts then BlockStateModelPart.getQuads, over SingleVariant, WeightedVariants or MultiPartModel with SimpleModelWrapper as the usual concrete part. None of it crosses the network: the server has no idea what a model is.

How an item picks its model

An item never asks for a model by name. An ItemStack names its model in DataComponents.ITEM_MODEL — an Identifier, nothing else, and a stack without one draws nothing — and that id is the whole decision: a stack whose component says diamond sword renders as a diamond sword whatever item it actually is.

ItemModelResolver — held by Minecraft, called by the entity, block entity and GUI renderers as they extract a frame — reads the component, asks ModelManager.getItemModel for the baked model and ModelManager.getItemProperties for the flags that travel with it, then calls ItemModel.update, which appends the layers it draws to an ItemStackRenderState. Both lookups fall back rather than fail: an unknown id quietly yields the missing item model and the default ClientItem.Properties, whose two flags are whether the item plays the hand-swap animation and whether it may overflow its slot in the GUI, beside a scale for the swap animation.

What each id maps to came from items/, parsed into ClientItem by ClientItemInfoLoader and baked alongside the block models. ItemModels registers eight kinds of unbaked item model and only one of them, CuboidItemModelWrapper, draws quads of its own. EmptyModel draws nothing, CompositeModel stacks the others, SpecialModelWrapper hands the layer to a renderer instead of quads, BundleSelectedItemSpecialRenderer is a bundle’s peek — and the remaining three, RangeSelectItemModel, SelectItemModel and ConditionalItemModel, do not draw at all. They branch. SpecialModelRenderers covers the thirteen shapes no cuboid model can express, from chests and banners to shields, heads, tridents and decorated pots (block-entity rendering is what eleven of those thirteen borrow from). ItemModelGenerator is the odd one out and by far the most-used model in the game: it extrudes a flat sprite into geometry by tracing its alpha channel, which is what item/generated means.

What the three branching models branch on

The three are the whole of how an item’s appearance changes without the item changing, and each reads from its own registry of properties under client/renderer/item/properties. There are thirty-three of them in three shapes, and the shape decides which model can use it.

the modelwhat the property returnshow manyexamples
RangeSelectItemModela float, thresholded against declared cut-offs10, in RangeSelectItemModelPropertiesCompassAngle, Damage, Cooldown, CrossbowPull, BundleFullness, UseDuration
SelectItemModela value matched against declared cases10, in SelectItemModelPropertiesMainHand, DisplayContext, TrimMaterialProperty, ItemBlockState, LocalTime, ContextDimension
ConditionalItemModela boolean, choosing one of two models13, in ConditionalItemModelPropertiesDamaged, Broken, IsUsingItem, IsSelected, IsCarried, FishingRodCast, IsKeybindDown

Three of them keep state, which nothing else in the baking pipeline does. A compass and a clock each run a NeedleDirectionHelper, a damped follower updated once per tick for the baked model rather than per stack, and a compass that has a target damps only when its owner is the local player — which is the wobble, and a model may declare wobble: false and get the raw angle instead. LocalTime is the third, and it re-reads the wall clock at most once a second, which is why a chest item notices Christmas and a placed chest does not.

DisplayContext is the row worth knowing for a different reason: it is what lets one item file draw one thing in your hand, another in a frame and a third in the GUI, and the context comes from whoever is drawing rather than from the stack.

One rule is enforced here. A block model may only use the block atlas, and a cuboid item model must draw every quad from a single atlas — items or blocks, not both. A model that mixes them throws, the exception is caught upstream, and the item falls back to the missing model. Where the stack itself comes from is items and stacks.

Questions players ask

Why does lag slow the water down but the pause menu not stop it? Because animations do not advance once per tick. TextureManager.tick sits outside the client’s catch-up tick loop, so a laggy client advances every animation by one tick however many ticks it just owed. Pausing does not touch it — DeltaTracker keeps handing out ticks with the menu open — and the one thing that does stop the water is /tick freeze, because the guard on that call, beside a tick being owed, is Minecraft.isLevelRunningNormally.

Can I look at the atlas? Yes, and it is the fastest way to understand this page. Options.keyDebugDumpDynamicTextures runs TextureManager.dumpAllSheets, which writes every atlas in the game to disk under the game directory with a text listing of each sprite’s position and size beside it. A shared-constant flag, SharedConstants.DEBUG_DUMP_TEXTURE_ATLAS, makes every atlas dump itself at each upload too, without the keypress.

For a 1.21-era reader. BlockModelShaper is now BlockStateModelSet; BlockElement and BlockElementFace are CuboidModelElement and CuboidFace; BlockModelDefinition is BlockStateModelDispatcher; and BlockRenderDispatcher and ItemRenderer are gone with no successor of that name. One trap: BlockModel still exists and now means something entirely different — the display model above, where the JSON one is CuboidModel.

Where to look

ModelManager.reload for the shape of the pipeline, then AtlasManager.prepareSharedState for the handshake that makes it parallel. ModelDiscovery for resolution, ModelBakery.bakeModels for baking, SpriteLoader.loadAndStitch and Stitcher for the atlas, FaceBakery.bakeQuad for where a face’s layer is decided, and ItemModelResolver and BlockStateModelSet.get for the two ways out.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Entity rendering

Verified against Minecraft 26.3 · Part XI · a zombie is drawn: extract, submit, prepare, execute.

A zombie shuffles out of the dark towards you, you hit it, and for a moment it flashes red. Between that zombie and those pixels stand four stages, each handing the next what the stage before made of it, never the live mob: the live mob is read into a fresh render state, the state is described as things that ought to be drawn, the descriptions are grouped and the translucent ones sorted, and only then does anything write a vertex. Which is why EntityRenderer has no render method — nothing on this page draws until the last stage — and why the zombie is posed at least twice in the frame you are looking at, three times if it is glowing, an arrangement that is only sound because Model.setupAnim resets every part to its baked pose before it starts.

All four stages are one thread, inside one Minecraft.renderFrame. The split is a discipline, not a threading boundary: the first stage touches live entities, the other three only snapshots. Nothing here reads the network — everything drawn is a redrawing of what the server already told the client (what the client is told, synched entity data).

The cast

classwhat it decidesthread
LevelExtractorwhich entities are visible at all, and when their states are builtRender thread
EntityRenderDispatcherwhich EntityRenderer an entity getsRender thread
EntityRendererwhat goes into the render state, and what gets submitted — it never drawsRender thread
EntityRenderStateone entity’s whole frame, copied by value, holding no Entity and no Levela value object
RenderLayerthe extras — armour, held items, eyes, capes — each submitting at its own orderRender thread
SubmitNodeStoragethe submit nodes, bucketed by a global draw orderRender thread
FeatureRenderDispatcherthe grouping, the batching, and where the vertices are finally writtenRender thread
ModelFeatureRendererthe one feature renderer that walks a ModelPart treeRender thread

Four stages, and what each hands the next

flowchart TD
    subgraph EX["GameRenderer.extract"]
        E["Extract: LevelExtractor gets one fresh render state per visible entity"]
    end
    subgraph RE["LevelRenderer.render"]
        S["Submit: each renderer describes what should be drawn"]
        P["Prepare: FeatureRenderDispatcher groups, sorts the translucent, writes vertices"]
        X["Execute: the frame graph's passes issue the draws"]
    end
    E -- "value objects, no Entity, no Level" --> S
    S -- "submit nodes, bucketed by order" --> P
    P -- "one prepared frame" --> X

The four stages and what each hands the next, the first inside the extract and the other three inside the level’s render. Only the box at the top reads a live entity: everything below it works from the copy.

The worked instance is one zombie, traced through extract, submit and prepare in the sections below.

Extract: the live entity becomes a snapshot

In: ClientLevel.entitiesForRendering, and a frustum. Out: one freshly allocated render state per surviving entity. Decided: visibility, and everything the other three stages will ever know.

LevelExtractor.extractVisibleEntities walks the client level’s entity list, drops what LevelExtractor.isEntityVisible rejects, and hands each survivor to EntityRenderDispatcher.extractEntity. That call finds the renderer for the entity’s EntityType through EntityRenderDispatcher.getRenderer — a map the dispatcher rebuilds on every resource reload from EntityRenderers, the static table that files one EntityRendererProvider per type and is the reason a renderer is shared rather than per-entity — allocates a state with EntityRenderer.createRenderState, fills it by running EntityRenderer.extractRenderState down the whole inheritance chain, and then lets EntityRenderer.finalizeRenderState reach into the world one last time to sample the shadow.

Visibility is three tests in two places. EntityRenderer.shouldRender runs two of them, in this order: first Entity.shouldRender (which is the distance test, its limit scaling with the entity’s own bounding box), and only then the frustum. The third — “is the section this entity stands in compiled and faded at least a third of the way in” — belongs to LevelExtractor, which asks LevelRenderer to look the section up: a question the reachability walk never answers, since an entity is not tested against its list. Block entities keep the distance half and meet the frustum only with their section, the few drawn off screen not at all, which is the first difference block-entity rendering makes. Several things escape the frustum entirely: anything indirectly carrying the local player, the three renderers that declare themselves unculled, a Display that sets its own no-culling flag — DisplayRenderer is large for the same reason the entity is unusual: one abstract base carrying the interpolation and billboarding every display shares, with a nested renderer each for a block, an item and a line of text — and, the ones nobody expects, an entity on the other end of a visible leash, an end crystal with a beam target and a guardian firing one, each of which is drawn for what it is attached to: the leash and the beam when they are in view, the crystal whenever it has a target at all.

One zombie through this stage, from the three tests to a finished state:

sequenceDiagram
    participant LX as LevelExtractor
    participant ERD as EntityRender<br/>Dispatcher
    participant ZR as ZombieRenderer

    LX->>ERD: shouldRender, from LevelExtractor.isEntityVisible
    ERD->>ZR: shouldRender — Entity.shouldRender's distance, then the frustum
    LX->>LX: the third test — is its section compiled and visible?
    LX->>ERD: extractEntity, at the zombie's own partial tick
    ERD->>ZR: createRenderState, with the zombie and that partial tick
    ZR->>ZR: a fresh ZombieRenderState, filled down the whole chain
    ZR->>ZR: finalizeRenderState samples the shadow from the blocks below

Extract for one zombie: two of the three visibility tests are the renderer’s, and the extractor asks the third of LevelRenderer. Everything after them happens inside the renderer, which builds a new state object and fills it.

Light is not read at draw time. It comes from EntityRenderer.getPackedLightCoords during extract — the dispatcher has a method of the same name, and nothing calls it — and that method returns full block light for a burning entity. Shadows are sampled here too, and never past sixteen blocks: the renderer walks the blocks under the entity, computes an alpha for each, and stores the shapes and alphas in the state, so that the feature renderer two stages later only has to turn them into quads. The strength falls to nothing at sixteen blocks, so a distant mob has no shadow at any settings, and an invisible entity skips the sampling entirely.

The ladder of render states

The class hierarchy under EntityRenderState: each branch shows how many types descend from it EntityRenderState (99) LivingEntityRenderState (70) ArmedEntityRenderState (17) HumanoidRenderState (13) 3 with no subclasses HoldingEntityRenderState (5) 5 with no subclasses EquineRenderState (2) 2 with no subclasses FelineRenderState (1) CatRenderState SlimeRenderState (1) SulfurCubeRenderState 39 with no subclasses DisplayEntityRenderState (3) 3 with no subclasses ArrowRenderState (1) TippableArrowRenderState ItemClusterRenderState (1) ItemEntityRenderState MinecartRenderState (1) MinecartTntRenderState 18 with no subclasses
Every entity render state, cut three levels down: a number counts the states below a name, a folded row the states it folds, and the zombie's last two rungs are inside HumanoidRenderState's count. Click to enlarge.

The tree is a ladder, each rung adding what the rung below could not assume. EntityRenderState is the floor: position, EntityRenderState.ageInTicks, EntityRenderState.lightCoords, EntityRenderState.outlineColor, EntityRenderState.nameTag, EntityRenderState.leashStates and EntityRenderState.shadowPieces — as true of a dropped item as of a wither. LivingEntityRenderState adds rotations, LivingEntityRenderState.walkAnimationPos, LivingEntityRenderState.deathTime, LivingEntityRenderState.isBaby and LivingEntityRenderState.hasRedOverlay. ArmedEntityRenderState adds hands, HumanoidRenderState a pose and equipment, UndeadRenderState one overridden rule, which the zombies share with the illagers and not the skeletons, ZombieRenderState two flags of its own. The player’s AvatarRenderState branches off the humanoid rung beside them.

Nothing in any of them holds an Entity or a Level — verified across every class in the tree. The one member that looks live, an AnimationState on the eleven states that carry one, is a single-int tick counter copied by value, not a handle back into the world. The one real handle is a falling block’s: its moving-block state carries the level’s light engine, which lights it at prepare.

The red flash is not a colour

It starts as LivingEntity.hurtTime — or LivingEntity.deathTime — and becomes the boolean LivingEntityRenderState.hasRedOverlay here, at extract. At submit, LivingEntityRenderer.getOverlayCoords packs that boolean into an OverlayTexture coordinate alongside a separate white-flash axis, the one the creeper’s fuse uses — and, besides the creeper, only primed TNT, a primed TNT minecart and the sulfur cube’s inner layer. The wither is not on that list: it flashes by swapping to a second texture. That packed integer rides through the submit node untouched and lands, at prepare, as a per-vertex attribute. Nothing along the way is ever tinted red.

Submit: describing a draw without making one

In: the render states, the camera, and a shared PoseStack. Out: submit nodes in SubmitNodeStorage, bucketed by order. Decided: what should be drawn, and where in the world’s draw order.

LevelRenderer.submitEntities walks the states and calls EntityRenderDispatcher.submit for each, after EntityRenderDispatcher.prepare has set the camera for the frame. LivingEntityRenderer.submit then describes the body, poses the model, and lets every RenderLayer describe its own extra through RenderLayer.submit — in that order, because the pose is only needed by the layers. One zombie through this stage:

sequenceDiagram
    participant LR as LevelRenderer
    participant ERD as EntityRender<br/>Dispatcher
    participant ZR as ZombieRenderer
    participant SNS as SubmitNodeStorage
    participant ZM as ZombieModel

    LR->>ERD: submit, for each state LevelRenderer.submitEntities walks
    ERD->>ZR: submit, the pose translated to the zombie
    ZR->>SNS: submitModel — the body, its pose copied
    ZR->>ZM: setupAnim — the first pose, for the layers
    ZR->>ZR: each RenderLayer.submit, at the layer's own order
    ZR->>SNS: submitLeash if leashed, submitNameTag if named, from EntityRenderer.submit
    ERD->>SNS: submitFlame if burning, submitShadow if it has pieces

Submit for one zombie: the body, then the one pose the layers need, then the extras, all of it descriptions filed into SubmitNodeStorage. Nothing here writes a vertex.

The description API is SubmitNodeCollector and its ordered form. The methods a renderer is likely to call are OrderedSubmitNodeCollector.submitModel, .submitItem, .submitText, .submitNameTag, .submitShadow, .submitFlame, .submitLeash, .submitBlockModel and .submitCustomGeometry, plus SubmitNodeCollector.order to choose a bucket — between them eight of the twelve kinds of node the storage can hold, since text and name tags make the same kind, the other four being moving blocks (a falling block’s among them), shape outlines, gizmos and particle groups, which the zombie never submits. SubmitNodeStorage keeps one SubmitNodeCollection per order, and each collection files what it is given into one of fifteen named phases — SubmitNodeCollection.solid, .translucentModels, .breakingOverlay, .outline and eleven more, all catalogued in submit phases and feature renderers.

SubmitNodeCollector.order is a global key, not a per-entity one. Order one means after every entity’s order-zero body in the whole world, not “after this entity’s body”. That is how the eyes layer, the armour layers and the enchantment glint stack correctly across a crowd instead of interleaving one mob’s helmet with another’s head. Armour claims consecutive orders as it goes, so a dyed, enchanted, trimmed helmet occupies four — leather is the only dyeable helmet and its equipment definition has two layers of its own — and exactly one layer in the game asks for a negative order, to get underneath everything: SulfurCubeInnerLayer, which has to sit inside the shell it is drawn with.

The pose stack is transient, and half of it is dropped. A submit copies the current pose; nothing downstream ever sees the stack. Models, items, block models, flames, shape outlines and custom geometry copy the full pose, while shadows, name tags, text, leashes and moving blocks copy only the 4×4, so no normal matrix crosses for those. A leaked push is fatal, but the check happens at the end of the submit phase, on a local stack, not at the end of the frame. Avatars differ in one small way: the crouch offset is removed before the shadow is submitted for a player and after it for everything else, which stops a sneaking player’s shadow sinking into the ground.

The renderers and models being described

Renderers and models are shared and mutable; render states are not. One ZombieRenderer serves every zombie in the world — though it holds an adult model, a baby model and two baked armour sets — and safety comes entirely from the fresh per-entity state and from replaying the animation at prepare time. The chain is EntityRenderer → LivingEntityRenderer → MobRenderer → AgeableMobRenderer → HumanoidMobRenderer → AbstractZombieRenderer → ZombieRenderer. AgeableMobRenderer is deprecated and is not the base of every humanoid: the enderman and the giant extend MobRenderer directly, the armour stand and the avatar extend LivingEntityRenderer, all four while using humanoid models. Players are served by AvatarRenderer, keyed by skin model rather than entity type — the type map has no entry for the player or the mannequin, and the dispatcher keeps two avatar maps, one for players and one for mannequins, each keyed wide and slim and falling back to wide.

Geometry is Model → EntityModel → HumanoidModel, built out of ModelParts. A model is baked once from a LayerDefinition / MeshDefinition / PartDefinition tree named by a ModelLayerLocation in ModelLayers and held in an EntityModelSet; LayerDefinitions is the single static table that builds every one of them, out of the CubeListBuilder / CubeDefinition / CubeDeformation / PartPose vocabulary. Posing is Model.setupAnim, hand-written or driven by an AnimationDefinition of Keyframes through KeyframeAnimation, whose channels interpolate linearly or along a Catmull–Rom spline.

Extras hang off RenderLayer — HumanoidArmorLayer, ItemInHandLayer, CustomHeadLayer, WingsLayer, EyesLayer, CapeLayer and forty-odd more. Armour and trims funnel through EquipmentLayerRenderer, dressed by EquipmentAssetManager and EquipmentClientInfo out of the resource packs, and a renderer holds a whole ArmorModelSet per body size rather than one armour model. Held or worn items come through ItemModelResolver and ItemStackRenderState, which models and atlases covers.

A player is a skin record and seven booleans

Player anatomy leaves the drawing here, and the answer is that all of it becomes render state at extract like everything else. AvatarRenderState carries the whole PlayerSkin record by value — three textures, the arm width and the secure flag — read, for a player, off the tab-list entry rather than off the entity, which is why a skin change needs no entity packet; a mannequin carries its own. Beside it sit seven booleans, one per PlayerModelPart: the hat, the jacket, the two sleeves, the two trouser legs and the cape, each copied from Avatar.isModelPartShown once per frame, so a customisation toggle is a part the model is told not to draw rather than a different model. PlayerModelType is the arm width inside that record, and the dispatcher reads it back out of the state at submit as the key into the players’ avatar map, whichever avatar the state came from. The textures themselves arrive by a road of their own, SkinManager and SkinTextureDownloader, with a PlayerSkinRenderCache in front for the profiles a mannequin or a head names and DefaultPlayerSkin standing in until one does.

Prepare: sorting, batching, and the vertices

In: a whole frame of submit nodes. Out: a FeatureRenderDispatcher.PreparedFrame whose vertices already exist. Decided: how few draws this can be.

FeatureRenderDispatcher.prepareFrame drains every phase of every order bucket, groups the nodes, and lets the twelve feature renderers build geometry — ModelFeatureRenderer being where a ModelPart tree finally becomes vertices, as it does here for one zombie:

sequenceDiagram
    participant LR as LevelRenderer
    participant FRD as FeatureRender<br/>Dispatcher
    participant MFR as ModelFeature<br/>Renderer
    participant ZM as ZombieModel

    LR->>FRD: prepareFrame
    FRD->>FRD: drain every phase of every order bucket, and group
    FRD->>MFR: prepareGroup, for the model submits
    MFR->>ZM: setupAnim — the second pose, from the baked one
    MFR->>ZM: renderToBuffer — the vertices, written now
    Note over LR,ZM: drawn later, when the frame graph runs the main pass

Prepare for one zombie: the model is posed a second time because its submit carried the state but not a pose for every part. The vertices exist before any of the frame graph’s passes has run.

The phases come in two kinds and the kind decides how hard the grouping is allowed to try. With improved transparency off, as a new client starts, twelve are a SimpleFeatureRenderPhase, which groups by feature type and then by batch key — and only two of the twelve kinds of submit have a batch key at all, a model and a piece of custom geometry. Everything else keeps the order it was submitted in. Either way, a simple phase’s RenderTypeFeatureRenderer.Group is free to fold a node’s geometry into any earlier draw of the same RenderType rather than only the adjacent one, which is how the zombies of the world collapse into one draw.

The other three phases are a TranslucentFeatureRenderPhase (with improved transparency on, ten of the fifteen names share one simple phase, and only the see-through phase sorts). That one keeps every node, sorts them back to front by squared distance to the camera, and marks the group strictly ordered — which switches off exactly one of the group’s two merges. Consecutive submits of one render type still share a draw. What stops is the fold into a non-adjacent earlier draw, which is the merge that would move geometry ahead of everything between it and its target, and so undo the sort the phase exists for. A render type may also opt out of both merges on its own account, through RenderType.canConsolidateConsecutiveGeometry, which is how something whose primitives are chained keeps them chained. Which three phases are the translucent ones, and what lands in each of the fifteen, is submit phases and feature renderers.

Why the zombie is animated more than once

Once during submit — but only because it has layers, and ItemInHandLayer, CustomHeadLayer and several others need posed ModelParts to hang things off. Once again, per model submission, at prepare time, because the submit node carried the model and the state but not a pose for every part. A glowing zombie is animated three times, since the outline is a second submission of the same model into SubmitNodeCollection.outline. A fourth pass exists in the machinery — the crumbling overlay — but no entity ever reaches it: the only non-null ModelFeatureRenderer.CrumblingOverlay in the game is built in LevelExtractor’s block-entity loop, so it is a chest being mined that gets a fourth pose, never a mob. That the repeats are sound at all is only because Model.setupAnim resets every part to its baked pose first, so each call is idempotent from a known base — not because the model is stateless, which it emphatically is not.

Execute: the frame graph pulls the trigger

In: the prepared frame. Out: draws. Decided: almost nothing — the ordering was fixed at submit and prepare.

FeatureRenderDispatcher.PreparedFrame exposes eight drains, each handed the render pass to draw into, and the frame graph’s main pass is where each is called: .executeSolid always, .executeOutline when something glows, .executeTranslucent and .executeTranslucentAfterTerrain for classic transparency, .executeWaterMask and .executeOit for improved transparency, and .executeSeeThrough and .executeAlwaysOnTop when something needs them. Only the solid and classic translucent drains share the main render pass; the others each get a render pass the main pass opens, the always-on-top one clearing depth first. Where those passes sit relative to terrain, sky and post-processing is the subject of visibility and the frame graph; the draws go out through blaze3d.

What borrows this pipeline without being it

Block entities take the same four stages under a different visibility policy, a different partial tick and an empty block model — block-entity rendering is the whole of the difference.

The first-person hand is a second pipeline from submit on: its state is extracted with the player’s (FirstPersonHandsAndItems), then FirstPersonHandsAndItemsRenderer submits into a SubmitNodeStorage it shares with the screen effects, which is drawn by FeatureRenderDispatcher.renderAllFeatures outside the frame graph — see the frame.

Hitboxes left the renderer. F3+B is a debug-entry toggle read by a separate debug renderer that emits gizmo primitives, suppressed under reduced debug info, and the old hitbox render state record is dead code with exactly two references, both inside its own file. Name tags borrow in the same way: the submit node carries the text already laid out and measured, and every glyph in it is resolved by text and fonts, not here.

For a 1.21-era reader. MultiBufferSource does not exist anywhere in the game, nor any other buffer source: a renderer submits, and the feature renderers write the vertices. RenderTypes.entityCutoutNoCull is now RenderTypes.entityCutout, the polarity flipped so that the culled variant, RenderTypes.entityCutoutCull, is the one that says so. And ItemBlockRenderTypes is gone: a quad’s layer is read out of its sprite.

Where to look

EntityRenderDispatcher.extractEntity and EntityRenderer.extractRenderState for the first stage, LivingEntityRenderer.submit for the second — the clearest single method in the part. Then SubmitNodeCollection for the phase list and ModelFeatureRenderer for where vertices are written. ModelLayers and LayerDefinitions for how a model is described, and submit phases and feature renderers for both catalogues.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Block-entity rendering

Verified against Minecraft 26.3 · Part XI · a chest on the ground and a chest in your hand, drawn in the same frame by two renderers that share one model definition.

You place a chest, step back, and hold a second one up in front of your face. Both are chests, both are lit, both open the same lid on the same hinge — and almost nothing about how they got onto the screen is shared. The one on the ground is a block entity: the terrain mesh at its position contains no geometry at all, and everything you can see of it was extracted from the live world by ChestRenderer a few microseconds ago. The one in your hand is an item: it has no block entity, it is extracted as part of your player, and it is drawn by a class in a different package that exists for chests that are not block entities. The seam shows if you type /tick freeze. The chest on the ground is nailed to its last tick; the chest in your hand keeps swaying with your view bob, because the two are drawn at different partial ticks and only one of them respects the freeze.

This page is the sibling of entity rendering, and does not re-teach it. Four stages run here exactly as they run there — extract reads the live world into a value object, submit describes what ought to be drawn without drawing it, prepare groups every description in the frame and sorts the translucent phases back to front, and execute is the frame graph’s passes issuing the draws — along with render states that hold no live entity, SubmitNodeCollector, and the phases a submission can land in. All of that is that page’s, and all of it is true here. What follows is only the differences, and they are larger than the shared machinery suggests.

The cast

classwhat it decidesthread
LevelExtractorwhich block entities are candidates at all — the two lists it walksRender thread
BlockEntityRenderDispatcherthe renderer for a type, and two of the three gates an extraction passesRender thread
BlockEntityRendererthe geometry, and its own answers to how far and off screenRender thread
BlockEntityRenderStateone block entity’s whole frame: position, block state, type, light, break progressa value object
ClientLevelmembership of the globally-rendered set, decided once when the block entity is addedRender thread
SpecialModelRendererthe thirteen shapes an item model cannot express, drawn without a block entityRender thread
SpecialModelWrapperhow an item model reaches one — the item road into renderer/specialRender thread
BuiltInBlockModelshow a block state reaches one, in a model table terrain never readsa worker thread, on every resource reload

Three roads to the same collector

flowchart TD
    BE["A chest placed in the world"]
    IT["A chest as an item: held, shelved or dropped"]
    BD["A chest block shown by an entity"]
    SEC["LevelExtractor walks the visible sections, then the globally-rendered set"]
    BERD["BlockEntityRenderDispatcher — one shared ChestRenderer, one fresh ChestRenderState"]
    IMR["ItemModelResolver finds the stack's item model"]
    SMW["SpecialModelWrapper opens a layer with its renderer"]
    BMR["BlockModelResolver reads the built-in block-model table"]
    SBM["SpecialBlockModelWrapper sets its renderer on a BlockModelRenderState"]
    CSR["a ChestSpecialRenderer, from renderer/special"]
    COL["SubmitNodeCollector — the same phases, the same feature renderers, the same vertices"]
    BE --> SEC
    SEC --> BERD
    BERD --> COL
    IT --> IMR
    IMR --> SMW
    SMW --> CSR
    BD --> BMR
    BMR --> SBM
    SBM --> CSR
    CSR --> COL

Three ways a chest reaches the collector: its own block entity on the left, and two roads that end in the same special renderer class on the right. Only the left road’s chest is a block entity; the other two are drawn by whatever holds them.

Two of the three roads end in renderer/special, and that is the package’s whole reason to exist: a chest that is not a block entity still has to look like a chest. Only the left-hand road has a visibility policy of its own, or an extract stage that reads the live world.

entityblock entityspecial model
what is walkedthe level’s renderable entitiesthe visible sections’ meshes, then a global setnothing — it is reached from a model
the visibility testa frustum, plus a size-scaled distancethe section is visible, then a per-renderer radiuswhatever drew the thing holding it
the stagesextract, finalize, submitextract, submitresolved in its holder’s extract, submitted in its holder’s submit
the statean EntityRenderState subclassa BlockEntityRenderState subclassa layer of an ItemStackRenderState, or a BlockModelRenderState
the partial tickone computed per entityone for every block entity in the worldits holder’s — in your hand, the player’s, then the camera entity’s
where the pose comes fromthe dispatcher, from the state’s positionLevelRenderer, translated to the blockthe item transform for the display context, or the block model’s own
how manyone renderer per entity type, or per skin model for a player or a mannequin26 of the 49 types, served by 24 classes — ChestRenderer takes three of them13 renderers under 13 ids

The chest, both halves, one frame

Followed through one frame, band by band, the two chests take these routes:

sequenceDiagram
    participant LX as LevelExtractor
    participant FPHAI as FirstPersonHands<br/>AndItems
    participant BERD as BlockEntity<br/>RenderDispatcher
    participant ChestR as ChestRenderer
    participant LR as LevelRenderer
    participant FPHAIR as FirstPersonHands<br/>AndItemsRenderer

    rect rgba(0, 0, 0, 0.04)
    Note over LX,FPHAIR: extract — the hand at the player's partial tick, the block entities at the world's
    LX->>FPHAI: extractRenderState, at the player's own partial tick
    FPHAI->>FPHAI: the held chest resolved — a special renderer, no quads
    LX->>BERD: tryExtractRenderState, for a block entity in a visible section
    BERD->>ChestR: createRenderState, then extractRenderState, past the off-screen and distance gates
    end
    rect rgba(0, 0, 0, 0.04)
    Note over LX,FPHAIR: submit — into the level's storage, then the hand's own
    LR->>BERD: submit, the pose translated to the block
    BERD->>ChestR: submit — one model, one sprite, no world access
    FPHAIR->>FPHAIR: the held stack's layer submits to the ChestSpecialRenderer
    end

Both chests in one frame, extract then submit: the ground chest goes through the block-entity classes, the held one through the hand’s. They have separate renderers, states and storages, and share the chest model’s layer definition.

The two chests are not two runs of one pipeline. The world’s block entities go through LevelRenderer.submitFeatures into the frame graph. The held chest is extracted with the player, by FirstPersonHandsAndItems, and FirstPersonHandsAndItemsRenderer submits it into the hand’s own submit storage, drained after the whole world is already on the screen. They never share a submit node.

The chest’s block model is empty, and there are two tables of them

Open blockstates/chest.json and it names one model for every state. Open that model and it declares a particle texture and no elements. So when SectionCompiler walks the section and tesselates every block whose render shape is MODEL, the chest contributes exactly zero quads to the terrain mesh — and, in the same pass, adds itself to the section’s list of block entities. Everything you see of a placed chest is drawn one stage later, by a renderer, from a snapshot.

That is what a block entity renderer is for: the shapes that a cuboid model cannot express, and the state that a block state cannot hold. Only 26 of the 49 entries in BlockEntityTypes have a renderer registered in BlockEntityRenderers, and the other 23 — furnaces, hoppers, barrels, beehives — are drawn entirely by their block models, like any other block. Having a block entity does not make a block interesting to look at.

There are two baked block-model tables, and confusing them is easy. ModelManager.getBlockStateModelSet is the one built from the resource packs, and it is the one SectionCompiler reads: a chest is empty in it. ModelManager.getBlockModelSet is built on top of that one and merged with BuiltInBlockModels.createBlockModels, which attaches a SpecialBlockModelWrapper to every state of every chest, banner, skull, shulker box, conduit, decorated pot, bell, enchanting table, end gateway, end portal and copper golem statue in the game. Nothing in terrain ever reads that table — that is the whole of the separation, and it is membership rather than behaviour. BlockModelResolver is its one reader, and every caller that reads it is an entity renderer: item frames, block displays, minecart contents, the golems, the block an enderman is carrying, and a few more. It reaches the block-entity side too, as a field of BlockEntityRendererProvider.Context — the record the block-entity renderers are constructed from — where nothing reads it, because a block entity already has a renderer and does not need to find one through a model. When an entity renderer draws a chest it gets whatever quads the block has, which for a chest is none, and the special renderer, because that road draws whatever it finds; terrain simply never asks this table, and reads BlockStateModelSet instead, where the chest’s entry is empty.

What one entry in the built-in table actually holds

The reason one entry can hold quads and a renderer is the object the road resolves into. BlockModelResolver hands the model a BlockModelRenderState, which has a slot for each: a list of BlockStateModelPart, a transformation, a RenderType, a SpecialModelRenderer with a transformation of its own, and the tint layers and light. Every BlockModel implementation writes into that one object, and what comes out is submitted in one go.

Not every entry in it is a wrapped block, either. The three airs are an EmptyBlockModel; wildflowers and pink petals are a SelectBlockModel that branches on the display context; and the two ordinary chests are a CompositeBlockModel whose special half is a ConditionalBlockModel — the Christmas switch this page comes back to. Most of the rest are a CompositeBlockModel too: the block’s own quads and the special renderer stacked. Five are not, and get the renderer alone with no quads under it — the bell, the conduit, the end gateway, the end portal and the enchanting table, whose built-in model is BookSpecialRenderer and nothing else. Put an enchanting table in a block display and you get a book hanging in the air.

Frustum-culled by section, not one by one

A block entity is never frustum-tested on its own. LevelExtractor.extractVisibleBlockEntities starts from LevelRenderer.visibleSections — the sections of the reachability walk that survived the frustum, which visibility and the frame graph describes — and takes each section’s compiled list of block entities whole. Culling has already happened, one section at a time.

Three gates of visibility decide whether a block entity is extracted from a section, and only the first belongs to the walk. The other two are stricter than they look, and BlockEntityRenderDispatcher.tryExtractRenderState holds both. The first is the section’s own fade-in: a freshly uploaded section reports a visibility ramping from zero to one over a duration the chunk section fade-in time option sets, and LevelExtractor skips its block entities until that number reaches 0.3. Terrain fades in from the first frame, and the chests inside it appear about a third of the way through — furniture arriving after the room. The ramp starts at a section’s first upload and a recompile never restarts it, so the gate bites on arriving terrain at any distance, and never on a chest you place in terrain already drawn.

The second is the off-screen flag, which the section after this one is about. The third is a distance test with the same name as the entity one and only half of its behaviour. EntityRenderer.shouldRender does a size-scaled distance test and a frustum intersection. BlockEntityRenderer.shouldRender keeps the distance half alone: a camera position, and whether the block’s centre is within BlockEntityRenderer.getViewDistance.

Sixty-four blocks — the default, taken by nineteen of the twenty-four renderer classes, and it does not scale with your render distance the way Entity.shouldRender does.

rendererhow farwhy
the other nineteen64the interface default
PistonHeadRenderer68a moving block starts outside the block it is drawn from
BlockEntityWithBoundingBoxRenderer96the structure block’s outline is a build tool — and its extraction sets a visibility flag from Player.canUseGameMasterBlocks or spectator mode, so its output, and the box the test-instance renderer draws through it, depends on your permissions
TheEndGatewayRenderer256the beam is the thing you are looking for
BeaconRendererthe render distance in blocksand measured horizontally only
TestInstanceRendererthe larger of its two delegatesit wraps a beacon and a bounding box

The beacon is the interesting row twice over. Its BlockEntityRenderer.shouldRender flattens both positions onto the horizontal plane before comparing, so altitude never costs you the beam — you can be at build height above a beacon at bedrock and still see it. And its extraction scales the beam’s radius by the horizontal distance divided by 96, floored at one, so the beam gets visibly wider the further away you stand, which is why a distant beacon does not thin into nothing. Raising a spyglass resets the scale to one, because BeaconRenderer checks whether the local player is scoping. The topmost beam segment is drawn BeaconRenderer.MAX_RENDER_Y tall, 2048 blocks up from where it starts.

Off screen means off this list

BlockEntityRenderer.shouldRenderOffScreen picks which of two lists a block entity is drawn from, and it is enforced twice. ClientLevel.onBlockEntityAdded puts a block entity into ClientLevel.getGloballyRenderedBlockEntities only if its renderer says yes, and BlockEntityRenderDispatcher.tryExtractRenderState throws the extraction away unless the flag it was called with equals the renderer’s answer. The second check is what stops double-drawing: a beacon inside a visible section is in that section’s list and in the global set, and the equality test is the only thing that picks one. Exactly three renderers say yes — BeaconRenderer, BlockEntityWithBoundingBoxRenderer and TestInstanceRenderer, which says yes because either of its two delegates does.

What a block entity’s snapshot carries

blockentity/state holds 26 classes: the base and twenty-five subclasses. The base is five fields wide — the position, the block state, the type, packed light sampled from the level, and the crumbling overlay — and it is filled by one static method, BlockEntityRenderState.extractBase, that every renderer calls before adding its own. There is no EntityRenderer.finalizeRenderState counterpart here: BlockEntityRenderer declares one extraction method, not two, so there is no second extraction step; the two live handles this page comes to are held by reference instead.

The crumbling overlay is the fifth field and the only one built outside the renderer. LevelExtractor looks the block position up in ClientLevel.destructionProgress, takes the last entry of the sorted set — BlockDestructionProgress orders on the stage first and the digger’s entity id only to break a tie, so two players on one block show whichever crack is further along — and wraps that stage and a pose into a ModelFeatureRenderer.CrumblingOverlay. This is the only place in the game a non-null one is made, which is why a mob is never crumbled and a chest being mined is. The global list is extracted with a null overlay outright, so a beacon someone is breaking gets its cracks from its block model alone, never from its renderer.

Twenty-five subclasses, twenty-six classes: BedRenderState is reachable from nothing in the game. There is no bed block entity in BlockEntityTypes and no bed renderer, and a corpus-wide search for the name finds only its own file. It is the only orphan in the package.

The five states that carry another pipeline’s snapshot

Five states carry another pipeline’s snapshot inside them, which is where the machines touch — and two of the five carry an entity state, not an item one. SpawnerRenderState.displayEntity is a whole EntityRenderState, extracted through EntityRenderDispatcher from a display entity the spawner creates client-side — a mob that LevelExtractor never sees, never frustum-tests, and whose light the spawner overwrites with the block’s. VaultRenderState.displayItem reads like the item cases and is not one: it is an ItemClusterRenderState, which extends EntityRenderState, and the vault submits it through ItemEntityRenderer — the same renderer that draws a dropped item lying on the ground. The three genuine item carriers are ShelfRenderState.items, an array of three ItemStackRenderState — so a shelved item is resolved and submitted by the item road, from inside a block-entity render state, and lands on whichever special renderer its own model names — and CampfireRenderState.items and BrushableBlockRenderState.itemState for what is cooking and what is buried.

Two live handles survive into snapshots, and the first is not unique to this side: MovingBlockRenderState is a one-block fake world that holds the level’s MovingBlockRenderState.lightEngine and MovingBlockRenderState.cardinalLighting by reference, so a moving block is lit at prepare time rather than at extract. PistonHeadRenderState carries up to two of them, and the entity side’s FallingBlockRenderState carries one.

Signs are the second. SignRenderState stores the block entity’s own two SignText objects rather than laid-out glyphs, and AbstractSignRenderer calls Font.split during submit when that text has no lines cached for the current filtering setting — a stage later than the rest of the snapshot.

One partial tick for the whole world

This is the difference a player can see. Six partial ticks are in play in one frame, and block entities take the row that has no exception in it. The entity side is asked per entity, TickRateManager.isEntityFrozen at a time, so a mob exempt from a freeze keeps interpolating while its neighbours stop. Block entities get no such question: they all receive the single DeltaTracker.getGameTimeDeltaPartialTick value the world got, which returns exactly 1.0 while the game is frozen. Every block entity in the world is therefore pinned to its last completed tick, with no per-block exemption anywhere in the path.

The held chest is asked the entity side’s question, twice: FirstPersonHandsAndItems.extractRenderState runs at the local player’s own partial tick, and the hand is posed at submit off Camera.getCameraEntityPartialTicks, which puts the same question to the camera entity. The frozen check never freezes a Player, so under /tick freeze the item in your hand is redrawn from a live partial tick while every chest lid in the world is stopped dead. A mob exempt from the freeze, a chest lid and the chest in your hand: three things drawn in one frame, and whether each one moves depends only on which question it was allowed to ask.

The same split shows up in the Christmas textures, which the game implements three times. ChestRenderer reads SpecialDates.isExtendedChristmas once, in its constructor, and its constructor runs only when BlockEntityRenderDispatcher.onResourceManagerReload rebuilds every renderer — so a placed chest that was ordinary at 23:59 on the 23rd stays ordinary until the next resource reload. The item model in items/chest.json selects on the minecraft:local_time property, whose LocalTime implementation re-checks the clock at most once a second. And the built-in block model wraps its two chests in a ConditionalBlockModel whose IsXmas property calls the same static method live, every time the model is resolved. Two of the three notice midnight almost at once. The one you are standing in front of does not.

Where renderer/special borrows its geometry

SpecialModelRenderers.bootstrap registers thirteen renderers under thirteen ids, dispatched by a codec, and the package holds exactly thirteen renderer classes to match. Nine of them implement NoDataSpecialModelRenderer and read nothing at all from the stack; the other four — banner, decorated pot, player head and shield — pull what they need out of it through SpecialModelRenderer.extractArgument, which is the closest thing this road has to an extract stage.

Eleven of the thirteen reach into client/renderer/blockentity for their geometry. Three hold an instance of the block-entity renderer outright (BannerSpecialRenderer, DecoratedPotSpecialRenderer, ShulkerBoxSpecialRenderer); the rest call a static submit or name a static texture or model layer on one — SkullBlockRenderer.submitSkull, AbstractEndPortalRenderer.submitSpecial, BannerRenderer.submitPatterns, ChestRenderer.LAYERS. Only TridentSpecialRenderer and CopperGolemStatueSpecialRenderer stand alone. The chest in your hand really is a ChestModel, baked from the same ModelLayerLocation, posed at a fixed openness instead of an interpolated one.

ChestRenderer aside, this page teaches the shape of the twenty-four rather than the instances, because a family is what they are: each is one BlockEntityRenderer.extractRenderState and one BlockEntityRenderer.submit, and the interesting ones differ in how far they can be seen and from which list, both above, and in what they put in their render state — the structure block drawing gizmos instead of submitting geometry, and the test instance drawing gizmos beside a submitted beacon beam. The shared piece worth naming is WallAndGroundTransformations, which is how a skull, a banner or a sign answers am I on the floor or on a wall — one transformation per Direction for the wall case and an array indexed by rotation segment for the free-standing one, built once into a static field of the renderer rather than per frame.

How an empty item model turns into a chest

Reaching a special renderer from an item is one indirection: SpecialModelWrapper is an ItemModel like any other, and its baked form puts the renderer into a layer of the ItemStackRenderState through ItemStackRenderState.LayerRenderState.setupSpecialModel. A layer either has quads or has a special renderer, never both, and the layer’s submit picks whichever it has. That is the entire mechanism by which an empty item model turns into a chest.

For a 1.21-era reader. BedRenderer is gone, and so is the bed’s block entity: a bed is drawn by its block models like any other block, and the BedRenderState the renderer filled is left in blockentity/state with nothing to fill it.

Where to look

BlockEntityRenderDispatcher.tryExtractRenderState first — it contains two of the three visibility gates. Then LevelExtractor.extractVisibleBlockEntities for the two lists it walks, and ChestRenderer as the clearest renderer in the package, since its counterpart in renderer/special is the one this page compares it with. For the other road, SpecialModelWrapper and ItemStackRenderState.LayerRenderState, then BuiltInBlockModels for the block-state road nobody expects to exist. Submit phases and feature renderers is the catalogue everything here submits into.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Lightmap, fog and sky

Verified against Minecraft 26.3 · Part XI · the sun goes down: the light, the fog and the sky, traced back to the day’s keyframe curves.

Stand on a hill and watch the light go. The sky over the taiga slides from blue towards black, the murk closes in until the far trees dissolve, stars come up, the moon takes whatever shape it is owed tonight, and if a storm arrives the scene goes grey and streaked. Five renderers make those colours — Lightmap decides how bright, FogRenderer how far, SkyRenderer and CloudRenderer what is up there, WeatherEffectRenderer what is coming down — and four of them ask one question above all: what is this attribute worth, here, now? The surprise is who they ask. Most of them no longer know what time it is. They ask a probe for a named value at a position and a partial tick, and the day/night curve behind it is keyframes in a data pack. Two still read the level’s raw game time — the clouds, because they drift, and the rain, whose texture scrolls — but the weather asks nobody for a colour: it seeds each column of rain from that column’s own coordinates, and touches no attribute and no probe at all.

The cast

classwhat it decidesthread
EnvironmentAttributeProbewhat any attribute is worth at the camera — cached once a tick, read many times a frameRender thread
LightmapRenderStateExtractorthe lightmap’s ten uniforms, and whether to redraw at allRender thread
Lightmaphow bright, as the 16×16 texture every terrain vertex samplesRender thread
FogRendererhow far you can see, in what colour, and in which mediumRender thread
SkyRendererwhat hangs above the horizon — and which of two skies it isRender thread
CloudRendererthe cloud cells, and the face list built from themthe reload’s CloudRenderer.prepare bakes on a worker, the rest on Render
WeatherEffectRendererwhich columns get rain, which get snow, and how hardRender thread
LevelRendererwhich of those draw at all, and inside which frame-graph passRender thread

What a renderer has to know about an attribute, and no more

An environment attribute is a named, typed quantity — a colour, a distance, an angle, a moon phase — that the world answers for a position and an instant. EnvironmentAttributeSystem assembles that answer by running a short stack of layers over the attribute’s own default: the dimension, the biome, one layer per timeline the dimension runs, and weather where a dimension can have it. That machinery belongs to environment attributes and timelines; this page assumes it and names only what it consumes. Three consequences shape everything below.

The client resolves the same stack from the same data — it is never sent a resolved colour — and what it adds is EnvironmentAttributeProbe, on the camera: a per-tick cache that smooths in space and in time and evicts anything nobody asked for. What matters on this page is that every renderer here that asks for an attribute at all goes through the probe and never through the system — and one of the five asks for none.

Whether a value can smooth or must step is declared on the attribute’s type, and whether it smooths across a biome border on the attribute itself, not chosen by the renderer — which is why the sky colour slides and EnvironmentAttributes.MOON_PHASE snaps — and the shape of a smooth one’s curve in time is the timeline’s data, its track’s ease.

ClientLevel adds two layers of its own on top of the four, and both are the lightning flash (the stack a value falls through). Neither has anything to do with the End’s sky flash, which never enters the stack at all.

What the dimension type and the biome still carry

Some of the old per-dimension and per-biome data survived the migration unchanged. DimensionType.skybox is a three-valued DimensionType.Skybox — DimensionType.Skybox.NONE, DimensionType.Skybox.OVERWORLD, DimensionType.Skybox.END — and it is a branch, not a colour. BiomeSpecialEffects holds only BiomeSpecialEffects.waterColor, BiomeSpecialEffects.grassColorOverride, BiomeSpecialEffects.grassColorModifier and the foliage colours: every fog and sky colour comes from Biome.getAttributes. None of it crosses the network as pixels — the inputs arrive as registry sync during configuration, attribute maps filtered through EnvironmentAttributeMap.NETWORK_CODEC, then world time and weather during play (protocol phases) — so a data pack retints a dimension without touching the client.

The five askers

rendererwhat it asks forwhen it askswhat it produces
Lightmap, through LightmapRenderStateExtractorhow bright block light and sky light should read, and in what tintonce per tick, at a partial tick of exactly oneten std140 uniforms and one 16×16 texture
FogRendererthe colour of the murk and the six distances it lives betweenonce per frame, inside the camera extractone FogData, uploaded as one UBO slice
SkyRendererwhere the sun, moon and stars are, and how brightonce per framea SkyRenderState for the sky pass
CloudRendererwhat colour the clouds are and how high they sitonce per frame, read for it by LevelExtractora compressed face list, rebaked only when it must be
WeatherEffectRendererno attribute: the rain level, then each column’s height, precipitation and lightonce per frametwo lists of WeatherEffectRenderer.ColumnInstance, rain and snow

Dusk, from a keyframe track to five renderers

sequenceDiagram
    participant LRSE as LightmapRender<br/>StateExtractor
    participant EAP as Environment<br/>AttributeProbe
    participant EAS as Environment<br/>AttributeSystem
    participant LM as Lightmap
    participant FR as FogRenderer
    participant SR as SkyRenderer

    rect rgba(0, 0, 0, 0.04)
    Note over LRSE,SR: a client tick
    LRSE->>LRSE: tick — the flicker walks, the flag raised
    EAP->>EAP: tick — each value rolled over or evicted, the biome blend resampled
    EAS->>EAS: invalidateTickCache — cached values stale, none recomputed
    end
    rect rgba(0, 0, 0, 0.04)
    Note over LRSE,SR: each frame's extract, in this order
    opt the flag a tick raised, so at most once a tick
        LRSE->>EAP: getValue(SKY_LIGHT_FACTOR, BLOCK_LIGHT_TINT, …) at partial tick 1
    end
    EAP->>EAS: getValue, an attribute's first ask this tick — timelines sampled now
    LRSE->>LM: LightmapRenderState, and the flag
    FR->>EAP: getValue(FOG_COLOR, SUNRISE_SUNSET_COLOR, …) at the frame's partial tick
    SR->>EAP: getValue(SUN_ANGLE, SKY_COLOR, SUNRISE_SUNSET_COLOR, …)
    end
    rect rgba(0, 0, 0, 0.04)
    Note over LRSE,SR: each frame's render
    opt the same flag
        LM->>LM: render — one three-vertex draw into the 16×16 texture
    end
    SR->>SR: render, in the sky pass — disc, fan, sun, moon, stars
    end

Dusk across one tick and one frame, drawn for the lightmap, the fog and the sky. The probe asks the attribute system only on an attribute’s first read in a tick, and the lightmap asks and draws only when a tick has raised its flag.

The clouds are left out because they add nothing new to the picture: LevelExtractor reads their colour and height from the same probe during the extract. The rain asks it nothing.

The middle band’s order is the order GameRenderer.extract makes its calls in. GameRenderer.extract runs LightmapRenderStateExtractor.extract, then GameRenderer.extractCamera — where FogRenderer.setupFog stashes its FogData on CameraRenderState.fogData — then LevelExtractor.extract, which drives WeatherEffectRenderer.extractRenderState and SkyRenderer.extractRenderState. Then GameRenderer.renderLevel uploads the fog with FogRenderer.updateBuffer, takes one slice with FogRenderer.getBuffer, and LevelRenderer.render declares the passes.

SkyRenderer.renderSunriseAndSunset is the clearest instance of the pattern. The sunrise fan’s colour is EnvironmentAttributes.SUNRISE_SUNSET_COLOR, an ARGB keyframe track, and its visibility is that colour’s own alpha channel — which the renderer also scales the fan’s depth by. The fade is a property of the data, not of the geometry, so a data pack restyles the sunset without a line of client code changing.

How bright: one draw per tick, and no partial ticks at all

Lightmap is a 16×16 GpuTexture plus a MappableRingBuffer of uniforms. Lightmap.render writes those uniforms and issues one three-vertex draw with RenderPipelines.LIGHTMAP: the brightness curve lives in the shader and the whole texture is a by-product of it.

What it draws from is LightmapRenderState: ten values in std140 order — six floats from LightmapRenderState.skyFactor and LightmapRenderState.blockFactor to LightmapRenderState.brightness, then four colours, LightmapRenderState.blockLightTint and LightmapRenderState.skyLightColor from EnvironmentAttributes.BLOCK_LIGHT_TINT and EnvironmentAttributes.SKY_LIGHT_COLOR, the other two from EnvironmentAttributes.AMBIENT_LIGHT_COLOR and EnvironmentAttributes.NIGHT_VISION_COLOR — and an eleventh field, LightmapRenderState.needsUpdate, which is not a uniform at all but the flag that decides whether the draw happens. LightmapRenderStateExtractor.tick runs the torch-flicker random walk in LightmapRenderStateExtractor.blockLightFlicker and raises its own copy of the flag; LightmapRenderStateExtractor.extract copies it across, clears it, and reads the probe alongside Options.gamma, Options.darknessEffectScale, the conduit-power water vision and LightmapRenderStateExtractor.calculateDarknessScale. So the lightmap is recomputed once per tick and not once per frame — and, deliberately, GameRenderer.extract hands the extractor a partial tick of exactly one while FogRenderer.setupFog and SkyRenderer.extractRenderState get the real one. Sky and fog interpolate mid-tick. World lighting steps.

Three leftovers. Lightmap.getBrightness survives but no longer feeds the lightmap: it is a CPU-side version of the shader’s curve that also mixes in the dimension’s ambient light, kept for Hud, EntityRenderer’s shadow sampling and LevelExtractor’s underwater overlay alone. The packing statics moved out of the texture into LightCoordsUtil, from where a packed value reaches a vertex through VertexConsumer.setLight. And there are two lightmaps, not one: GameRenderer.levelLightmap always returns the real 16×16 texture, UiLightmap is a 1×1 white DynamicTexture, and GameRenderer.lightmap hands out the second for exactly as long as GameRenderer.useUiLightmap is set — which is the GUI block and nothing else, and which is why the HUD is not shaded by the light you are standing in.

Two curves that look like one

EnvironmentAttributes.SKY_LIGHT_FACTOR is a visual attribute, spatially interpolated, and the lightmap reads it; EnvironmentAttributes.SKY_LIGHT_LEVEL is a gameplay attribute, not positional, and Level.updateSkyBrightness turns it into Level.skyDarken, which gameplay reads, from mob spawning to whether it is bright outside, and which reaches the screen through the three CPU-side brightness readers above. Timelines.OVERWORLD_DAY keyframes both, at slightly different times and to different night values — so they look like one number, and a data pack can pull them apart.

How far: one block for the whole frame, filled by a priority walk

FogRenderer‘s output is a mutable FogData: FogData.color plus six distances — a start and an end each for the medium and the horizon, then FogData.skyEnd and FogData.cloudEnd. In open air the colour starts from EnvironmentAttributes.FOG_COLOR and leans toward EnvironmentAttributes.SUNRISE_SUNSET_COLOR when you face the sun and toward a weather-darkened EnvironmentAttributes.SKY_COLOR, and the medium’s and the sky’s and clouds’ distances come from EnvironmentAttributes.FOG_START_DISTANCE, EnvironmentAttributes.FOG_END_DISTANCE, EnvironmentAttributes.SKY_FOG_END_DISTANCE and EnvironmentAttributes.CLOUD_FOG_END_DISTANCE, shifted by rain and clamped by the render and cloud distances, the render distance alone setting the horizon pair in every medium; underwater they are EnvironmentAttributes.WATER_FOG_COLOR, EnvironmentAttributes.WATER_FOG_START_DISTANCE and EnvironmentAttributes.WATER_FOG_END_DISTANCE instead. It owns one ring buffer, FogRenderer.regularBuffer, and beside it a second buffer of its own, FogRenderer.emptyBuffer, filled with infinitely far for when fog is off.

There is one fog UBO for the whole frame, not one per pass. LevelRenderer.render takes a single slice and hands the same one to the sky and main passes, and to nothing else: the clouds, the weather and the always-on-top features draw inside the main pass, and the clouds read a cloud fog end out of the same buffer anyway, because the binding is sticky and each of them binds whatever was last set. The sky and cloud fog ends are separate fields inside that one block, which the shaders choose between, so what a player sees as per-element fog is a shader decision and not a binding.

The list that decides the colour

The colour and the darkening come from different places. FogRenderer.FOG_ENVIRONMENTS is an ordered list and the order is the priority: LavaFogEnvironment, PowderedSnowFogEnvironment, BlindnessFogEnvironment, DarknessFogEnvironment, WaterFogEnvironment, and AtmosphericFogEnvironment last, which answers only for open air. FogRenderer.computeFogColor makes one pass down that list carrying two independent latches — it takes the colour from the first environment whose FogEnvironment.providesColor is true and the darkness from the first whose FogEnvironment.modifiesDarkness is, which need not be the same one — whereas FogEnvironment.setupFog stops at the first applicable one, and it and FogEnvironment.isApplicable are the class’s only abstract methods. MobEffectFogEnvironment declares FogEnvironment.providesColor false on purpose: blindness and darkness may darken somebody else’s colour, never supply one, and each medium has exactly one environment that does. Which medium the camera is in is a FogType (FogType.WATER, FogType.LAVA, FogType.POWDER_SNOW, FogType.ATMOSPHERIC, FogType.NONE), and NONE maps to the atmospheric environment. Rain fog is the only stateful one: AtmosphericFogEnvironment.rainFogMultiplier is an exponential follower, so the murk lags a storm starting rather than snapping to it, and AtmosphericFogEnvironment.updateRainFogState thickens it even in a biome with no precipitation, at half strength.

What is up there: two skies, and a texture that is never bound

SkyRenderer builds every buffer it will ever need in its constructor, from SkyRenderer.buildStars to SkyRenderer.buildMoonPhases against the AtlasIds.CELESTIALS atlas, and per frame fills a SkyRenderState running from SkyRenderState.skybox through EnvironmentAttributes.SUN_ANGLE, EnvironmentAttributes.MOON_ANGLE, EnvironmentAttributes.STAR_ANGLE and EnvironmentAttributes.STAR_BRIGHTNESS to SkyRenderState.endFlashIntensity.

The stars are the same in every world. SkyRenderer.buildStars seeds a fixed constant and rejects samples outside a shell, so SkyRenderer.STAR_COUNT is an attempt count rather than a star count, and they are rebuilt only when a resource reload takes the whole renderer down: LevelExtractor.onResourceManagerReload sets LevelExtractor.shouldResetSkyRenderer and LevelRenderer.addSkyPass closes and reconstructs the entire SkyRenderer, stars, moon phases and all. The moon phase, likewise, is no longer arithmetic on the day count — it is EnvironmentAttributes.MOON_PHASE driven by Timelines.MOON, whose period is MoonPhase.COUNT days, and the renderer picks a sub-quad of an eight-quad buffer by MoonPhase.index.

The End takes a different branch entirely. With DimensionType.Skybox.END, SkyRenderer.extractRenderState fills only the End-flash fields: the sun angle, the moon phase, the sky colour and the dark disc are never sampled. And EndFlashState is not the dragon fight — it is a flash on a six-hundred-tick cycle of the dimension’s clock, seeded per interval for its offset, duration and angles, advanced by EndFlashState.tick in any dimension whose skybox is the End’s. The sky is also skipped five ways, four of them in one method: LevelRenderer.addSkyPass bails in lava, in powder snow, when CameraRenderState reports that a mob effect blocks the sky — which is blindness and darkness folded into one boolean before the method is entered — and when DimensionType.Skybox is NONE, which is the Nether. The fifth is outside it: GameRenderer.renderLevel suppresses the sky when a boss bar wants world fog, with AtmosphericFogEnvironment.setupFog clamping the fog hard in that case.

The clouds, which are never handed a fog slice or their texture

The clouds are the first of the two exceptions: their colour and height are EnvironmentAttributes.CLOUD_COLOR and EnvironmentAttributes.CLOUD_HEIGHT, but their drift is the level’s raw game time. And the cloud texture is never bound as a texture. The reload’s CloudRenderer.prepare does the whole job on a worker — reading the image and baking it into CloudRenderer.TextureData through CloudRenderer.packCellData, one 64-bit word per pixel with the colour in the high bits and four neighbour-emptiness flags in the low four — and CloudRenderer.apply is two statements on the Render thread that install the result and raise the rebuild flag. CloudRenderer.buildMesh walks cells of CloudRenderer.CELL_SIZE_IN_BLOCKS, writing three bytes per face through CloudRenderer.encodeFace — a compressed face list, expanded to quads in the shader, with CloudRenderer.RelativeCameraPos and CloudStatus deciding which faces exist. The per-frame CloudRenderer.prepare, at the top of the main pass, rebuilds it after a reload or a full re-mesh of the world, when the camera’s cell of the drifting cloud grid or its side of the layer changes, or when the CloudStatus changes — and a data pack setting the cloud colour to zero alpha skips the clouds entirely.

What is coming down: rebuilt every frame, and seeded from the ground

WeatherEffectRenderer is the second exception, and the one that asks least of anybody. Each column’s randomness is seeded from a hash of its own x and z — so the same column of rain keeps its speed and its offsets every frame it exists, and two clients standing in the same storm see the same drops in the same places without a byte crossing between them. Game time enters only afterwards, to scroll the streaks down the quad, and snow’s sideways too. It holds one WeatherEffectRenderer.vertexBuffer and the precomputed tangent tables WeatherEffectRenderer.columnSizeX and WeatherEffectRenderer.columnSizeZ, and its per-frame product is two lists of WeatherEffectRenderer.ColumnInstance records, rain and snow, inside a WeatherRenderState. WeatherEffectRenderer.extractRenderState returns immediately when the rain level is zero, so a clear sky costs nothing. Otherwise it loops every column in a square of radius Options.weatherRadius, querying the heightmap and the precipitation at each — every frame, on the CPU. The vertex buffer is rebuilt in WeatherEffectRenderer.prepare, at the top of the main pass, rather than in extract, and rain and snow are two indexed draws sharing it. WorldBorderRenderer rides in the same pass and is nothing to do with the weather: the main pass simply draws the two of them back to back after the translucent terrain — a single time, or once per stage with improved transparency — and the border’s WorldBorderRenderer.prepare is handed the render distance and the far plane, which bound the wall’s width and its height. What the border is belongs to level data and rules. Particles and sound are somebody else’s job: ClientLevel.tickWeatherEffects spawns those per tick within the same radius, next to ClientLevel.animateTick, whose scatter of EnvironmentAttributes.AMBIENT_PARTICLES belongs to particles.

What is not an attribute

The migration was not total, which is why everything is an attribute now needs a qualifier. DimensionType.ambientLight and DimensionType.cardinalLightType are plain record fields, read directly — and the first of the two no longer reaches the lightmap at all: its two readers are Lightmap.getBrightness, the CPU-side version this page has already said the shader does not use, and one deprecated method on LevelReader.

Directional shading, which is per dimension and is not data

The second of those is the one you can see. How bright a face is by which way it points comes from a CardinalLighting record, and there are exactly two in the game: CardinalLighting.DEFAULT and CardinalLighting.NETHER, both hard-coded. DimensionType carries the choice between them and nothing else — a data pack picks, it does not supply numbers. The choice is read in two places: ClientLevel.cardinalLighting hands the record to the face shading of the mesher and of moving blocks, and Lighting, a single UBO of two diffuse light directions sliced five ways, one slice per Lighting.Entry, picks the directions it writes by the same choice. Four of the five — Lighting.Entry.ITEMS_FLAT, .ITEMS_3D, .ENTITY_IN_UI and .PLAYER_SKIN, which is why a flat item in a slot, a side-lit one and the player in the inventory screen are each lit differently — are written once in the constructor and never again. Only Lighting.Entry.LEVEL is rewritten, by Lighting.updateLevel, each time the game renderer is given a level.

Block tint never moved at all: grass, foliage and water are still BiomeColors reading BiomeSpecialEffects through the four ColorResolvers, with no probe and no layer stack in it. And the clouds still read the level’s game time, because a value sampled at the camera and lerped by partial tick is the wrong shape for a drift — as does the weather, whose streaks scroll off it, though the weather is the one renderer here that asks for no attribute at all and seeds each column from its own coordinates.

For a 1.21-era reader. LightTexture is now Lightmap, with its packing statics in LightCoordsUtil. LevelRenderer.addCloudsPass and LevelRenderer.addWeatherPass are gone: the clouds and the weather are draws inside LevelRenderer.addMainPass, and only the sky keeps a pass of its own, LevelRenderer.addSkyPass (visibility and the frame graph).

Where to look

LightmapRenderStateExtractor.extract first, then Lightmap.render for what it feeds. EnvironmentAttributeProbe.getValue for the question every renderer here asks, and environment attributes and timelines for how it is answered. FogRenderer.computeFogColor for the priority walk. SkyRenderer.extractRenderState and LevelRenderer.addSkyPass for the sky and its two branches, CloudRenderer.buildMesh and WeatherEffectRenderer.prepare for the meshes rebuilt inside the frame, and BiomeColors for the colour system that did not move.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Particles

Verified against Minecraft 26.3 · Part XI · A player breaks a block, and the puff of block texture appears on every screen within sixty-four blocks.

The block goes. On the breaker’s own machine the puff is already there, predicted, before the server has heard the dig finish; on every other machine within sixty-four blocks it arrives a moment later as a level event and lands as the same sixty-four textured quads, built by the same method from the same shape. Two entirely different routes, one visual result — and the interesting thing is what neither route does. Neither asks, on your machine, how far away you are. Neither asks whether the packet was worth sending. And neither asks your particle setting, which three pieces of the client read three different ways, while the server is told what you chose and never once acts on it. A particle is not something the game decides to show you. It is something that survives a series of gates that disagree about what they are gating.

The cast

classwhat it decidesthread
ServerLevelwhich players are told about a particle at all — on dimension and distance, and for a level event who caused itServer thread
ParticleTypethe type’s identity in the registry, and ParticleType.getOverrideLimiter, the “ignore the limits” flag baked into iteither
ClientLevelthe gated entry point, ClientLevel.doAddParticle — and the ungated ones beside itRender thread
ParticleResourceswhich provider a type gets, registered once at construction, and which SpriteSet — rebound on every reloadload off-thread, bind on the Render thread
ParticleProviderone per registered type: what class of Particle an options record turns intoRender thread
ParticleEnginethe groups, the one-tick admission queue, the emitters, the per-limit countsRender thread
ParticleGroupwhether there is room: the per-render-type cap and the probabilistic reservoirRender thread
ClientExplosionTrackerhow many explosion particles happen this tick, and where — the client’s own budgeted generatorRender thread
SingleQuadParticle.Layerwhich of three atlases a quad reads, and which pipelines draw itRender thread

Everything below the second row runs on the Render thread, and the only off-thread work in the system is the load half of ParticleResources.reload — the bind that rebuilds each SpriteSet comes back to the Render thread. Several things cross the network and none of them is a particle. Three carry the bulk of it: a ClientboundLevelParticlesPacket is an explicit request carrying count, spread, a speed on each axis, a randomization type and two override flags, a ClientboundLevelEventPacket is an event the client interprets, and a ClientboundExplodePacket is a description the client expands itself.

Does the particle happen at all?

Both routes start in the same place. At the end of a dig that succeeded, Block.spawnDestroyByEntityParticles raises level event LevelEvent.PARTICLES_AND_SOUND_DESTROY_BLOCK with the breaker as the source, and the two branches diverge only because of who that source is relative to whoever is watching. That the wire carries an int rather than a particle at all — and what else comes through the same door — is what makes a sound happen, which owns LevelEventHandler; this page owns what the client does with the puff once the switch has decided.

flowchart TD
    subgraph BC["the breaker's client"]
        MPGM["MultiPlayerGameMode.destroyBlock, predicting"] --> PWD1["Block.playerWillDestroy, which calls Block.spawnDestroyByEntityParticles"]
    end
    subgraph SV["the server"]
        SPGM["ServerPlayerGameMode.destroyBlock, once the dig succeeds"] --> PWD2["the same, on the server's level"]
        PWD2 --> SL["ServerLevel.levelEvent, the breaker as source"]
        SL --> PL["PlayerList.broadcast: within 64 blocks, not the breaker"]
    end
    subgraph OC["each other client in range"]
        CPL["ClientPacketListener.handleLevelEvent"]
    end
    MPGM -- "the dig reaches the server" --> SPGM
    PL -- "ClientboundLevelEventPacket" --> CPL
    PWD1 --> CL["ClientLevel.levelEvent: LevelEventHandler, the sound, then ClientLevel.addDestroyBlockEffect"]
    CPL --> CL
    CL -- "one TerrainParticle per cell" --> PE["ParticleEngine.add, never through ClientLevel.addParticle"]

One break puff by two routes: the breaker’s client raises the event itself, and the server sends it to everyone else in range. Both routes end in the same client code, which hands finished particles straight to the engine.

Neither route is gated. Both end in ClientLevel.addDestroyBlockEffect, which calls ParticleEngine.add directly and never passes through ClientLevel.addParticle — so neither the distance check nor the particle setting applies to either of them. The branches differ in who dispatches the event, not in what the client then does with it.

And the breaker is not always a player. LevelEvent.PARTICLES_AND_SOUND_DESTROY_BLOCK has fourteen call sites and only three pass a source at all — a bed, a tall plant, and Block.spawnDestroyByEntityParticles, which passes on the player Block.playerWillDestroy hands it. A fox faceplanting into snow, a rabbit eating a carrot down one age, a sheep eating grass, a zombie breaking a door, a suspicious block that fell and shattered, and Level.destroyBlock itself, through Block.spawnDestroyParticles, all raise it with a null source — which means the server broadcasts it to everybody, including whoever caused it.

Sixty-four quads make a full cube’s puff, because ClientLevel.addDestroyBlockEffect walks every box of BlockBehaviour.BlockStateBase.getShape on a fixed quarter-block grid with a minimum of two cells per axis.

That is the outline shape, not the collision shape, and the difference is visible: a torch has no collision shape at all and still gives twelve particles, where reading the collision shape would give none. Each particle shows a different randomly-offset quarter-crop of the block’s sprite, which is why the puff does not look tiled. A block may opt out of the whole thing — BlockBehaviour.BlockStateBase.shouldSpawnTerrainParticles gates both the destroy and the crack particles — and the providers that build a TerrainParticle for other callers additionally refuse Blocks.MOVING_PISTON.

Three neighbours that look like the same thing

The crack particles that fly off while you are still mining come from ClientLevel.addBreakingBlockEffects, and the server drives them: ServerPlayerGameMode.tick raises LevelEvent.PARTICLES_DESTROY_PROGRESS once per server tick of the dig — LevelEvent.PARTICLES_AND_SOUND_DESTROY_PROGRESS every fourth, which adds the hit sound — with a null source, so every screen within sixty-four blocks, yours included, adds one crack particle per tick of your dig. Your own client adds one more itself, with the hit sound, when the dig starts, from MultiPlayerGameMode.startDestroyBlock. The /particle command arrives as ClientboundLevelParticlesPacket and goes through ClientLevel.addParticle once per requested count with Gaussian spread, unless the requested count is zero, which is a second mode entirely: one particle whose velocity is the offset vector scaled by the speed, which is how a directed particle is spawned. And the ambient scatter is a fixed cost paid every tick regardless of what is there — ClientLevel.animateTick samples 1,334 random block positions, 667 within sixteen blocks and 667 within thirty-two, most of which do nothing. Riding on that one loop are the drip particles (ClientLevel.trySpawnDripParticles), the biome’s AmbientParticle list named by EnvironmentAttributes.AMBIENT_PARTICLES (environment attributes and timelines), and the barrier and light block markers — in creative, holding one of those two items makes every matching block in range emit a marker particle, which is the only reason you can see them.

Who is allowed to see it?

There are three distance rules on the two routes this section follows, the particle packet and the level event, enforced by three different pieces of code, and two of them happen to be the same number; an explosion’s packet has a 64-block rule of its own, and an entity’s own packets (a crit, an item pickup) reach whoever is tracking it. The table’s fourth row is the absence that makes the page’s hook true: once a finished particle is handed to the engine, the client applies no distance rule at all.

the gatemeasured fromthe distancewhat an override does to it
the server choosing whom to send a ClientboundLevelParticlesPacket tothe receiving player32 blockswidens it, to 512 — but from the caller’s own boolean, in practice /particle … force and the creaking heart’s, and never from the particle type
the client deciding whether to build the particle at all, in ClientLevel.doAddParticlethe camera32 blocksskips the check entirely
the server broadcasting a level event — the break puff’s second routethe receiving player64 blocksnot consulted: a level event carries no particle type
ClientLevel.addDestroyBlockEffect, and everything else that hands ParticleEngine.add a finished particle—nonenothing to override

The two thirty-twos are independent, not one check written twice. A particle that clears the server’s test can still be dropped by the client’s, because the client measures from where you are looking rather than from where your feet are, and a packet that took a tick to arrive is measured against a camera that has since moved. And the two overrides are not one flag, though one feeds the other: the client’s comes off the particle type, through ParticleType.getOverrideLimiter, whose three readers are all client-side, while the server’s is a boolean the caller passes in, which the packet then carries down as an override of its own. On the client an override deletes the distance check and the setting check outright; on the server it multiplies a different check by sixteen.

The 64-block radius is the third rule, and nothing overrides it in either direction. Once the puff’s level event lands the client asks no further questions, which is why a break puff at the edge of view is unconditional where the same particle requested by /particle would never have been sent.

Does the setting apply?

Three pieces of the client read the same particle setting, and none of them agrees with the others about what its values mean.

who reads itwhat it does
ClientLevel.doAddParticlean override creates the particle whatever the setting; otherwise decreased is rewritten to minimal about a third of the time, and minimal drops everything — except that the always-show flag rescues a minimal setting one time in ten, and the rescue lands on decreased, which is then re-rolled
ClientExplosionTrackeranything below All is off. The pending explosions are cleared unused, and there is no decreased tier for explosion block particles at all
ClientLevel.tickWeatherEffectson decreased, halves its column count — rain thins rather than stopping; on minimal it breaks out before adding anything, and there it does stop

Only the first of those is the gate everything is nominally supposed to go through, and four call sites outside the particle package bypass it entirely by handing a constructed particle straight to ParticleEngine.add: the break puff, the crack effect, the firework starter and the item-pickup streak. A fifth is inside the system, a firework spark spawning more sparks. The same puff arriving as a particle packet would be distance-culled and might be diced away by the setting. Arriving as a level event, it is unconditional.

The last piece reads like a bug and is not. The server knows your particle setting and never uses it. It arrives in the client information and is stored on the player, and the broadcasts filter on dimension, distance and, for a level event, who caused it, never on the setting — so turning particles down saves your GPU and costs the server exactly nothing.

Is there room for it?

Explosions budget themselves before they ask anyone else, and they are not a particle packet at all. What ServerLevel.explode sends is a description: a ClientboundExplodePacket of a radius, a block count and a WeightedList of ExplosionParticleInfo, which ClientPacketListener.handleExplosion hands to ClientExplosionTracker to expand locally. Each tick it totals the block counts of every explosion it holds, caps the total at ClientExplosionTracker.MAX_PARTICLES_PER_TICK, and draws that many weighted samples, each a random direction at a cube-root-distributed radius so the samples fill the volume evenly rather than crowding the centre, and each rejected outright if the block there is not air. Then the whole list is cleared, spent or not — which is the budget: an explosion gets one tick’s worth of particles and no second chance.

Everything that reaches ParticleEngine.add, an explosion’s samples included, then meets the engine’s own two limits.

flowchart TD
    A["a particle reaches ParticleEngine.add"] --> B{"a ParticleLimit?"}
    B -- "none, nearly all" --> Q
    B -- "ParticleLimit.SPORE_BLOSSOM, full" --> X1["dropped, never queued"]
    B -- "under it, counted" --> Q["queued in ParticleEngine.particlesToAdd"]
    Q --> D{"next ParticleEngine.tick: its group's room"}
    D -- "at ParticleGroup.MAX_PARTICLES" --> X2["refused; a counted one's count given back"]
    D -- "past ParticleGroup.RESERVOIR_START" --> E{"kept by chance?"}
    D -- "below ParticleGroup.RESERVOIR_START" --> K["kept"]
    E -- "yes" --> K
    E -- "no" --> X2
    K --> T["ticked from the next tick onward"]

The engine’s two limits in the order a particle meets them: the per-limit count on arrival, and its render type’s group at the next tick. Only a particle carrying ParticleLimit.SPORE_BLOSSOM has a count to meet, and one the group refuses gives its count back.

There are four render types in the game: ParticleRenderType.SINGLE_QUADS, which is almost everything, ParticleRenderType.ITEM_PICKUP, ParticleRenderType.ELDER_GUARDIANS and ParticleRenderType.NO_RENDER. One ParticleGroup exists per type. The cap, ParticleGroup.MAX_PARTICLES, is per render type rather than global, and its last quarter is probabilistic: past ParticleGroup.RESERVOIR_START the acceptance probability falls as the square of the free fraction, so the last few hundred slots are very hard to fill and a particle storm degrades gradually rather than hitting a wall. Since almost everything is a single-quad particle, that one group’s budget is effectively the whole budget; the other three have their own.

The per-limit machinery beside it is the strangest thing in the system. ParticleLimit is a full accounting apparatus — a key carried by the particle, a count map in ParticleEngine.trackedParticleCounts, a decrement when a group refuses a particle the limit had already accepted — and it has exactly one instance, ParticleLimit.SPORE_BLOSSOM. The whole mechanism exists to hold down one ambient particle, the haze of spores a spore blossom scatters around itself, and ParticleEngine.hasSpaceInParticleLimit is the only thing that ever reads the map. The number on the debug screen is a different count: ParticleEngine.countParticles walks the live render-type groups, and DebugEntryParticleRenderStats is its only consumer.

When does it move, and when is it drawn?

Admission is deferred by up to a tick. ParticleEngine.add puts the particle in ParticleEngine.particlesToAdd, and ParticleEngine.tick — which runs from Minecraft.tick, right after the ambient scatter, and only while the game is unpaused and the level is running normally — does three things in a fixed order: tick every existing group, then tick the emitters, then drain the queue into groups. Because the drain is last, a particle never moves on the tick that admits it, and a particle created during rendering is invisible until a tick has run.

The emitters are the exception to almost everything. A TrackingEmitter is a NoRenderParticle bolted to a moving entity, spending its short life calling ClientLevel.addParticle on that entity’s behalf — crits, the enchanted-hit sparkles and the totem burst are all emitters. It lives in ParticleEngine.trackingEmitters rather than in a ParticleGroup, so it is never counted, never culled and never extracted. It is only ticked. So is a NoRenderParticleGroup: ParticleEngine.tick iterates the whole group map, but ParticleEngine.extract iterates ParticleEngine.RENDER_ORDER, which lists three of the four render types, so a no-render group ticks its contents forever and is never asked for a render state. Which is exactly what a no-render particle is for.

The eighty-odd subclasses, which are one shape

The eighty-odd Particle subclasses are a family, and the shape above is all of it: a provider for most of them, a lifetime, a per-tick move, and a group. The two largest files are DripParticle and FireworkParticles, the second a holder class rather than a subclass, and both are large for the same reason: each is a nest of providers and nested subclasses (every fluid that drips, every stage of a rocket) behind one class name. What varies between any two of them is which ParticleOptions record the registry hands the provider: ParticleType is a data-driven registry entry like any other, and ParticleOptions is the argument its codec parses — usually nothing at all, sometimes a block state, an item stack, a colour or a target position.

Extract, where the interpolation and the culling both live

Everything visible happens at extract time, once per frame, from LevelExtractor. That is where the particle’s previous and current positions are lerped by the partial tick and made camera-relative before being packed — interpolation is not a property of the particle, it is a property of the extract. It is also where the culling happens, and the cull is a point test: the particle’s centre, not its quad, against a Frustum whose origin has been slid a few blocks behind the camera so that particles just past the near plane survive. Two of the other three groups take a Frustum and ignore it, and the no-render group is never asked. Only QuadParticleGroup culls.

What survives is packed into ParticlesRenderState, one ParticleGroupRenderState per group, with QuadParticleRenderState writing twelve floats and two integers per particle into a per-layer QuadParticleRenderState.Storage — a growable pair of arrays, one of floats and one of integers, reset and reused each frame rather than reallocated — and QuadParticleFeatureRenderer turning that into draws through Blaze3D. The layer decides which atlas is bound, and the particle system draws from three of them, not one:

the sprite lives onopaquetranslucent
the particle atlasSingleQuadParticle.Layer.OPAQUESingleQuadParticle.Layer.TRANSLUCENT
the block atlasSingleQuadParticle.Layer.OPAQUE_TERRAINSingleQuadParticle.Layer.TRANSLUCENT_TERRAIN
the item atlasSingleQuadParticle.Layer.OPAQUE_ITEMSSingleQuadParticle.Layer.TRANSLUCENT_ITEMS

Without improved transparency the six resolve to two pipelines, RenderPipelines.OPAQUE_PARTICLE and RenderPipelines.TRANSLUCENT_PARTICLE. SingleQuadParticle.Layer.bySprite picks a row and a column by reading whether the stitched sprite contains translucent texels and which atlas the sprite lives on — and only three particle classes ever ask it: TerrainParticle, BlockMarker and BreakingItemParticle. Every other quad particle hard-codes opaque or translucent on the particle atlas, so the four terrain and item layers exist solely for block- and item-textured particles (models and atlases).

The last surprise is in the submission. A quad particle group is submitted twice per frame — once into the solid bucket and once into the after-terrain bucket, the same render state object entered twice, with the feature renderer filtering each entry by whether the layer is translucent. Opaque particles therefore draw before terrain-translucent geometry and translucent ones after, and the per-particle packing still happens only once. The feature renderer owns no render target and draws into the pass it is handed; under improved transparency the after-terrain bucket is the order-independent one, and the three translucent layers draw once per OitStage through RenderPipelines.OIT_PARTICLE (visibility and the frame graph). Two particles’ draws escape this system: ItemPickupParticle carries an EntityRenderState and is submitted through EntityRenderDispatcher, so the item flying into your inventory is a rendered entity wearing a particle’s lifetime, and ElderGuardianParticle is submitted as a model.

What empties the engine, and what it crashes on

Two events empty the engine wholesale, and both have to. ParticleEngine.clearParticles runs on a resource reload, because a quad particle holds a sprite reference into an atlas that no longer exists, and ParticleEngine.setLevel runs the same clear, emitters and all. Particles are crash-report sites by design, though none of the reports is raised on ParticleEngine: they come from ParticleGroup.tickParticle, from QuadParticleGroup.extractRenderState, and from ClientLevel.doAddParticle for a provider that throws while constructing. The one malformed particle that does not crash is one arriving in a particle packet — ClientPacketListener.handleParticleEvent logs it and drops it, with the rest of that packet’s count. And Particle.move skips the collision sweep above a fixed speed, so a particle thrown hard enough passes through the world for as long as it moves that fast, while one that has been stopped by a collision once stays flagged as stopped.

For a 1.21-era reader. Particle.getLightColor is now Particle.getLightCoords and ParticleFeatureRenderer is QuadParticleFeatureRenderer. SingleQuadParticle.Layer.TERRAIN and ITEMS each split into an opaque and a translucent layer, which SingleQuadParticle.Layer.bySprite picks between, and a full ParticleGroup now refuses a newcomer where it used to evict its oldest. ParticleResources, ParticleRenderType and ParticleLimit kept their names.

Where to look

ClientLevel.doAddParticle first — it is the gate the page is about, and reading it tells you at once how little goes through it. Then ClientLevel.addDestroyBlockEffect for the route that does not, and ClientLevel.animateTick for the loop that pays for the ambient world. ParticleEngine.add and ParticleEngine.tick for admission and the order the three things in a tick happen in, ParticleGroup for the cap and the reservoir, ParticleResources.registerProviders for the catalogue of types, and ParticleEngine.extract for where interpolation and culling actually live. SingleQuadParticle.Layer.bySprite is the small surprise worth reading on its own — a layer chosen by looking at a sprite’s pixels.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Post-processing

Verified against Minecraft 26.3 · Part XI · you press Escape and the world goes soft behind the menu — and the machine that softened it is the one that turns everything green when you spectate a creeper.

The blur behind the pause menu is not a GUI effect. It is a post-processing chain: a file called blur.json, sitting in the jar beside creeper.json, in the same format, loaded by the same loader, compiled into the same kind of object and run by the same two classes, PostChain and PostPass. Five such files ship, covering the pause menu, the three things it is unpleasant to spectate and the glow around a spectral-arrowed mob. A resource pack can rewrite every one of the five, with as many passes as it likes, running fragment programs it also ships — a real and underused amount of rope. It can also add a sixth: the list of chains run over the world every frame starts with end_of_frame, and no file in the jar answers to that name.

The cast

classwhat it decidesthread
PostChainConfigwhat a chain is as data: its own targets, and its passes in orderparsed on a worker
ShaderManagerwhich chains exist, when they are compiled, and when they are thrown awayconfigs read and pipelines compiled on workers (a chain’s while the Render thread waits), the rest on the Render thread
PostChainwhich targets a pass may name, and where a pass’s output livesRender thread
PostPassone draw: a pipeline, its inputs as samplers, its uniforms as buffersRender thread
UniformValuethe seven types a JSON-declared uniform may have, and how each is packeddecoded on a worker, packed on the Render thread
LevelTargetBundlethe two names the level’s targets answer to, and which set a chain may ask forRender thread
LevelRendererthe one chain that becomes passes in the world’s own frame graphRender thread
GameRendererthe blur and the per-frame list of chains, each given a frame graph of its own, built and thrown away on the spotRender thread

Nothing here talks to a driver directly: a pass is a RenderPipeline and a RenderPass like every other draw in the game, and the vocabulary this page spends — BindGroupLayout, GpuBuffer, MappableRingBuffer, Std140Builder, TextureTarget — is taught on the Blaze3D page. The graph the passes go into belongs to visibility and the frame graph.

From a file on disk to a pass in a graph

flowchart TD
    subgraph RELOAD["once per resource reload"]
        DISK["a JSON file under post_effect, and the GLSL it names"]
        PREP["ShaderManager.loadConfigs, on a worker: one PostChainConfig each"]
        APPLY["the apply half: every cached chain closed, a new empty cache"]
    end
    subgraph FRAME["inside a frame"]
        GET["ShaderManager.getPostChain, on the Render thread"]
        ASK{"cached already?"}
        LOAD["PostChain.load: one PostPass per pass, each pipeline compiled now"]
        CACHE["cached by id, and only by id"]
        ADD["PostChain.addToFrame: the targets resolved"]
        PASS["PostPass.addToFrame: one FramePass per pass"]
        EXEC["FrameGraphBuilder.execute: three vertices a pass"]
        OUT["the last pass writes an imported target"]
    end
    DISK --> PREP --> APPLY
    APPLY -- "a later frame asks" --> GET --> ASK
    ASK -- "no, the first ask" --> LOAD --> CACHE --> ADD
    ASK -- "yes" --> ADD
    ADD --> PASS --> EXEC --> OUT

A chain’s life in its two phases: the reload parses every file and empties the cache, and the first frame to ask for a chain compiles and caches it. Every later ask is answered from the cache.

The chain splits across two phases of the client’s life, and the split is the thing to hold: the top box happens once, during a resource reload, and the bottom one inside a frame, every frame the effect is on — all but the compile and the cache entry, which happen only on the first ask after a reload. That first ask is inside a frame, which is what makes the compile the awkward step.

What a chain declares, and the two kinds of name in it

A PostChainConfig is two things: a map of targets it wants for itself, and a list of passes in the order they run. That is the entire schema.

Each pass names a vertex program and a fragment program by Identifier, an output target, a list of inputs and a map of uniform blocks. An input is one of exactly two shapes: PostChainConfig.TargetInput names another target and may ask for its depth attachment rather than its colour, and PostChainConfig.TextureInput names a PNG under textures/effect with its dimensions. Both carry a sampler name, and two inputs on one pass sharing one is rejected by the codec while the file is being parsed, so the chain never reaches the config map at all rather than failing later when something asks for it. That name is the contract with the GLSL: PostChain appends Sampler to it when it builds the pass’s BindGroupLayout, so an input called In is the shader’s InSampler.

The distinction that matters is between the two kinds of target name. A name in the chain’s own targets map is internal: it belongs to the chain, is created fresh inside the frame graph at screen size unless the chain overrides that, and is gone when the graph finishes. Any other name is external and must be supplied by whoever runs the chain. PostChainConfig.Pass.referencedTargets collects both kinds, PostChain.load subtracts the internal ones, and what remains must be a subset of the allowed set the caller passed in — LevelTargetBundle.MAIN_TARGETS or LevelTargetBundle.OUTLINE_TARGETS — one name or two, each set being the main target plus whatever else that caller is prepared to hand over. A chain naming a target its caller did not offer does not load at all. An internal target may also be declared persistent, in which case PostChain allocates it itself, again whenever its size changes, keeps it in PostChain.persistentTargets until the chain drops out of the frame’s list, and imports it rather than creating it, so a pass can read what it wrote last frame. None of the five asks for one.

Loaded off-thread, compiled inside a frame

ShaderManager is a PreparableReloadListener that splits its own work across the two places a reload listener has. ShaderManager.loadConfigs runs on the reload’s worker executor: it reads every GLSL source and include under shaders, and parses every JSON under post_effect with PostChainConfig.CODEC into an immutable map — a malformed chain is logged and simply absent from it. Still before the barrier, ShaderManager precompiles the static pipeline catalogue through GpuDevice.compilePipeline, whose compiler resolves each source’s #include directives against what was just read. ShaderManager.apply then runs on the Render thread and installs the compiled set as a new PipelineCache.

Post chains are not in that precompiled set. They are built lazily, the first time somebody asks: ShaderManager.getPostChain consults the cache, and on a miss PostChain.load walks the config, builds a RenderPipeline per pass from RenderPipelines.POST_PROCESSING_SNIPPET, names it chain id slash pass index, and precompiles it there and then. The first frame you spectate a creeper compiles two shader programs in the middle of itself. A failure throws ShaderManager.CompilationException, which ShaderManager.getPostChain logs, caches as an absence until the next reload so the next frame does not try again, and, for the first such failure since that reload, reports to Minecraft.triggerResourcePackRecovery — the path that deselects every resource pack it can and reloads, or, when none can be deselected, crashes the game if vanilla is all that is selected and otherwise returns to the title screen. Closing the old cache, meanwhile, closes every PostChain in it, destroying its persistent targets and freeing each PostPass’s uniform buffers: a reload does not rebuild the chains, it forgets them.

A pass is three vertices, and its uniforms are written once, at load

PostPass.addToFrame adds one FrameGraphBuilder.addPass named after its pipeline’s location. Every target input becomes a FramePass.reads, the output a FramePass.readsAndWrites, and the body goes in through FramePass.executes — nothing is drawn while the graph is built.

When the body eventually runs, it sets an orthographic projection — ShaderManager keeps one Projection and one ProjectionMatrixBuffer and lends them to every chain — writes the output size and each input’s size into a MappableRingBuffer as the SamplerInfo block, then opens a RenderPass, binds pipeline, default uniforms, custom uniform blocks and inputs, and draws.

Three vertices make every post-processing draw, in all twenty-four passes the five chains declare (PostPass.addToFrame). There is no quad and no vertex buffer: both vertex programs the chains use build one oversized triangle out of the vertex index alone, and the fragment shader sees the whole screen.

The uniforms are stranger than they look. A UniformValue has seven types — int, ivec3, float, vec2, vec3, vec4 and a 4×4 matrix — and a block is a list of them under a name. PostPass’s constructor sizes the block with UniformValue.addSize, packs it with UniformValue.writeTo and uploads it to a GpuBuffer once, at load, never to be written again. The per-entry name in the JSON is read by no codec — only the type and the value are, and members match the GLSL block positionally. Only the block’s own name, the key in the uniforms map, has to match anything.

That is why the blur’s working radius is not one of them. blur.json declares a radius of zero, and box_blur treats anything under a half as “ask elsewhere”: it falls back to a member of the Globals block, which GlobalSettingsUniform.update rewrites every frame — from OptionsRenderState.menuBackgroundBlurriness, in this case — and RenderSystem.bindDefaultUniforms binds to every post pass. Anything a chain needs to vary per frame cannot be a chain uniform. It has to come in through a block the game writes itself — the global block, or the pass’s own SamplerInfo sizes, the only two a post pipeline declares beside its own — whose membership is fixed in Java.

The five chains

The last column is a reading of the shaders, not a citation: the GLSL says what it does to a pixel and does not say what that looks like, so those cells are the one place on this page where the book is describing rather than reporting.

chainwho declares itwhat it readswhat it looks like
blurGameRenderer.processBlurEffect, called from inside GuiRenderer.drawthe main target and one internal target, six passes alternating between themthe world going soft behind a pause or options screen
creeperGameRenderer.render, when the camera entity is a Creeperthe main target, and one internal target it bounces throughluminance collapsed into the green channel, then posterised and mosaicked
spiderGameRenderer.render, when it is a Spiderthe main target and four internal targetsthe view repeated through several skewed, blurred, red-tinted lobes
invertGameRenderer.render, when it is an Endermanthe main target, and one internal target it bounces throughcolours inverted, four fifths of the way
entity_outlineLevelRenderer.render, only on a frame where something submitted an outlinethe entity-outline target, and one internal target it bounces through — never the main onethe coloured halo around a glowing mob

Only one of those is a screen effect. blur runs over whatever is currently on the main target, world and GUI alike, because GuiRenderer.draw splits the GUI in two and runs this chain in the gap. Where that boundary falls, who asks for it and why a chest does not are questions for the GUI render tree; what this page owns is that the thing running in the gap is an ordinary post chain over the main target, with nothing about it that knows it is a menu.

Three are world effects: creeper, spider and invert run at the end of GameRenderer.render’s world block, after the level and before any GUI, so they warp the world and leave the HUD alone. They get there on a list GameRenderer.update rebuilds every frame — end_of_frame, then LocalPlayer.getActivePostEffects, then the spectator chain unless F4 has switched it off — which GameRenderer.render looks up and runs in that order. No file ships as end_of_frame: ShaderManager.isPostEffectValid logs the miss and the name is skipped until the next resource reload, so the slot is a resource pack’s. The middle of the list is the server’s: /posteffect (PostEffectCommand) edits a list ServerPlayer saves with the player and sends down in ClientboundPostEffectsPacket, and the client runs any chain on it that it has, so long as main is the only external target the chain names. The last row is neither kind, and is the next section’s.

The outline chain, end to end

Take the one whose whole life is visible. A mob is glowing, so something submits it to SubmitNodeCollection.outline, and FeatureRenderDispatcher.PreparedFrame.executeOutline draws it — flat, in the glow colour — into the entity-outline target during the main pass. That target is a TextureTarget LevelRenderer owns across frames.

sequenceDiagram
    participant LR as LevelRenderer
    participant ShadM as ShaderManager
    participant PChain as PostChain
    participant PPass as PostPass
    participant FGB as FrameGraphBuilder
    participant GR as GameRenderer

    LR->>FGB: importExternal — the entity outline target, for the main pass
    opt something submitted an outline this frame
        LR->>ShadM: getPostChain for entity_outline, allowing main and entity_outline
        ShadM-->>LR: the chain, compiled on its first ask
        LR->>PChain: addToFrame with the screen size and the level's target bundle
        PChain->>FGB: createInternal — the chain's own swap target, at screen size
        PChain->>PPass: addToFrame, four times, in declared order
        PPass->>FGB: addPass, declaring what it reads and what it writes
    end
    LR->>FGB: execute — with the chain added, sobel, blur, blur, blit back
    GR->>LR: blitEntityOutline, after the graph — the glow onto the main target

The outline chain across one frame: declared into the level’s graph only when something glows, run when the graph executes, and composited by a separate call after the graph has finished. The four passes bounce between the outline target and the chain’s own swap target, ending where they began.

The first pass is an edge detector and what it detects edges in is alpha, not colour: the target is transparent everywhere nothing was submitted, so the boundary of the silhouette is the boundary of the halo. The next two blur that outline across and then down, and the last blits it back where it started — the chain ends on the same external target its first pass read, and the internal swap target is what makes that legal, since no pass ever reads and writes one buffer at once. Then it stops, and the compositing is somebody else’s job. LevelRenderer.blitEntityOutline runs after GameRenderer.renderLevel returns, outside the graph entirely, and blends the glow onto the main target with a pipeline of its own. This is the one chain whose result is invisible until a separate blit puts it on screen.

Two doors into the GPU, and one of them is deprecated

PostChain has two entry points, and which one a chain goes through is the whole difference between the two halves of this page.

PostChain.addToFrame takes a FrameGraphBuilder somebody else already started and appends to it. That is what LevelRenderer does with the outline chain: it is not a second rendering path but four more passes in the graph it was building anyway, ordered by their declared reads and writes alongside the clear, the sky and the main pass. They survive that graph’s culling for the ordinary reason — their last pass writes a target imported from outside — and they appear in the profiler under their pipeline names, because the level’s graph is executed with an inspector that pushes a zone per pass.

PostChain.process is the other door, and it is marked deprecated. It builds a FrameGraphBuilder of its own, imports one target as main, adds the chain, executes it and throws it away. Both its callers are in GameRenderer: GameRenderer.applyPostEffects, which runs the frame’s list at the end of the world block, and GameRenderer.processBlurEffect in the middle of the GUI. Neither passes the inspector the level’s graph is executed with, so the blur and the spectator shaders never get a slice of the F3 pie chart to themselves: their cost is folded into whichever enclosing zone they ran under, render → world for the spectator effects and render → gui → draw for the blur, and no name in the chart tells you a post chain is what you are looking at. The graphs are throwaway; the memory the targets sit in is pooled either way.

Questions players ask

Why does the creeper effect vanish when I press F5? Because third person clears it, and the perspective key does it directly. GameRenderer.checkEntityPostEffect switches on the camera entity’s class and sets GameRenderer.spectatedEntityPostEffect to one of the three ids written as literals in its own branches, clearing it for anything else — including for no entity at all. The perspective key calls it straight out of Minecraft.handleKeybinds; Minecraft.setCameraEntity is the other door into the same method, for when what you are spectating changes rather than how. F4 (Options.keyToggleSpectatorShaderEffects) is a separate switch, flipping GameRenderer.spectatedEntityEffectActive without forgetting which chain was chosen; the F3 screen’s DebugEntryPostEffects lists only the chains the renderer applied, so a switched-off one drops out of it.

What happens to a chain when the window is resized? Nothing, for the five that ship. A PostChain without a persistent target has no size of its own: the dimensions arrive as arguments to PostChain.addToFrame every frame, and internal targets are described fresh from them each time. GameRenderer.resize clears the resource pool, freeing the old targets at once rather than three frames later, and LevelRenderer.resize resizes the entity-outline target it owns. The compiled pipelines never mention a resolution, so they are untouched.

For a 1.21-era reader. The transparency chain is gone, and LevelTargetBundle.SORTING_TARGETS with it: improved transparency is LevelRenderer.executeOit, drawn inside the level’s main pass, and GraphicsPreset.FABULOUS sets Options.improvedTransparency on every platform, macOS included. PostChain, PostPass, UniformValue and the deprecated PostChain.process kept their names.

Where to look

PostChainConfig first — the record is the file format. Then ShaderManager.loadConfigs and ShaderManager.getPostChain for where a chain comes from and how long it lives, PostChain.load for the validation that decides whether it loads at all, and PostChain.addToFrame for the only thing a chain does. PostPass is one pass and one draw. For the callers, LevelRenderer.render declares one chain into the world’s frame graph, GameRenderer.update lists the frame’s chains, GameRenderer.render and GameRenderer.processBlurEffect run the rest through the deprecated door, and LevelTargetBundle names what any may 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.

XII · World generation

Verified against Minecraft 26.3 · Part XII · The one system in the game built to be reproducible from a world’s settings and its data packs alone, where it frays, and the single deliberate exception to it.

What a player recognises the part by is the seam: the flat shelf of ground under a village that was not there before, the cave that is flooded the moment you break into it, the desert that becomes a jungle along a ragged line, the tree that grows up through another tree.

Everything this part generates is determined by the world’s saved settings — the seed, the dimension list and a few options such as whether structures generate — and the data packs enabled when the world is opened: WorldLoader.load re-reads the worldgen registries out of the current packs every time. No entity and no tick is among those inputs. Give the same settings and the same packs to two copies of the game and they will agree on every biome, every structure’s place and every block of terrain — which is the property speedrunners, seed-hunting sites and structure finders all depend on. It holds not because nothing here reads the world — the decoration step reads block states and heights through PlacementContext (features and placement), and the carvers read a whole neighbourhood of chunks for the biome that seeds each cave (terrain) — but because everything it reads is itself a function of those settings and those packs. Two edges fray. Decoration writes into its neighbours, and no rule orders a chunk’s decoration against a neighbour’s, so what a feature finds at a chunk border can depend on which of the two decorated first (what a feature may write); and a template a structure block saved into the world is tried before the packs’, so every later piece that names it, and the ground a beard shapes round that piece, follows the saved file (where a template comes from). And there is one deliberate exception, where generation reads chunks the current seed did not produce: chunks an older version generated.

Counting world/level/levelgen and world/level/biome together, the way the atlas counts everything else, that is 477 files and 49,303 lines — and two of them are the standing counter-example to the paragraph above. PatrolSpawner and PhantomSpawner sit in these packages, run on the server tick, and belong to Parts III and VI.

The shape of the part

Part XII is a substrate, a pipeline, and a wing. The substrate is the scalar field every terrain step samples; the pipeline is the chunk being made, step by step; the wing is the three structure lectures, which run first in the game and are taught last here, before only lecture ten. Part IV owns the conveyor that runs the statuses (the chunk generation pipeline); this part is the cargo of five of them.

The numbers below are the order to watch and the arrows the order the game runs, and the disagreement between them is the argument for the order.

flowchart TD
    CW["10 · Creating a world"] -.->|"chose the settings and packs"| DF["1 · Density functions"]
    DF -.->|"is sampled by"| L2
    subgraph SS["the structure statuses"]
        S7["7 · Structure placement"] --> S8["8 · Jigsaw and templates"]
        S7 --> S9["9 · Hand-built structures"]
    end
    SS --> L2["2 · Biomes, at BIOMES"]
    L2 --> TE
    subgraph TE["TERRAIN"]
        L3["3 · Terrain"]
        L4["4 · Blending"]
    end
    TE --> FE
    subgraph FE["FEATURES"]
        L5["5 · Features and placement"] --> L6["6 · Trees"]
    end
    SS -.->|"pieces become blocks"| FE

The part laid against the chunk-status ladder and numbered to the watch order; a solid arrow is the order the game runs, a dashed one says what it means in its label. The structure arc, at ChunkStatus.STRUCTURE_STARTS and ChunkStatus.STRUCTURE_REFERENCES, is first in the game and, before lecture ten, last in the lectures.

A structure is decided at ChunkStatus.STRUCTURE_STARTS, two statuses before the biomes it will stand in exist, and writes no block until ChunkStatus.FEATURES, one status after the noise fill: told in run order it is two lectures with biomes and terrain between them, and told last it is one arc — the dashed arrow from the structure box to ChunkStatus.FEATURES is that gap. The arrow out of lecture ten is the same trade at the other end. Both cost one thing, a forward topic — two of the six pages before the structure arc reach for the beardifier before the page that owns it (structure placement).

The arrow out of lecture one means is sampled by rather than happens before: the biome step is the first status to sample the field across a whole chunk, through a caching Climate.Sampler of its own. The decoration and structure packages never mention DensityFunction at all, and reach the substrate only through the beardifier, the heights the generator hands them and the Climate.Sampler a structure’s biome test reads — the generator’s own for a structure start, and one that StructureCheck and JigsawPlacement.generateJigsaw each build for themselves (density functions).

Before you start

The chunk generation pipeline from Part IV, and not optionally. It is the only page that says when any of this runs, what the ten chunk statuses are, how the dependency pyramid keeps neighbours out of each other’s way, and which thread each step is on. Eight of the ten pages here name a status, and two of them open on one.

Chunk anatomy, for what is being written into — sections, the two paletted containers, and the heightmaps the terrain steps maintain by hand. And environment attributes and timelines, also from Part IV, for lecture two: the sky, the fog, the music and twenty-three gameplay attributes come out of a stack of modifier layers in which the biome is one layer rather than the owner.

Codecs, NBT and JSON and identifiers and registries from Part II, because worldgen is the most thoroughly data-driven system in the game and this part assumes the dynamic-registry model rather than re-teaching it. The data-driven type pattern lists fifty-six instances of the pattern, and twenty-six of them are owned by a page in this part — more than any other part of the book.

Watch in this order

  1. Density functions — the substrate, and the abstract one. Three forms of one graph, two rewrites, and one kind of cache that caches nothing until something else grants it.
  2. Biomes — a nearest-neighbour search in seven dimensions, one of which is not sampled from the world at all, and the two biome borders the game keeps a couple of blocks apart.
  3. Terrain — noise, surface and carvers. Seven hundred and sixty-eight cells filled from their corners, and a cave whose water was decided before the cave was.
  4. Blending at the old-chunk border — the part’s deliberate exception, where generation reads chunks an older version left. Sixteen columns an old chunk measures out of its own blocks, and a seam where the terrain splines have no say.
  5. Features and placement — decoration as a stream of positions folded through filters, in one order the whole dimension shares, sorted once — and which a data pack can make impossible.
  6. Trees — one algorithm with five slots in it, and the clearance scan that runs after the crown has been sized.
  7. Structure placement — the part’s policy page. A lottery that never looks at the world, an absence stored as a hole, and a command that generates chunks to answer a question.
  8. Jigsaw and templates — how a village assembles itself, and how any piece becomes blocks. A growth limit that takes the right pool away a piece before it stops the queue.
  9. Hand-built structures — the older assembler, which is still most of the code. Four families of piece grammar, and the one structure that throws itself away and starts again.
  10. Creating a world — where the seed and the data packs came from. A screen that is a running data-pack load with widgets on it, settings carried across a reload by being serialised to JSON, and a Cancel button that does not undo.

Watch one to six straight through: that is the chunk being made, in the order it is made, and each page from two on leans on one before it. ChunkPyramid makes ChunkStatus.BIOMES a requirement of ChunkStatus.TERRAIN, so two really does come before three, and four needs both. Then stop and read seven, eight and nine as one arc — eight and nine are alternatives to each other, not a sequence, so either order works. Ten is the only page that moves: a viewer who wants the origin before the machinery can watch it first, at the cost of meeting the generators, biome sources and features it names before their lectures.

Where the part stops

At ChunkStatus.FEATURES. What happens to a chunk after that — lighting, spawning, promotion to a live chunk, and being saved — is Part IV; what the client is told about any of it is Part IX, and the answer is the finished chunk with none of the machinery.

It stops in a second direction too: 17% of the part’s lines are named on no page in the book, and almost all of them are one shape — an algorithm that writes blocks and adds no mechanism. Fifty-eight of the sixty-seven classes in the feature package are one Feature each — icebergs, geodes, lakes, coral, the End spikes — and beside them five smaller families of the same kind: the block predicates, the height providers, the state providers, the rule tests and the material rules and conditions, whose members are combinators over a shape one of these pages already teaches. Naming them would be a catalogue, and what this book skips declines it. What the part owes, and pays, is the head of each family — on features and placement, trees, terrain, its surface pass and jigsaw and templates.

Reference this part uses

Density-function nodes is the catalogue behind lecture one: all forty-four node types, what each takes, what the compiler turns it into, what range each reports, and which ids the shipped data writes. Beside it, registries for the seventeen worldgen/ registries a data pack writes into; structure spawn overrides for the six structures whose JSON replaces the biome’s spawn list, which lecture seven describes and Part VI runs; level data and rules for which file the seed, the dimensions and the rules end up in, which lecture ten links to rather than restates; and math and primitives for the two random families the determinism this page opens on lives in. Diagram lanes and the twenty-five rows naming drift keeps for this part are the two every part can use. And the glossary holds this part’s twenty-five headwords, which is where to go first if a page above spends one before you have met it.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Density functions

Verified against Minecraft 26.3 · Part XII · One number out of one point: how a JSON file becomes “stone or air”, and why the graph you can read in the registry is never the graph that runs.

Open the overworld’s depth function in a data pack and you can read the shape of the world out of it in twelve lines: a gradient down the Y axis, added to overworld/offset — which is fifteen hundred lines of continents and erosion wrapped in a node called cache. Both files are honest and readable. And the object it parses into is never sampled by anything, because it has no method that answers a number for a position: it is rewritten and compiled per dimension, and only the compiled form ever computes a number for a block. The cache named in that file caches nothing. It is a request, and something else grants it.

Terrain is a scalar field: a function from a block position to a float, where the convention is that zero is the surface and positive means solid. DensityFunction is the interface, DensityFunctions is the library of factories you build one out of — the node classes themselves sit in the generator and op packages under it — and a data pack assembles them as JSON. Forty-four ids are registered, which is not the same number as the classes behind them: eleven of them are one UnaryFunction, six are one BinaryFunction, four are one RoundFunction, three are one SimpleDensityFunction, and old_blended_noise is BlendedNoise, which lives with the noises in the synth package. This page is the three forms that one graph takes and the machinery that moves between them; the node types themselves are the node catalogue, and the terrain steps that sample the result are terrain.

Nothing here touches a block, and nothing here differs between two worlds built from the same settings and the same packs. This is the layer that the seed means.

The cast

classwhat it ownsits clock
DensityFunctionthe description, which answers no number for a position: DensityFunction.compileSampler builds its sampler, DensityFunction.rewriteChildren lets a rule rebuild it, and DensityFunction.range and DensityFunction.domainAxes are the static analysis—
DensityFunctionsthe factories, the registration of the node types, and the codecs that dispatch a JSON type to one of themstart-up, then data-pack load
DensitySamplerthe compiled form, the only one that answers a number: DensitySampler.sampleValue for one point, DensitySampler.sampleVolume for a whole DensityVolume at oncecompiled once per dimension
CacheFunctiona cache request, wrapping one function; it refuses to compilereplaced once per dimension
NoiseRouterthe eight functions a generator asks for, as one record; NoiseRouter.createClimateSampler turns six of them into a Climate.Sampler—
NoiseRouterDatavanilla’s graph, written in Java and emitted as the JSON that shipsbuild time, and at run time for NoiseRouterData.none and NoiseRouterData.peaksAndValleys
RandomStatethe per-dimension instantiation: the seed’s random factories, the noise memo, the DensityFunctionCompiler, the MaterialSystemonce per level
NoiseChunkone run of the graph: its DensityVolume — the chunk’s, or one column’s for a height query — its aquifer, and the one SamplerContext every sampler it hands out is bound toonce per chunk, and once per height query

Underneath all of it is the synth package: NormalNoise (a noise definition — octaves, amplitudes, normalisation — which NormalNoise.create seeds into one NoiseStack of paired PerlinNoise octaves), PerlinNoise (one octave of 3-D Perlin over GradientNoise’s permutation table), NoiseStack (octaves summed, each at its own frequency and amplitude), BlendedNoise (the pre-1.18 terrain noise, itself a density function node, built from SmearedPerlinNoise octaves), LegacyFbmInitializer (the old construction the two nether climate noises keep), and SimplexNoise, which no noise definition reaches: the End islands node uses it directly, and the fixed-seed noises Biome keeps for its own per-block temperature are built from it (biomes). Every seeded noise is a Noise, and with NoiseUtils that is the whole package. Noises, one package up, holds the sixty-four keys for the definitions, and Noises.instantiate seeds one from a positional factory.

Three forms of one graph

flowchart LR
    P["as parsed: pointers, cache requests, no seed"] -->|"rewritten"| O["as optimised: pointers inlined, caches numbered"]
    O -->|"compiled"| C["as compiled: one DensitySampler tree"]
    C -->|"bound, per run"| B["bound to one SamplerContext"]

One graph, three forms and a binding: the rewrite and the compile happen once per dimension, and every run binds the same compiled tree to a context of its own.

What changes between the forms is four kinds of node and any subgraph that ignores an axis, and the rest of the graph — an add, a gradient, a spline — comes through the rewrite as it was.

the nodeas parsedas optimisedas compiled
a pointer at another entry, DensityFunctions.HolderHoldera pointerthe graph it pointed atthat graph’s sampler
a cache request, CacheFunctionrefuses to compilea numbered DensityFunctionCompiler.PreparedCache, one per distinct inputa CachingDensitySampler that asks the context for its cell
a seeded leaf: NoiseFunction, and the shift, blended-noise and end-islands nodesa definition, no seedunchangedseeded from the compile context
the blend and beardifier leaves, SimpleDensityFunctionthree singletonsunchangeda ContextBoundSampler that asks the context, else answers 1, 0 or 0
a subgraph that ignores an axis its parent usesas writtenwrapped in a SliceFunction at zero on that axiscomputed once, copied along that axis
who samples itnothingnothingeverything, each against its own SamplerContext

The first two arrows are where the graph changes. A DfRewriteRule takes a node and returns one, reaching its children through DensityFunction.rewriteChildren, which rebuilds a node when a child comes back different; the compiler runs two in sequence — its own, which inlines every pointer and numbers every cache, then DfRewriteRule.SLICE_UNIFORM_AXES. Then DensityFunction.compileSampler turns what is left into samplers, each node building its own from its children’s. The third arrow changes nothing in the tree: it pairs it with a context, and everything a chunk adds — its caches, its blender, its beardifier — arrives that way.

Parse: one file, one graph

Every file under a pack’s worldgen/density_function directory becomes one registry entry through DensityFunctions.DIRECT_CODEC, which is an either: a bare number in the JSON is silently a ConstantFunction, and anything else dispatches on its type id through BuiltInRegistries.DENSITY_FUNCTION_TYPE — the ordinary shape of a data-driven type, and the reason a pack can write a new graph but not a new kind of node, because that registry is frozen at start-up (the data-driven type pattern, the freeze rule). Every child slot instead uses DensityFunction.CODEC, a RegistryFileCodec, so a string id, an inline object and a bare number are interchangeable everywhere a function is expected — and a RegistryFileCodec is exactly the kind that cannot resolve an id without a registry context to read it against (codecs, NBT and JSON). A string becomes a DensityFunctions.HolderHolder — a live pointer at another entry, which is how a graph references a graph.

Two things a reader of the JSON cannot see happen at the compile; construction keeps every node as the file wrote it. The compiler folds: BinaryFunction.compileSampler turns a two-argument node with one constant argument into a sampler that carries the constant, such as BinaryFunction.ConstAddSampler, unless a min or max has already been settled from its bounds, so a node type in the file is not necessarily the sampler that runs. And it reads the bounds, a static analysis of the data pack: DensityFunction.range answers from a node’s children without a position — one that talks back, because compiling a min or a max over two ranges that cannot possibly overlap logs a warning naming both arguments, and compiles only the side that wins whenever the inputs stay inside their declared ranges.

DensityFunctions.HolderHolder is the one parsed node that cannot be written back out: it is not registered, and asking it for its codec throws. It exists only in memory, and re-serialising a graph goes through DensityFunction.CODEC, which recognises it and emits the id string it came from.

Seed: once per dimension

RandomState.create forks the seed into one positional factory — from which RandomState.getOrCreateRandomFactory hands out named ones, aquifer and ore among them, each a factory of XoroshiroRandomSources unless the settings ask for the legacy family (math and primitives) — and builds a DensityFunctionCompiler, rewriting nothing: the first RandomState.getSampler for a function rewrites and compiles it, once for the dimension. The eight router fields partition cleanly by consumer, and the partition is the map of the rest of the part: six are the climate sampler’s (biomes), one — chunk_surface_level — is the surface rules’, and the last, final_density, is the one the fill samples a chunk from (terrain). The aquifer and the ore veins read functions of their own, from the settings’ aquifers block and the two ore-vein material rules; the aquifer’s surface_level, sampled on a quart grid, is how it finds the ground it should put a water table under. Nothing in the decoration or structure packages ever mentions DensityFunction at all; they reach the substrate only through the beardifier, the heights the generator hands them and the Climate.Sampler a structure’s biome test reads — the generator’s own for a structure start, and one that StructureCheck and JigsawPlacement.generateJigsaw each build for themselves. A density function is seeded inside the compile and nowhere else, through the DensityFunction.CompileContext that RandomState builds: a noise leaf gets a seeded Noise from RandomState.getOrCreateNoise, a memo keyed by the definition’s id; BlendedNoise gets a random source forked by the name terrain, or under the legacy family a LegacyRandomSource on the bare seed; the end-islands node gets a LegacyRandomSource on the bare seed. The parsed graph itself is never touched.

Two details in there matter later. The two nether climate noises are special-cased into a legacy construction over a LegacyRandomSource and therefore skip the memo entirely. And the compiler keeps its own memo of the caches it has prepared, keyed by what each one wraps, so a cache reached from several router fields is prepared once and stays one object — which is precisely what makes the per-chunk caching in the next step pay, because router fields that share a cache share its number, and a number is one cell in a context.

The memo goes further than that, because it keys on the wrapped node itself and the nodes are records: two separately parsed but structurally identical cache requests are equal, and so are merged into one object with one cache.

The compiled form is the only one anything ever samples, and away from the fill it is sampled in production: the biome step, the biome searches, the structure starts and checks, the height queries behind ChunkGenerator.getBaseHeight, the spawn search and the F3 noise readout, which NoiseBasedChunkGenerator.addDebugScreenInfo fills from the settings’ own debug_functions list, all run the dimension’s compiled samplers against contexts of their own. That is the whole reason a compiled sampler has to stay safe to run against any context, down to SamplerContext.EMPTY_UNCACHED, which holds no cache and no field at all. No second graph is built for the climate either: RandomState.createClimateSampler binds the six climate fields’ samplers to whatever context its caller brings, and that is the Climate.Sampler biomes reads.

Wrap: once per chunk

NoiseBasedChunkGenerator.createNoiseChunk builds the workspace and rewrites nothing; the SamplerContext the NoiseChunk constructor builds is the switch that matters. It has caches on, buffers from a DensityBufferPool borrowed from RandomState, and user fields that carry this chunk’s world: the chunk’s Beardifier under Beardifier.CONTEXT_KEY and, when the step’s Blender is not empty, the blender under Blender.CONTEXT_KEY and two samplers under Blender.ALPHA_KEY and Blender.OFFSET_KEY, over an alpha and an offset the constructor has already measured for the whole volume, before anything is sampled (blending). Every sampler the chunk hands out is the dimension’s compiled one, wrapped in a DensitySampler.Bound with that context. The three SimpleDensityFunction leaves look their key up on every call and fall back to 1, 0 and 0 when it is missing, and a blend_density node passes its input through when the context has no blender.

So a cache written into a data pack does something, but not by itself: it is a request that the compiler give that slot a number, and the node itself cannot even be compiled. The cell behind the number is the context’s, and a context built without caches, such as SamplerContext.EMPTY_UNCACHED, grants none. Worldgen performance lives in the compiler and the context, not in the node.

The fill samples the router’s final_density as the data pack wrote it, one volume for the whole chunk (terrain walks the loop that reads it). The beardifier is in the data too: every shipped router’s final_density ends in an add with a beardifier node, and the chunk only puts a Beardifier into the context, so a noise dimension is beardified only if its router’s graph has a beardifier node in it, and “structures flatten terrain” is implemented as a context lookup inside one leaf.

Nothing here needs reversing: the registry keeps the graph as parsed, and it serialises back unchanged, while DensityFunctionCompiler.PreparedCache refuses to encode and the compiled form is not a DensityFunction at all.

The one cache, and what a single point may use

This is the payoff of the whole arrangement: the data asks for one kind of cache, and what that cache does is decided by the context it runs in and the call it answers.

Each cache number is one cell in a context that enables caches, and the cell answers two calls. SamplerContext.sampleVolumeCached answers a volume: the first request samples the whole volume into a buffer from the context’s arena, and every later request for the same volume gets a copy, until a different volume replaces it. SamplerContext.sampleValueCached answers one point — from the one point the cell last computed, else from the cell’s buffer when the point lies on its volume, else by computing it and remembering it. A context built without caches has no cells, and every cache computes straight through.

A single-point sample is not a cache bypass — it reads the volume a cache already holds, when the point lies on it. A volume can be coarser than the blocks it covers, as the corners of interpolation cells are, and then a point on its grid is a hit while a point between is computed. Every sampler carries both calls, DensitySampler.sampleValue and DensitySampler.sampleVolume, and the volume is what the fill drives: one call for the chunk’s whole volume, each node filling a buffer from its children’s.

The resolutions are worth saying once. interpolated is not a cache at all: it carries its own cell size — four by eight by four under the overworld’s and the Nether’s final density, eight by four by eight under the End’s and the floating islands’, sixteen by one by sixteen for chunk_surface_level — samples its input at the cell corners, and fills the blocks between by trilinear interpolation. The per-column saving is no node’s: DfRewriteRule.SLICE_UNIFORM_AXES wraps every subgraph whose DensityFunction.domainAxes lack an axis its parent’s have, constants and gradients aside, in a SliceFunction at zero on that axis, so a two-dimensional term inside a three-dimensional graph is computed once per column of whatever volume is asked for. And vanilla spends the one cache freely: thirty-two cache nodes sit in the shipped density_function files, and no noise_settings file contains one at all.

What a bound is worth, and which form told you

DensityFunction.range is answered without a position, which makes it a static analysis of the data pack — and the analysis is of a graph that will be rewritten and compiled before it runs. Which individual nodes report something other than their child’s range is the catalogue’s (the node catalogue). What this page owns is what the compiler does with a bound — settles a min or a max from it, and hands the sampler the other side’s limit so that a single point can skip that side — and that every form that reports a bound reports the same one.

Seeding changes nothing. A noise leaf answers from its definition, not from a seeded noise: NormalNoise.range is fixed when the definition is decoded, and every one of the sixty-four shipped noise definitions reports a symmetric bound, from ±0.87 to ±5.73, before any seed exists. A freshly parsed router therefore reports the bounds the compiled one runs with.

Nor does the context. The three leaves a context can replace declare the range of what replaces them — zero to one for blend_alpha, unbounded for blend_offset and the beardifier — and their fallbacks, 1, 0 and 0, sit inside it. So a min or a max the compiler settles above a blend leaf is settled on the range the running graph has.

What nothing reaches

Among the names in this system that read as machinery and are reached by nothing, two are worth a warning, and one of them is the most misleading thing in the catalogue. DensityFunctions.shift, the three-dimensional domain warp, is written by no shipped file: vanilla uses only the two two-dimensional warps, which read the same noise parameters with their axes swapped, behind a registry id called offset rather than shift (which ids vanilla uses). Beside it, NoiseUtils.biasTowardsExtreme is a curve with no callers in a package where every other class builds or samples a noise. Density cannot join them: its three values are compile-time constants, which javac writes into whatever uses them, so the decompile cannot say who reads them — but it names the three conventions this whole system rests on: surface at zero, and the 64 and −64 a node reaches for when it wants to end an argument.

The nodes that read the world

Everything in the catalogue — every noise, spline, selector and cache — reads no block, no chunk and no level, which is why the whole system can run on the worker pool with nothing loaded, one chunk-status task at a time (the chunk generation pipeline). Four nodes from two sources are the exception, and all keep the property anyway by the same trick: they read only what the sampling context carries, and everything in it was harvested before the first sample. The three blend nodes reach BlendingData through the blender (blending at the old-chunk border); the beardifier node reads a Beardifier, whose Beardifier.forStructuresInChunk reads the structure references out of chunks at ChunkStatus.STRUCTURE_REFERENCES (structure placement).

That is what deterministic means here, stated once: the only two sources anything in the graph reads hold work done before the first sample — structures the same seed and the same packs placed, and, beside a chunk an older version left, what that chunk’s blocks measure — so the graph is a function of the seed and the packs everywhere except beside such a chunk, which is the part’s one deliberate exception (world generation).

For a 1.21-era reader. The cache markers flat_cache, cache_2d, cache_once and cache_all_in_cell are gone, and cache does their work; DensityFunction.compute is gone, and DensitySampler.sampleValue does its work on the compiled form.

Where to look

Read DensityFunction first — the interface is DensityFunction.compileSampler, DensityFunction.rewriteChildren and the static analysis, and everything else is a node — and then DensitySampler, the form that answers. Then DfRewriteRule and DensityFunctionCompiler, which are where the graph changes, with CacheFunction and DensityFunctions.HolderHolder as the two node types the rewrite exists to replace. DensityFunctions.DIRECT_CODEC is the parse; DensityFunctionCompiler.getSampler and the NoiseChunk constructor are the compile and the binding, in that order, and reading them side by side is the page. NoiseRouter names most of what they compile. Finish inside SamplerContext, where the cache cells live, and in NoiseRouterData.registerTerrainNoises for the graph vanilla ships. One door the page never opens: Noises.instantiate, which is where a noise definition becomes a seeded Noise.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Biomes

Verified against Minecraft 26.3 · Part XII · A point in the world gets a biome: six numbers quantised to integers, a nearest-neighbour search in seven dimensions, and the two different answers the game keeps for the same block.

Walk out of a desert into a jungle and watch the ground. The grass changes colour along one line, softened over a few blocks. The fog and the sky change along a different line, a couple of blocks away, and fade across it over a band several times as wide. Neither is a bug and neither is a rendering artefact: the game genuinely stores one biome per four-by-four-by-four volume and then answers “which biome is this block in?” two different ways, one jittered and one not, and different systems ask different questions. The surprise is which side each thing is on — grass colour, mob spawning and whether water freezes are all on the jittered side, and the sky is not.

A biome is a label in the chunk at quarter resolution — one per four-by-four-by-four block volume, which this book calls a quart cell (the code’s quart) — plus a bundle of consequences hanging off that label. The bundle is a thin one: Biome itself holds four things, and most of what a player would call “the biome” lives in the environment-attribute stack, where the biome is one layer among several rather than the owner (environment attributes and timelines).

The cast

classwhat it decideswhen
BiomeSourcewhich biome a quart cell gets, through the BiomeResolver it makes for the chunk fill. BiomeSources.bootstrap registers four — MultiNoiseBiomeSource, TheEndBiomeSource, FixedBiomeSource (one biome everywhere, which is what a Single Biome world is, and what a superflat and a Debug Mode world use too) and CheckerboardColumnBiomeSource (a listed few in squares) — and BiomeSource.possibleBiomes is the memoised pre-filter everything else leans onChunkStatus.BIOMES, on the worker pool
Climate.Samplerthe six climate numbers at a point — temperature, humidity, continentalness, erosion, depth and weirdness. RandomState.createClimateSampler binds the six to a sampling context, with or without caches: filling a chunk takes a caching one, whose functions MultiNoiseBiomeSource.createResolverForChunk samples over the whole chunk at once, and BiomeSource.createCachingResolver and BiomeSource.createUncachedResolver wrap one for a lookup outside the fill (density functions)once per chunk filled, or per point asked
Climate.ParameterListthe search space: Climate.ParameterPoints paired with biomes, indexed by a Climate.RTreebuilt at each world load
OverworldBiomeBuilderthe overworld’s parameter table, in Java — temperature, humidity, erosion and continentalness bands over six tables of biome keyseach world load
LevelChunkSectionwhere the answer lives: a second PalettedContainer keyed by biome holder, two bits per axiswritten at generation, saved, shipped
BiomeManagerthe jitter — which biome this block gets, as opposed to which cell it is inevery gameplay read
Biomefour things: climate settings, an EnvironmentAttributeMap, BiomeSpecialEffects and generation settings—
EnvironmentAttributeMapthe biome’s contribution to sky, fog, music, the spawn lists and the gameplay switches — as modifiers, not values. Twenty-three gameplay attributes exist; the sixty-seven vanilla biome files touch five of them between them — every file sets the spawn lists, and for fifty that is the only oneper attribute, per read

Deciding a chunk’s biomes, cell by cell

sequenceDiagram
    participant NBC as NoiseBased<br/>ChunkGenerator
    participant MNBS as MultiNoise<br/>BiomeSource
    participant ClimS as Climate.Sampler
    participant CA as ChunkAccess
    participant LCS as LevelChunkSection
    participant CPList as Climate.<br/>ParameterList

    Note over NBC,CPList: ChunkStatus.BIOMES, forked to the worker pool as createBiomes
    NBC->>MNBS: createResolverForChunk, with a caching Climate.Sampler
    MNBS->>ClimS: each of its six functions, sampled over the whole chunk
    ClimS-->>MNBS: a buffer per function, one value per quart cell
    MNBS-->>NBC: a resolver that reads the six buffers
    NBC->>NBC: decorateBiomeResolver, wrap the resolver twice
    NBC->>CA: fillBiomesFromNoise, with the wrapped resolver
    loop per section
        CA->>LCS: fillBiomesFromNoise, into a new container
        loop per quart cell, 64 of them
            LCS->>MNBS: getNoiseBiome, through the resolver
            MNBS->>CPList: findValue, the six buffered values as a Climate.TargetPoint
            CPList->>CPList: findValueIndex, the search down its Climate.RTree
            CPList-->>MNBS: the nearest biome holder
            MNBS-->>LCS: into the new container
        end
    end

A chunk’s biomes are the six climate functions sampled once over the whole chunk, then 64 searches per section, each a lookup into those samples and a nearest-neighbour walk; no block is read and none is written.

The two wrappers NoiseBasedChunkGenerator.decorateBiomeResolver adds only do anything beside or inside chunks an older version generated: the blender’s beside them (blending at the old-chunk border) and BelowZeroRetrogen’s inside one being deepened (the other passenger).

Biomes are decided before terrain, and not for its shape. ChunkPyramid makes ChunkStatus.BIOMES, one chunk out, a requirement of ChunkStatus.TERRAIN, so the order is enforced — but what the terrain step collects from it serves its surface pass alone: the biomes in the three-by-three chunks’ palettes (the carvers work theirs out again from the biome source). The noise fill never reads a biome. What makes a jungle and its terrain agree is that both were computed from the same noise router: four of the six functions in the climate sampler RandomState.createClimateSampler binds are the very depth, continents, erosion and ridges functions that shape the land — under the Climate.Sampler names depth, continentalness, erosion and weirdness. Neither was consulted about the other. The biome does not touch a block until the surface pass, which follows the fill inside the same ChunkStatus.TERRAIN task (terrain).

The fork at the top of that trace is not the noise generator’s: ChunkGenerator.createBiomes is the base class’s alone and forks to the worker pool, as createBiomes, for every generator, so a superflat world leaves the worldgen executor for its biomes exactly as the overworld does. What NoiseBasedChunkGenerator overrides is one step, ChunkGenerator.decorateBiomeResolver, for its two wrappers; FlatLevelSource and DebugLevelSource keep the base one, which wraps nothing. Sampling the whole chunk up front is the biome source’s choice, not the generator’s: only MultiNoiseBiomeSource overrides BiomeSource.createResolverForChunk, and the other three sources answer cell by cell through BiomeSource.createResolver. The End is a NoiseBasedChunkGenerator like the overworld and the nether — just with TheEndBiomeSource in front of it, which does not do a climate search at all: it thresholds a single erosion sample outside a fixed central radius.

The search, and the axis that is not sampled

Climate.quantizeCoord multiplies each of the six climate values by ten thousand and truncates, so the entire search is integer arithmetic. The target is a Climate.TargetPoint of six longs; each entry in the list pairs a biome with a Climate.ParameterPoint of six Climate.Parameter intervals, and most biomes have several; and Climate.ParameterList.findValue walks a Climate.RTree — up to nineteen children per node — minimising the sum of squared distances from the target to each interval.

Except the count is seven, not six. Climate.PARAMETER_COUNT is 7 and Climate.RTree.create refuses a point that does not supply seven, because each Climate.ParameterPoint carries a scalar offset alongside its six intervals, and Climate.TargetPoint.toParameterArray appends a literal zero as the seventh coordinate of every query. So the seventh term of the metric is always that biome’s offset squared: a fixed penalty added to its score. It is a “make this biome harder to win” dial, not anything sampled from the world.

The tree also remembers. Climate.RTree keeps the winning leaf in a ThreadLocal and seeds the next search with it as the initial candidate. Adjacent quart cells almost always resolve to the same biome, so the walk usually prunes immediately — which is what makes filling sixty-four cells a section cheap. The tree is therefore stateful per thread, though never incorrect: the remembered leaf is only a starting bound.

Which is also the answer to whether a pack can move the overworld’s biomes around: not the vanilla preset. OverworldBiomeBuilder is hardcoded Java, and the data-pack element that would carry it, MultiNoiseBiomeSourceParameterList, serialises to nothing but a preset name — MultiNoiseBiomeSourceParameterLists registers two. A pack may supply its own parameter list inline for a multi-noise source; it cannot edit the one that ships.

One thing the parameter table does not have is a separate underground system. OverworldBiomeBuilder.addUndergroundBiomes and OverworldBiomeBuilder.addBottomBiome place dripstone caves, lush caves, sulfur caves and the deep dark into the same seven-dimensional table by their depth band. A cave biome is an ordinary entry that happens to win only below the surface.

The two borders

The label goes into the section’s second paletted container — two bits per axis, so sixty-four biome cells per section (sections and their four counters) — and is shipped to the client inside the chunk payload. From then on it is stored, not computed, which is why FillBiomeCommand can exist at all and why ClientboundChunksBiomesPacket exists to tell the client about the result.

And then two different readers ask for it two different ways.

the jittered readthe exact read
entry pointLevelReader.getBiome → BiomeManager.getBiomeBiomeManager.getNoiseBiomeAtPosition, and on the client BiomeManager.getNoiseBiomeAtQuart
what it doesoffsets by two, takes the eight surrounding quart corners, and picks the one minimising BiomeManager.getFiddledDistance — a seeded hash worth up to ±0.45 of a cell per axisfloors to the quart cell and reads the palette
who uses itfreezing and precipitation, mob spawning, commands, the surface rules during generation — and block tint: grass, foliage and water colour, through ClientLevel.calculateBlockTintthe environment-attribute stack, for every attribute but the spawn lists — EnvironmentAttribute.isFullResolutionBiomes sends those to the jittered read — and nothing else that goes through BiomeManager
what it looks likethe ragged borderthe straight one

Block tint is on the jittered side, which is the half of this that surprises people: grass colour comes from the same jittered read as whether snow falls. What softens the colour boundary in game is not the biome lookup but a box blur on top of it, which the client owns (the client level). Fog and sky are the ones on the other border.

The client’s exact read is the more expensive of the two, and it is spent unconditionally: the probe that feeds the attribute stack runs its Gaussian pass over the neighbourhood every tick whether or not any attribute at that position is interpolated at all, because the test is applied later, when the layer is applied (the same value on the client). The server never interpolates and never pays it — it passes no interpolator.

What a biome still owns

Four things, and none of them stops being read once the chunk is generated — NaturalSpawner reads the spawn attribute every tick and a bone-mealed grass block asks the generation settings.

Biome.climateSettings is precipitation, a base temperature, a Biome.TemperatureModifier and downfall. Temperature is the interesting one: Biome.getHeightAdjustedTemperature samples noise per block high above sea level — which is why snow lines are uneven rather than flat — and Biome keeps a fixed-size per-thread cache in front of it that evicts rather than grows. Most of the public surface is the questions rather than the number: Biome.warmEnoughToRain, Biome.coldEnoughToSnow, Biome.shouldFreeze, Biome.shouldSnow. Biome.getBaseTemperature is the raw, uncached, unadjusted escape hatch.

BiomeSpecialEffects is only block tint — five fields, all of them colours or a grass-colour modifier, and only the water colour is mandatory. Fog, sky, clouds, ambient sound, music and particles are attributes instead. What is left is read by nothing in the attribute system at all: BiomeColors reaches these five fields through four ColorResolvers, with no probe and no layer stack anywhere in the path (lightmap, fog and sky). And when the four optional ones are silent, the grass and foliage tints do not come from the effects at all: they are a lookup into the colormap images by the biome’s base temperature and downfall, through GrassColor, FoliageColor and DryFoliageColor. “The biome’s grass colour” is usually just the two climate numbers that index a texture.

Biome.getAttributes returns the EnvironmentAttributeMap, and what a biome puts in it are modifiers, not values — which is how a swamp thickens water fog without naming a distance (arguments, not values). One restriction lands here specifically: EnvironmentAttributeMap.CODEC_ONLY_POSITIONAL means a biome may not set a non-positional attribute at all. And one cost lands on the whole dimension: a positional layer is built per attribute that any biome in the registry mentions, at level construction (the stack a value falls through), and a layer is not free where the biome that wanted it is absent — a biome that names an attribute no other biome does adds a layer every position in every dimension then falls through.

BiomeGenerationSettings holds two lists, and they are read by different pages: the placed features, one set per decoration step, by features and placement, and the carvers by terrain, at a different status. BiomeGenerationSettings.getBoneMealFeatures is its only reader outside worldgen, and its one caller is GrassBlock. The spawn lists are in the attribute map, as EnvironmentAttributes.NATURAL_MOB_SPAWNS, which every biome file sets with a MobSpawnSettings argument and NaturalSpawner reads through the attribute stack — though a structure at the position can replace them outright before the spawner ever looks (a spawn attempt is a filter) — and the MobSpawnSettings.SpawnerData constructor silently rewrites any miscellaneous-category entity type to pig.

Of the four, the client is given two and a half. Biome.NETWORK_CODEC sends the climate settings, the syncable attributes and the effects, and substitutes empty generation settings; the spawn-list attribute is not syncable, so features, carvers and spawn lists never cross the wire. And when a client asks for a biome in a chunk it does not have, it gets plains.

Questions players ask

Why do I always spawn near the origin? Because the chunk is chosen by a climate search. The search runs every time the world is loaded, since the structure state of a dimension whose settings name a spawn target asks for its result, but only a brand-new world takes a spawn from it, running the search a second time to do so (building the levels owns when). NoiseSpawnFinder.findSpawnPosition looks for the point whose climate best matches the noise settings’ spawn target — a list of SpawnTargetPoints, each a set of intervals on named density functions, for the overworld five of the six climate functions with depth left out — in two spiral passes, the first out to 2,048 blocks and the second a finer one around its best, with the fitness deliberately biased toward the origin so that a tie lands near 0,0. NoiseBasedChunkGenerator.getOrigin keeps only that answer’s chunk, and MinecraftServer does a terrain search from it: an eleven-by-eleven chunk spiral of PlayerSpawnFinder.getSpawnPosInChunk, over a first guess taken from ChunkGenerator.getSpawnHeight or, failing that, the WORLD_SURFACE heightmap. A dimension whose settings name no spawn target skips the climate half and starts that spiral at the origin chunk.

Why does /locate biome find biomes in chunks I have never visited? Because it asks the generator, not the world. BiomeSource.findClosestBiome3d spirals through a resolver from BiomeSource.createCachingResolver and never reads a palette — so it finds biomes in ungenerated chunks and will never find one placed by /fillbiome. It pre-filters against BiomeSource.possibleBiomes, so asking for an impossible biome fails instantly rather than after a spiral out to six thousand four hundred blocks.

For a 1.21-era reader. Biome.getMobSettings is gone: the spawn lists are the attribute EnvironmentAttributes.NATURAL_MOB_SPAWNS. BiomeSource is not itself a BiomeResolver; it makes one. RandomState.sampler and NoiseChunk.cachedClimateSampler are gone; RandomState.createClimateSampler does their work. Climate.findSpawnPosition is NoiseSpawnFinder.findSpawnPosition.

Where to look

BiomeResolver.getNoiseBiome is the one method the whole search hangs off, and BiomeSource.createResolver and BiomeSource.createResolverForChunk are where a source makes one; read them with MultiNoiseBiomeSource beside them, and TheEndBiomeSource after, for the source that does no search at all. Then Climate whole — it is one file holding Climate.Sampler, Climate.quantizeCoord, Climate.ParameterList.findValue and the Climate.RTree — and Climate.PARAMETER_COUNT is the line that explains the seventh dimension. OverworldBiomeBuilder.addBiomes is vanilla’s table, in Java, and OverworldBiomeBuilder.addUndergroundBiomes is where the cave biomes turn out not to be a separate system. For the two reads, BiomeManager.getBiome against BiomeManager.getNoiseBiomeAtPosition, with BiomeManager.getFiddledDistance as the jitter itself. Finish at Biome, which is short, and at LevelChunkSection.fillBiomesFromNoise for where the answer is stored. Two doors the page does not open: BiomeColors, for how the five effects fields reach a rendered block, and Climate.RTree.search, for the walk the page only describes.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Terrain

Verified against Minecraft 26.3 · Part XII · One chunk’s rock: seven hundred and sixty-eight cells filled from their corners, a water table decided before the caves are cut, and the cave that fills with water because of it.

You dig into a cave and it is flooded. The water is not a fluid that flowed in and settled; nothing flowed anywhere. Before the cave existed, while the chunk was still solid stone, something decided that a point at that depth in that column belongs to water rather than to air — and when the hole the carver marked out came to be filled, the aquifer gave the same answer. A carver does not choose the block it carves. It chooses the shape; the Aquifer chooses the material, and it chose it for the stone as well.

This page is one chunk status, ChunkStatus.TERRAIN, and the three steps inside it that turn a scalar field into blocks — the noise fill, the surface pass and the carvers — which share one workspace for as long as the status’s one task runs. The conveyor that runs the statuses, the dependency pyramid and the threading are the chunk generation pipeline in Part IV; this is the cargo. The scalar field itself is density functions, and the labels that steer the surface pass are biomes.

The cast

classwhat it decideswhen
ChunkGeneratorthe API the status calls — ChunkGenerator.buildTerrain, one call for all three steps. ChunkGenerators.bootstrap registers exactly three implementationsworldgen executor
NoiseGeneratorSettingsthe per-dimension recipe: the NoiseSettings floor and height, the default block and fluid, the NoiseRouter, the material rule, the sea level, and the aquifer’s own functions where there is onedata, loaded with the world
NoiseChunkthe per-chunk workspace — the chunk’s DensityVolume, the samplers that carry its caches, the Aquiferbuilt when the terrain task starts, closed when it ends; a height query builds a one-column one of its own
InterpolatedFunctionthe cell: its input sampled at the corners, every block between them interpolatedwhenever the graph is sampled
Aquiferwhat liquid, if any, belongs at a point — and therefore what a carved hole is filled withfill and carving
OreVeinRulecopper or iron, from the sign of one functionthe surface step: in the overworld’s tree, after the bedrock floor and before any surface rule
MaterialSystemthe column re-skin: grass over dirt over stone, sand, the badlands bands, the ore veinsthe surface step and the carvers’ re-skin, one instance per level
WorldCarverthe shape of caves and canyons, and nothing about their contentsthe carver step

Everything here starts on the worldgen executor, and for this generator the whole status fans out from it: NoiseBasedChunkGenerator.buildTerrain hands the fill, the surface pass and the carvers to the worker pool as one task (which steps fork, and why the parallelism is smaller than the thread names). What is worth carrying from that into this page is one object. RandomState — the per-level seed root — is built once in ChunkMap and shared by every generating chunk, and it owns the MaterialSystem, which is therefore per level, not per chunk.

Three steps, and what each hands on

flowchart LR
    BIO["BIOMES"]
    subgraph TER["TERRAIN, one task"]
    FIL["fill"] -- "rock and water" --> SUR["surface"]
    SUR -- "a skin, the veins" --> CAR["carvers"]
    end
    FEA["FEATURES, a later page"]
    BIO -- "biomes, no workspace" --> FIL
    CAR -- "holes, workspace closed" --> FEA

The terrain status is one task: the three steps inside it share one NoiseChunk, built when the task starts and closed when it ends, and the last box belongs to a later page.

The frame is the point. The three steps are one job. ChunkStatusTasks.buildTerrain builds a Blender for the chunk’s neighbourhood and hands it, with the region, to NoiseBasedChunkGenerator.buildTerrain, which runs the fill, the surface pass and the carvers back to back as one task. The workspace is born at the start of that task, when NoiseBasedChunkGenerator.createNoiseChunk gives it the chunk’s DensityVolume, samplers that carry the chunk’s caches, and an Aquifer, and it dies at the end, when NoiseChunk.close hands its density buffers back to RandomState. The biome step before it builds none of this: it samples the climate through a caching sampler of its own (biomes). Two passengers ride in on the workspace, and both are things this page’s terrain is bent by rather than things it decides.

The beardifier is one of them: a beardifier node in the final density’s own data, which reads zero unless the sampling context holds a Beardifier — and NoiseBasedChunkGenerator.createNoiseChunk puts one there for this chunk, built by Beardifier.forStructuresInChunk (wrap: once per chunk). Because it is built with the workspace, a structure has bent the density field before the field is ever sampled — the flat shelf under a village is a number added to a scalar field, not a block edit made afterwards. What that term’s shape is — Beardifier.Rigid boxes, junctions at half weight, and the five TerrainAdjustment modes — belongs to structure placement. A structure asking how high the ground is gets the answer from before it bent anything: NoiseBasedChunkGenerator.iterateNoiseColumn, behind ChunkGenerator.getBaseHeight and ChunkGenerator.getBaseColumn, builds a throwaway NoiseChunk one block wide with an empty Blender and no Beardifier at all, samples a column, and closes it.

The blender is the other, and it is why an empty one appears in that sentence. Blender reads measurements harvested from chunks an older version generated and enters the density graph as three nodes. The one that reaches the graph is the one ChunkStatusTasks.buildTerrain builds with Blender.of for this task, which NoiseChunk puts in the sampling context beside the beardifier; the one the biome step builds bends only the biome resolver (blending at the old-chunk border). BelowZeroRetrogen, the world-deepening path, rides the same hooks: it wraps the biome resolver, patches bedrock after the terrain step, and gates the spawn step (blending’s other passenger).

That one workspace then serves all three steps, so the carving step reuses what the aquifer has already worked out: the water tables it computes on its grid, and the surface levels it caches, are still there when the carved holes are filled — and since each is fixed by the seed, the position and, beside an old chunk, the blender, they would come out the same if they were not. The surface pass samples the preliminary surface and the ore veins through the same samplers and their caches (the one cache, and what a single point may use), and nothing is pinned to a thread: the three steps run back to back inside one task on one worker, so program order is all that serialises them.

Filling the noise: six loops, one number at the bottom

The fill asks for the whole chunk in one call. NoiseBasedChunkGenerator.doFill has the workspace’s samplers sample the router’s final density over the chunk’s DensityVolume — sixteen by 384 by sixteen blocks in the overworld — and gets back one buffer holding a number for every block. The cells are inside that call. NoiseSettings holds only a floor and a height; the cell size belongs to the interpolated node in the final density, and the overworld’s asks for four blocks across and eight up. So the unit of overworld terrain is a cell four blocks wide, four deep and eight tall, and a chunk is four by four by forty-eight of them.

768 cells make one overworld chunk, each holding 128 blocks (InterpolatedFunction).

An interpolated node samples its input at cell corners only. Everything inside a cell is three linear interpolations away from the eight corners around it, and the walk that does this is six loops deep:

flowchart TD
    subgraph CZ["4 cell rows, in Z"]
    subgraph CX["4 cell columns, in X"]
    subgraph CY["48 cells, upward"]
    subgraph BZ["4 block rows, in Z"]
    subgraph BX["4 blocks in X"]
    BY["8 blocks upward, one number written at each"]
    end
    end
    end
    end
    end

Six loops, and none of them samples the graph: every corner was sampled in one call before the first, and everything nested inside is interpolation.

looprunswhat it does
cell row, in Z4nothing but advance
cell column, in X4reads the four corners at the column’s foot
cell, Y upward48reads the four corners above, which become the next cell’s floor
block row, in Z4four interpolations in Z
block, in X4two more in X, and the step from block to block in Y
block, Y upward8one addition, one number written

Read the nesting as the cost model. Every corner is sampled once, in the one volume call made before the first loop, and nothing below it evaluates the graph again: each cell reads four new corners and inherits four, and every block inside it is arithmetic on eight numbers. The counts go up by one in each direction when you move from cells to their corners: four by four by forty-eight cells have five by five by forty-nine corners between them:

1,225 corner samples per chunk for each four-by-eight interpolated term the fill reads (InterpolatedFunction, one volume of five by forty-nine by five).

Six interpolated terms sit in the overworld router, resolved through every reference: one round the bulk of the final density, four inside the noodle-cave graph, and one in chunk_surface_level, whose cell is a whole chunk wide and one block tall. The fill reads the first five; the sixth is the surface pass’s, and the ore veins bring three more of their own. Neither the veins’ gap nor the aquifer’s barrier is interpolated: the vein rule and the Aquifer sample each block by block, and only where they need it. Everything else is sampled at the resolution of the volume it is asked for, less any axis it does not depend on — a term that ignores Y is sampled once per column (the one cache, and what a single point may use).

Then NoiseBasedChunkGenerator.doFill walks the finished buffer block by block — Z, then X, then Y downward — and hands each number to the aquifer. The two worldgen heightmaps are updated as those blocks are written, so walking down, a column’s height for each map is settled by the first block that counts for it.

The write at the bottom of that walk skips air entirely — a chunk starts empty, so only non-air is ever written — and updates Heightmap.Types.OCEAN_FLOOR_WG and Heightmap.Types.WORLD_SURFACE_WG by hand rather than through the chunk’s ordinary write path. It can, because it holds every section across its noise range for the whole fill (what placing a block actually does).

Those two stop being maintained once this status completes, and not because anything stops writing them: which maps are live is a property of the status, not of the step — the two _WG maps up to ChunkStatus.BIOMES, the status the chunk still holds while all three steps run, and the four final ones from ChunkStatus.TERRAIN on, primed by Heightmap.primeHeightmaps as the terrain task ends (the six heightmaps). A chunk saved mid-generation really does persist the pair the fill was keeping.

The aquifer: what the number becomes

NoiseBasedChunkGenerator.doFill does not read the density and compare it to zero. It hands the number to Aquifer.computeSubstance and writes what comes back, and the aquifer is the only thing the number meets.

The aquifer. Aquifer.computeSubstance receives the final density and decides, from four noises of its own — a barrier it samples block by block, and floodedness, spread and lava, which it samples on coarse grids — whether this point is stone, air, or a fluid. None of them is read from the router: the noise settings carry an Aquifer.Config of six functions, those four, an exclusion that forbids a local water table in any aquifer cell where it is positive, and surface_level — which the overworld points at its preliminary_surface_level function — which it samples once per four-by-four column to know roughly where the ground is before any ground exists. Aquifer.FluidPicker is the global fallback underneath its local water tables — the sea, and the lava — and Aquifer.FluidStatus is the record of any one table, local or global. Aquifer.NoiseBasedAquifer is the real implementation; a dimension with aquifers switched off gets a trivial one.

The ore veins are not decided here at all. OreVeinRule is a material rule, and the overworld’s rule tree tries its two veins, copper then iron, after its bedrock floor and ahead of every surface rule, so the surface pass writes them into the stone the fill left. That is why copper and iron veins are terrain rather than decoration: they exist before the carvers, and no feature places them. Every other ore in the game is an AbstractOreFeature placed at ChunkStatus.FEATURES (features and placement). Which of the two veins you get is the sign of one function, ore_vein/toggle — copper’s density reads it as it is and iron’s reads it negated — and there is no separate “which ore” noise.

If the aquifer returns nothing, the block becomes the settings’ default block.

The surface pass, and the two places it breaks its own rule

MaterialSystem.buildSurface compiles the dimension’s NoiseGeneratorSettings.materialRule tree once for the whole chunk — one rule tree per dimension, which then branches on biome inside itself, then walks each of the 256 columns downward from the worldgen surface heightmap, tracking depth below stone and water height in a MaterialRuleContext that carries its own caches. Every block that is neither air nor fluid is offered to the tree, and the first rule that answers is written: fluid is measured, never offered, and the overworld’s tree tries the ore veins, after its bedrock floor, before any surface rule, which is what makes aquifer water and ore veins immune to being turned into grass.

That rule tree is itself two registries of data-driven types, with files of their own that the noise settings name by id — MaterialRule for what to write and MaterialCondition for when, each dispatched on a type id like any other (the data-driven type pattern) — so a pack composes a surface out of the shipped condition types and cannot write a new kind of condition. The biome the tree branches on is the jittered read, BiomeManager.getBiome, not the palette’s exact one (the two borders), which is why a surface rule can change block for block along the same ragged line the grass colour does.

Almost none of this runs in a superflat world. FlatLevelSource lays its layers at the terrain status and DebugLevelSource writes nothing there — neither has a surface pass or carvers, both make spawning a no-op, and DebugLevelSource writes its state grid at the decoration step instead — so of the three ChunkGenerators.bootstrap registers, two skip most of this page. Debug flags can switch off much more: SharedConstants carries flags, set from JVM system properties in any build, that disable the surface pass, the carvers, the ore veins and every aquifer’s water and lava outright.

Two things sit outside the rule tree entirely, and neither asks it. MaterialSystem.erodedBadlandsExtension runs before the column walk and fills air with the default block to raise the terracotta pillars. MaterialSystem.frozenOceanExtension runs after it and writes snow and packed ice over air and over water. Both are selected by biome rather than by rule, and MaterialSystem holds the noises they need along with the one that offsets the badlands bands and the two behind the surface depth the conditions read; a noise-threshold condition asks RandomState for its own.

Carving, and who chooses the block

NoiseBasedChunkGenerator.generateCarvers does not carve the chunk it was given from the chunk it was given. It loops over a 17×17 neighbourhood of source chunks, asks each carver of that source chunk’s biome whether a cave or a canyon starts there, and has whatever does mark its shape into the centre chunk. That reach costs the dependency pyramid nothing: the neighbours are read only as memo holders for ChunkAccess.carverBiome, and the biome itself is recomputed from the biome source. The reach is already paid for, because every generation step from ChunkStatus.STRUCTURE_REFERENCES to ChunkStatus.FEATURES asks for structure starts eight chunks out anyway (the pyramid, drawn).

Two carver types are registered by WorldCarverTypes.bootstrap — cave and canyon, which are CaveWorldCarver and CanyonWorldCarver — and four carvers ship, because the cave type is configured three times: once for the surface caves, once for a deeper set, and once for the Nether. Each is a record carrying its own parameters, and its codec is the data-pack half: no configuration class stands between carver and file. None of them touches a block. A carver is handed a WorldGenerationContext for its heights and a CarverOutput to mark, and what it marks lands in a CarvingMask, a bit set that lives for one terrain task and is never saved.

And then the hook. When every source chunk has had its say, NoiseBasedChunkGenerator.applyCarvingMask walks the mask’s carved runs, each top down, and asks Aquifer.computeSubstance with a density of zero what belongs at each marked block. Aquifer.FluidStatus.at answers plain air above the local water table and the fluid below it — never null; the null is Aquifer.computeSubstance’s own, and it means do not carve here at all. So the water in a flooded cave was decided by the same object that decided the water in the stone around it, and a dry cave is air written one block at a time because the aquifer said so. What kind of block a cave may not eat through is decided by one tag, uncarvable (BlockTags.UNCARVABLE), which ships holding bedrock alone, so a cave cuts through the ore veins and the surface skin alike. If a grass or mycelium block was passed on the way down, the dirt below is re-skinned through MaterialSystem.topMaterial.

The Nether follows the same rule. Its nether_cave is the cave type with other numbers, and its holes are filled by the same question, put to the trivial aquifer a dimension with aquifers switched off gets — which answers from the global fluid picker alone: lava, the Nether’s default fluid, below its sea level of 32, and air above.

The seeding is one thing a data pack cannot reach here. NoiseBasedChunkGenerator.generateCarvers hardcodes a LegacyRandomSource whatever the settings say, so switching a dimension to the modern random family does not move a single cave. That setting, NoiseGeneratorSettings.useLegacyRandomSource, does reach past the level’s root random in one other way: it re-seeds BlendedNoise.

Questions players ask

Why is there always lava at the same depth, in every world? Because the two levels are anchored to different things. The sea level is a field of the noise settings and moves with them; the lava floor is not in the settings at all. NoiseBasedChunkGenerator.createFluidPicker builds the global fluid picker every aquifer falls back to, and it answers lava below Y −54 — or below the sea, where the sea is lower still — and the sea above. So the lava floor under every overworld cave, carved or not, is one fixed height rather than a depth below the sea, and a data pack moves it only through the sea: by sinking the sea beneath it, or by making the settings’ default fluid lava.

Why is the water in a cave already settled when I break into it? Because it was decided before the cave was, and placed as still water. Aquifer flags only the fluid it places near the border between two aquifer cells, and there only where it could flow — where the cells’ tables differ, in height or fluid or because one is dry, or where water sits directly on the lava floor — and the fill and the carving step record those positions for post-processing, so those few flow for real when the chunk starts ticking (what the chunk goes on holding). “Nothing flowed in” is a true statement about worldgen and not about the chunk’s first tick as part of a live world.

For a 1.21-era reader. NOISE, SURFACE and CARVERS are one status, ChunkStatus.TERRAIN, and the cell loop NoiseChunk ran is InterpolatedFunction’s. SurfaceSystem is MaterialSystem, and SurfaceRules are MaterialRule and MaterialCondition, with the ore veins among them as OreVeinRule; OreVeinifier is gone. ConfiguredWorldCarver and the carver configurations are gone — a carver is a record that carries its parameters — and NetherWorldCarver is gone, the Nether’s caves being the cave carver with other numbers.

Where to look

NoiseBasedChunkGenerator is the page in one class: NoiseBasedChunkGenerator.buildTerrain is the status, and NoiseBasedChunkGenerator.doFill, NoiseBasedChunkGenerator.buildSurface and NoiseBasedChunkGenerator.generateCarvers are its three steps; reading them in that order is the trace. Under the first, NoiseChunk is the workspace and InterpolatedFunction the six loops, and Aquifer.computeSubstance is the decision at the bottom of the write walk and repays being read twice, because the carving step calls it too. MaterialSystem.buildSurface is the column walk, and MaterialRule beside MaterialCondition is the data-pack half of it, with OreVeinRule for the veins. Then NoiseBasedChunkGenerator.applyCarvingMask, which is the sentence this page is built around, with CaveWorldCarver for the shape. NoiseGeneratorSettings is worth opening once to see how much of all this is data. Two doors the page does not open: MaterialRuleContext, for what a rule may read, and OreVeinRule.compile, for a vein in full.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Blending at the old-chunk border

Verified against Minecraft 26.3 · Part XII · a chunk generated beside one an older version left behind: one measurement of the old ground, and the five places generation is bent around it.

In a world old enough to have been generated by a previous version, the debug screen’s chunk-generation entry reads Blending: Old on one side of a line and stops reading it on the other. For roughly a hundred blocks on the new side of that line the game is generating chunks that are not free to be whatever the seed says — and directly against the seam they are hardly generated at all. The three functions that shape overworld terrain, offset, factor and jaggedness, are each swapped for a fixed target: a ground height measured from the old chunk’s own blocks, the constant ten, and zero. At the seam the overworld’s terrain splines have no say.

This is the deliberate exception the part’s premise names. Everywhere else in world generation, a chunk is built to be a function of the world’s settings and its data packs. Here it is a function of those and of the blocks in the old chunks around it — which the game remembers only in part and has to go and measure, one column at a time.

The flag is a nullable field, and looking for it costs a disk read

A chunk is old if ChunkAccess.blendingData is non-null. That is the whole test: ChunkAccess.isOldNoiseGeneration returns exactly whether that field is set, and the field is final — it arrives through the constructor from SerializableChunkData, the record that reads a chunk’s NBT (chunk storage), which pulls a blending_data compound out of it and nothing sets afterwards. Which saves carry that key is util/datafix’s business, which this book skips; by the time world generation sees a chunk the key is either there or it is not.

The awkward part is cost. A chunk being generated could ask its neighbours whether they are old — the dependency window guarantees them — but that is 193 chunks to reach for an answer that, in a world this version created, is always no. So Blender.of — the factory the BIOMES and TERRAIN steps call — starts by asking the save file. WorldGenRegion.isOldChunkAround hands the question to the level’s ChunkMap, which inherits SimpleRegionStorage.isOldChunkAround and lands on IOWorker.isOldChunkAround. That walks the region files covering a square of radius seven, and for each region it needs a BitSet with one bit per chunk, built by scanning all 1,024 chunks in the region — a region file is thirty-two by thirty-two (inside a region file) — for two NBT fields and nothing else: DataVersion and blending_data. The save check counts a chunk if its stored DataVersion is below 4997 or if it already carries a blending_data compound — a wider net than the flag, so a yes can still end in the empty blender. The scan runs on the worker pool, the caller joins it, and the bitset is kept in a 1,024-entry region cache.

Every chunk generated in every world pays for that lookup twice, whatever generator the dimension uses: ChunkStatusTasks.generateBiomes and ChunkStatusTasks.buildTerrain both evaluate Blender.of eagerly, before knowing whether the answer can possibly be yes (which steps may write), and nothing else in the game calls it. The noise generator adds none: its carving pass asks the TERRAIN run’s blender for its filter. When nothing is old the bitsets come back empty and Blender.of hands back the shared empty blender, an anonymous subclass over two empty maps that overrides the three answering methods with identities: alpha one, offset zero, density unchanged, resolver returned as given. The lookup is cheap after the first time, because the per-region bitset is memoised behind IOWorker.getOrCreateOldDataForRegion and so is scanned once per region rather than once per chunk; a debug flag set from JVM system properties, SharedConstants.DEBUG_DISABLE_BLENDING, can drop it entirely, which short-circuits Blender.of, the carving-mask filter and the border ticks alike.

The cast

classwhat it decideswhen
Blenderthe three answers — the height alpha and offset, the blended density, and the biome override — and the carving filter it builds beside them. The border ticks are a static method on the same class and never ask an instance anythingbuilt per step on the worldgen executor, asked from the worker pool; the border ticks run inline at ChunkStatus.FEATURES
BlendingDataone old chunk’s measurements: a ring of sixteen columns holding a height, a density profile and a biome column eachmeasured once per loaded chunk object
IOWorkerwhether any chunk within seven is old, from a per-region BitSet scanned out of the region fileson the worker pool, joined by the caller
NoiseChunkwhere the answers enter the density graph: the blender and its two samplers, set in the sampling contextChunkStatus.TERRAIN, one per run
NoiseRouterDatawhich router functions the shipped data makes blendable — three overworld splines and one post-process wrapperbuild time, emitted as the JSON that ships
CarvingMaskthe positions one run’s carvers dig, and the CarvingMask.Filter that, beside an old chunk, lets only those near it become blocksChunkStatus.TERRAIN
BelowZeroRetrogenthe separate world-deepening path that rides the same hooksChunkStatus.TERRAIN and ChunkStatus.BIOMES
SerializableChunkDatathe blending_data key: which chunks are flagged, and which measurements survive a savechunk load and save

One measurement, five consumers

BlendingData is gathered once and then read by five unrelated pieces of machinery at three chunk statuses — two inside the density graph, three nowhere near it, and two of the five reading only the old area the chunk was saved with, not its measured columns.

flowchart TD
    OLD["an old chunk, tagged blending_data"] --> BD["BlendingData: sixteen measured columns"]
    BD --> B["Blender: two maps and a carving filter"]
    B --> R1["BIOMES: the nearest old biome, or none"]
    B --> R2["TERRAIN: blend_alpha, blend_offset"]
    B --> R3["TERRAIN: blend_density inside the final density"]
    B --> R4["TERRAIN: carving only near old chunks"]
    BD --->|"its old area, read directly"| R5["FEATURES: border ticks, old chunk only"]

One old chunk’s BlendingData fans out five ways: three consumers go through the Blender and its two maps of measured columns, the carving pass through the filter Blender.of builds from nine chunks’ old areas, and the border ticks read the old chunk’s own old area with no blender at all.

Two maps, not one. Blender.of sweeps the square from seven chunks west to seven chunks east and clips it to a circle — the test is that the squared offsets sum to no more than sixty-four — which leaves 193 positions. Every one that yields data goes into the height-and-biome map; only the inner three by three also goes into the density map. That asymmetry follows the two reaches — the height blend looks twenty-seven quart cells out and the density blend two — and the reaches, not the maps, are why the terrain seam is a hundred blocks wide and the cave seam is a handful.

A neighbour yields data only if it passes two tests in BlendingData.getOrUpdateBlendingData: it carries a BlendingData, and ChunkAccess.getHighestGeneratedStatus is not before ChunkStatus.BIOMES. The first turns away every chunk this version generated, which never carries one. The second turns away a flagged chunk whose status, with any deepening target folded in, is before BIOMES.

Seven chunks is the radius the height blend reaches, which Blender derives from the twenty-seven-cell height range. The cell here is the quart — the four-by-four-by-four volume a biome is stored per (biomes) — and the arithmetic is four quart cells per section across seven sections, less one, plus three, converted back to chunks. It sits inside the radius-eight dependency window the BIOMES and TERRAIN steps both declare, so none of the 193 reads can trip the crash a read past that window would be (a read too far crashes, a read too wide only warns).

Sixteen columns, read out of blocks

BlendingData does not store a copy of the old terrain. It stores sixteen columns in a ring round the chunk’s edge — a column here being one horizontal position sampled through the chunk’s old area, the vertical band between the minimum and maximum sections saved with it — measured out of that same old chunk’s own blocks the first time anyone asks: BlendingData.getOrUpdateBlendingData fetches one chunk and hands it to its own BlendingData.calculateData. Seven of them are inside indices — the corner and three more along the north edge, three along the west — at block coordinates 0, 4, 8 and 12. The other nine are outside indices, sampled at block coordinate 15 along the east and south edges, which in cell arithmetic belong to the next chunk’s first cell. Sixteen slots is what BlendingData.Packed’s codec validates the saved height array against, and it is exactly the array length the class computes from a chunk being four quarts wide.

Which of the sixteen get filled depends on which way the chunk faces new ground. BlendingData.sideByGenerationAge asks each of the eight CompositeDirection.Direction8 neighbours whether it is old, and BlendingData.calculateData fills only the columns on the sides that are not — the ones facing chunks the game is about to generate. There is one call site in the whole game and it passes false, so “sides by generation age” only ever means “sides facing new chunks”. The method also guards on BlendingData.hasCalculatedData, so a chunk object measures itself once and never again: whichever sides were new at that moment are the sides it will carry until it is unloaded.

Each filled column gets three things. A height: BlendingData.getHeightAtXZ starts at the WORLD_SURFACE_WG heightmap (the six heightmaps) if the chunk has one primed, capped at the top of the old area, and at the top of the old area if not, then walks straight down looking for one of eleven block types — podzol, gravel, grass, stone, coarse dirt, sand, red sand, mycelium, a snow block, terracotta or dirt — and returns the first Y at which it finds one, or the bottom of the old area if it never does. A density profile: for each eight-block-tall noise cell, BlendingData reads fifteen consecutive blocks downward, scoring each plus or minus one for whether it is ground, and divides by fifteen — ground meaning not air, not a leaf, not a log, not a mushroom block and with a non-empty collision shape, so a cave counts as air and a tree does not count as terrain. One more pass rewrites the two cells straddling the measured height so the surface lands where the height said it did. And a biome column: one Biome holder per four-block layer of the old area, read straight out of that chunk’s own biome container.

Of those three, only the heights are saved. BlendingData.pack writes the minimum section, the maximum section and the sixteen floats, and omits the heights entirely if none of them was ever measured; the density array is declared transient and the biome list is not in the codec at all. Unload the region and come back and the game re-reads the old chunk’s blocks to rebuild both.

Following one chunk through

sequenceDiagram
    participant CST as ChunkStatusTasks
    participant Blender as Blender
    participant CM as ChunkMap
    participant BD as BlendingData
    participant NBC as NoiseBased<br/>ChunkGenerator
    participant NC as NoiseChunk
    participant BDFS as BlendDensityFunction.<br/>Sampler

    rect rgba(0, 0, 0, 0.04)
    Note over CST,BDFS: ChunkStatus.BIOMES, the blender built on the worldgen executor, the fill forked to the worker pool
    CST->>Blender: of, a blender for this chunk
    Blender->>CM: isOldChunkAround, radius 7, through the region
    CM-->>Blender: yes, from the region files' bitsets
    loop 193 positions, clipped to a circle
        Blender->>BD: getOrUpdateBlendingData
        BD->>BD: measure the sides facing new chunks, once
    end
    Blender-->>CST: a live blender, two maps of BlendingData
    CST->>NBC: createBiomes, with that blender
    NBC->>Blender: getBiomeResolver, wrapped again if the chunk is being deepened
    end
    rect rgba(0, 0, 0, 0.04)
    Note over CST,BDFS: ChunkStatus.TERRAIN, a second blender built the same way
    CST->>Blender: of, the same lookup and sweep again
    Blender-->>CST: a second blender, the one the terrain gets
    CST->>NBC: buildTerrain, with the second blender
    NBC->>NC: the constructor, one NoiseChunk per run
    NC->>Blender: blendOffsetAndFactor, 25 columns, into the sampling context
    loop every point the blend_density node samples
        BDFS->>Blender: blendDensity, when the context holds a live blender
        Blender-->>BDFS: the old density outright, or a lerp toward the noise
    end
    NBC->>Blender: getCarvingFilter, before carved blocks are written
    end

The blender that bends a chunk’s terrain is the one built at ChunkStatus.TERRAIN: the NoiseChunk is made inside that run and takes its answers into the sampling context, while the one built at ChunkStatus.BIOMES answers biomes and nothing else.

Two blenders are built, then, the same way, and each is asked different questions. The biome step builds no NoiseChunk — it samples climate through a caching sampler of its own — so its blender is asked for the biome override and nothing else. The NoiseChunk is made inside NoiseBasedChunkGenerator.buildTerrain, one per run and closed when the run ends, so the blender that reaches the density graph, and the carving filter with it, is the one built at TERRAIN.

The twenty-five columns are the other thing worth noticing, because they are where this page’s measurements physically enter the density graph. Before it binds a single sampler, the NoiseChunk constructor hands its blender the chunk’s volume, and Blender.blendOffsetAndFactor works through the five-by-five grid of quart columns that covers it and returns a Blender.OutputBuffer of twenty-five alphas and twenty-five offsets. The constructor sets the blender and the buffer’s two samplers in the sampling context, under Blender.CONTEXT_KEY, Blender.ALPHA_KEY and Blender.OFFSET_KEY, which is where the two blend leaves and the blend_density node look for them. With no old chunk near it sets none of the three, so the leaves fall back to the constants one and zero and blend_density hands its input through unchanged (wrap: once per chunk).

What the blender actually answers

Three questions, three different shapes of answer, and only two of them are blends. Three is fewer than the five consumers above because the last two are not answered from the maps: the carving filter is built by Blender.of straight from the BlendingData of the chunk and its eight neighbours, the border ticks read the old chunk’s own, and neither of the two maps is involved.

Height, as an alpha and an offset

For each column, Blender.blendOffsetAndFactor first checks whether the sample point sits exactly on a measured column; if it does, it returns alpha zero and the old height, put through Blender.heightToOffset — a rational function, not a polynomial. If not, it walks every measured height in the height map, keeps those within twenty-seven quart cells — a hundred and eight blocks — and averages them weighted by the inverse fourth power of distance, with alpha the smoothstep of the nearest distance over twenty-eight. Nothing in range means alpha one and offset zero: the identity.

Alpha is then used as a mixing weight in NoiseRouterData.splineWithBlending, which interpolates from a fixed target at alpha zero to the real spline at alpha one, and this is where the page’s opening claim comes from. Exactly three router functions are built that way — offset, factor and jaggedness, for each of the three overworld variants — and their targets are, respectively, SimpleDensityFunction.BLEND_OFFSET, NoiseRouterData.BLENDING_FACTOR (the constant ten) and NoiseRouterData.BLENDING_JAGGEDNESS (zero). Against the seam alpha is zero, so overworld terrain is not shaped by its splines at all: the ground height is whatever BlendingData measured, the factor is ten and the jaggedness is nothing. The shipped data pack agrees — blend_alpha appears in nine density function files and blend_offset in three, all of them under the three overworld directories.

Because offset and factor are also the two inputs to NoiseRouterData.preliminarySurfaceLevel, the surface_level the aquifer is configured with (the aquifer), the water table follows the old ground. Nobody wrote a rule for that: it falls out of the aquifer sampling a blended function.

Density, as a lerp with a very short reach

Blender.blendDensity is called per sample by BlendDensityFunction.Sampler, the compiled form of BlendDensityFunction — the wrapper blend_density decodes to — whenever the sampling context holds a live blender. It measures distance in noise cells with the Y difference doubled — noise cells are twice as tall as they are wide — keeps neighbours within two, and mixes toward the noise with an alpha of the closest distance over three. An exact hit returns the old density with no mixing at all. This one wrapper is in every dimension’s router: blend_density is written into four of the seven shipped noise settings and into the three overworld final_density files the other three name, while the alpha and offset nodes are overworld-only.

Biome, as a replacement

Blender.getBiomeResolver wraps the biome source’s resolver in another BiomeResolver — the functional interface the chunk’s biome fill takes (the trace: a chunk’s biomes) — that asks Blender.blendBiome first and only falls through when it declines. And it is not a blend: it finds the nearest measured biome within twenty-seven cells, adds twelve cells’ worth of a fixed shift noise to that distance, divides by twenty-eight, and returns the old biome if the result is below one half and nothing at all if it is above. So the biome boundary is a hard line at roughly half the terrain blending distance, roughened by noise — one biome or the other, never a mixture, which is the only answer a palette of biome holders can represent.

Between them those three shapes are why the blend can still leave a visible edge, in two different ways. A column whose old surface is none of the eleven blocks the height scan recognises reports the bottom of the old area instead, and that bogus height is averaged in with the rest. And because the biome is a threshold rather than a ramp, the material rules can change block for block along a line while the terrain under them is still sloping.

The two consumers that never touch the noise router

Carving is held to the seam. At TERRAIN the blender carries one thing besides its two maps: Blender.of ends with Blender.createAroundOldChunksCarvingMaskFilter, which collects the BlendingData of all eight CompositeDirection.Direction8 neighbours plus the chunk’s own, turns each into a Blender.DistanceGetter measuring distance to a box eight blocks either side of the chunk centre in X and Z — a whole chunk wide — and as tall as that chunk’s old area, and puts the minimum of them behind a CarvingMask.Filter, or builds none if none of the nine is old. A position within four blocks of any such box — after each axis is displaced by the same shift noise, scaled by four — passes it. The carvers never see it: WorldCarver.carveEllipsoid records what they dig in the run’s CarvingMask (carving, and who chooses the block), and NoiseBasedChunkGenerator.applyCarvingMask, turning those bits into blocks, is handed the filter Blender.getCarvingFilter returns and writes only the positions it passes. So beside an old chunk the carvers’ work survives only within about four blocks of it, and the rest is dropped. Neither the filter nor the mask is ever saved.

Leaves and water at the seam are marked for a second look. At FEATURES, after decoration, ChunkStatusTasks.generateFeatures calls Blender.generateBorderTicks — and this is the one hook that acts only on the old side. It returns immediately unless the chunk being generated carries a BlendingData of its own, which a chunk generated fresh today never does: it acts only on an old chunk that is still being carried through the statuses, one saved before FEATURES or one being deepened. Given such a chunk it sweeps four Y levels — one below and one at the bottom of the old area, one at and one above the top — across all 256 columns, and then, for each of the four horizontal neighbours that is not old, walks the whole sixteen-wide face from the bottom of the old area up to that column’s MOTION_BLOCKING height, capped at the top of the old area. Every leaf block and every non-empty fluid it passes goes to ChunkAccess.markPosForPostProcessing. Nothing is changed: the positions are queued for the post-processing pass that runs when the chunk becomes live (what the chunk goes on holding), which is what makes water at the seam flow and orphaned leaves decay instead of hanging there. The heightmap read is safe whatever the chunk’s history: an old chunk loaded at TERRAIN had its four final maps primed as it was read from the save, one that ran TERRAIN here had them primed as ChunkStatusTasks.buildTerrain ended, and ChunkAccess.getHeight primes a missing one on demand besides.

The other passenger

BelowZeroRetrogen is not blending, but it rides the same hooks and is easy to mistake for it. A chunk carrying one is being deepened, whether or not it is also blended: ChunkAccess.getHighestGeneratedStatus folds its BelowZeroRetrogen.targetStatus in (the four shapes a chunk takes), ChunkStatusTasks.buildTerrain calls BelowZeroRetrogen.replaceOldBedrock and then BelowZeroRetrogen.applyBedrockMask if there are holes, and BelowZeroRetrogen.getBiomeResolver wraps the blender’s resolver in one more layer that keeps three cave biomes from the new generation and takes everything else from the biome the chunk already has at Y 0 in each column.

Where to look

Start at Blender.of and read down: the two maps, the two radii, the three answering methods. Then BlendingData.calculateData and BlendingData.getHeightAtXZ for what a measurement is, and BlendingData.Packed for what survives a save. NoiseChunk’s constructor, SimpleDensityFunction and BlendDensityFunction show where the answers enter the density graph, NoiseRouterData which three functions were built to receive them, and ChunkStatusTasks when each hook fires.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Features and placement

Verified against Minecraft 26.3 · Part XII · A chunk decorates: a stream of positions folded through filters, an order every chunk in the dimension already agreed on, and a data pack that can stop the world from opening.

The oak in the middle of a plains chunk was not placed by the plains biome. It was placed by a list that every biome in the dimension contributed to, sorted once per dimension into one order per decoration step, with an index per entry that the random seed for each feature is derived from. That is how the same seed grows the same forest, all but where two chunks’ trees meet (below) — and it is also why two biomes that list the same two features in opposite orders make the world refuse to open. The order is a topological sort of a graph, and a graph can have a cycle.

Decoration is everything the terrain step did not put there: trees, flowers, ores, lakes, patches, springs. The system separates three things a modder usually wants separately — what to build, where to try, and who wants it — and this page is how those three meet at ChunkStatus.FEATURES. The what and the where are registries of dispatched types (the data-driven type pattern), and the who is the biome. The biggest single feature, the tree, has its own page (trees); the terrain that decoration lands on is terrain.

The chain, before anything else

Everything below is one picture: a stream that starts as a single position and is flat-mapped through an ordered list of modifiers until whatever survives it gets built. A realistic chain for a tree runs like this.

flowchart TD
    A["the chunk corner, at minimum Y"] -->|"1 position"| B["RarityFilter rolls"]
    B -->|"1 or 0"| C["CountPlacement copies it"]
    C -->|"N, all alike"| D["InSquarePlacement scatters"]
    D -->|"N, spread out"| E["SurfaceWaterDepthFilter drops some"]
    E -->|"N or fewer"| F["HeightmapPlacement sets Y"]
    F -->|"on the surface"| G["BlockPredicateFilter: would a sapling survive?"]
    G -->|"fewer"| H["BiomeFilter: does this biome list it?"]
    H -->|"fewer"| I["Feature.place, once each"]

One position becomes many and then fewer: only the count multiplies, Y is not set until the fifth modifier, and the shipped plains trees run this chain without its rarity roll.

List order is the meaning — the same seven modifiers in another order place a different forest, or none — and the rest of the page is who assembles that list, who seeds it, and what each link in it is allowed to know.

The cast

classits jobnotes
Featurethe algorithm and its parameters in one object, with one working method: Feature.place, handed the level, the generator, a random source and an origin, and returning whether it succeeded58 types registered into BuiltInRegistries.FEATURE_TYPE; the instances are the data registry Registries.FEATURE, and an instance is the unit a sapling grows
PlacedFeaturea feature plus an ordered list of modifiersthe unit a biome names — the one of the two that owns placement modifiers
PlacementModifierone function from a position to zero or more positions, handed to a consumer, over a PlacementContext18 registered types
FeaturePlacerthe fold: walks a placed feature’s positions through its modifiers depth first, on one stack, and calls the feature at each position the last modifier emitsthe driver builds one per chunk; only its biome-check entry records which placed feature a chain started from
GenerationStep.Decorationthe eleven steps, in order, from raw generation to top-layer modificationa biome’s list is a list of lists, by ordinal
FeatureSorterflattens every possible biome’s per-step lists into one sorted list per step, with an index lookuprun once; ChunkGenerator.featuresPerStep is the memoised field that holds its answer
WorldgenRandomthe seed, reseeded absolutely twice: once per chunk, then once per featureon the worldgen executor

One chunk’s decoration, from the corner outward

sequenceDiagram
    participant ChunkG as ChunkGenerator
    participant WR as WorldgenRandom
    participant FeatP as FeaturePlacer
    participant PMod as PlacementModifier
    participant Feature as Feature

    Note over ChunkG,Feature: ChunkStatus.FEATURES, on the worldgen executor
    ChunkG->>ChunkG: applyBiomeDecoration, write radius 1
    ChunkG->>ChunkG: read featuresPerStep, sorted once and memoised
    ChunkG->>WR: setDecorationSeed, from the level seed and the corner
    ChunkG->>ChunkG: the biomes of the 3x3 chunks, cut to the possible ones
    loop per decoration step, its structures first
        loop per placed feature those biomes list, in index order
            ChunkG->>WR: setFeatureSeed, from the index and the step
            ChunkG->>FeatP: placeWithBiomeCheck, from the chunk corner
            loop until its stack is empty, depth first
                FeatP->>PMod: modify, one position into zero or more
                alt that was the last modifier in the list
                    FeatP->>Feature: place, at each position it emitted
                else an earlier modifier
                    FeatP->>FeatP: push them, each for the next modifier
                end
            end
        end
    end

Two reseedings and a depth-first stack: every feature is reseeded from its own index before it runs, and each position the last modifier emits is one call into the feature’s algorithm.

The driver. ChunkGenerator.applyBiomeDecoration starts at the chunk’s minimum corner, at the world’s minimum Y. There is no eight-block population offset; the scatter comes later, from a modifier.

The seed. A WorldgenRandom is built over a genuinely random seed and then reseeded absolutely, twice, before anything uses it. WorldgenRandom.setDecorationSeed derives a per-chunk seed from the world seed and the chunk corner, and each feature then gets WorldgenRandom.setFeatureSeed from that seed, its index within the step and the step number, which the seed multiplies by ten thousand to keep the steps apart. Features in a step therefore do not share a random stream: every one is reseeded absolutely before it runs, so an extra draw inside a feature perturbs the rest of that feature and nothing after it.

Who wants what. The biome palettes of the surrounding 3×3 chunks are unioned and intersected with the biome source’s possible biomes (biomes). Every placed feature any of those biomes lists for this step (the per-step lists on its BiomeGenerationSettings) is collected by its index in that step’s list, and the indices are sorted — that sort is the execution order, and it is the same for every chunk in that dimension, because ChunkGenerator.featuresPerStep is memoised per generator and built from that generator’s possible biomes. The Nether’s order has nothing to do with the Overworld’s. Within each step, structures at that step are placed before its features (structure placement) — and they share the step loop without sharing the index space, so a structure and a feature sitting at the same index in the same step draw the same feature seed. They differ in reach as well: a structure piece gets an explicit writable box covering the centre chunk, where a feature gets only the softer write-zone check below.

The order is a graph, and a graph can have a cycle

That sort is where a data pack can stop a world from opening, and it is the only place in generation where one can. Every biome’s per-step list contributes this before that edges to one graph, whose nodes are features at their steps, and FeatureSorter.buildFeaturesPerStep topologically sorts it. Two biomes listing the same two features in opposite orders form a cycle, and the sort throws rather than returning an order — it will even re-run itself, dropping one source at a time, to name a minimal offending set.

Where you find out depends on which side you are on. The client calls ChunkGenerator.validate from WorldOpenFlows while opening the world, catches the exception and offers safe mode. A dedicated server never calls ChunkGenerator.validate at all, so there the cycle surfaces later, as a crash report wrapped around the first chunk that tries to decorate.

The fold

FeaturePlacer.placeWithBiomeCheck starts that stream at the chunk corner, at the world’s minimum Y, and flat-maps it through each modifier in turn, depth first on one stack. Nothing about it is a filter chain in the usual sense: a modifier may emit nothing, one position, or many.

Four of the modifiers in that chain are pure filters — RarityFilter at the head, which turns one position into one or none on a roll; SurfaceWaterDepthFilter; BlockPredicateFilter, the survival check; and BiomeFilter — and they are what a PlacementFilter is: a modifier that emits the position it was given, or nothing. Three other things about the chain are load-bearing, and none of them is obvious from a data pack.

A repeating placement does not scatter. CountPlacement, NoiseBasedCountPlacement and NoiseThresholdCountPlacement are RepeatingPlacements: they emit the same position N times, and the scatter is a separate modifier downstream. List order decides the outcome — count-then-scatter gives ten attempts in ten places, scatter-then-count ten attempts at one spot.

Y is set late. Positions travel through most of the chain at the world’s minimum Y; a HeightmapPlacement or a HeightRangePlacement is what puts them on the ground, and a chain that forgets one places at the bottom of the world. WorldGenRegion.getHeight returns the stored height plus one, so a heightmap placement lands on top of the surface rather than in it. Two of vanilla’s heightmap presets read the worldgen heightmaps, which are not among the four ChunkStatusTasks.buildTerrain primes as the terrain step ends (the six heightmaps).

The biome is checked twice. A feature was selected because some biome in the 3×3 wanted it; BiomeFilter re-reads the biome at the scattered position and asks whether that biome’s generation settings contain this exact placed feature. Without it every biome would bleed its trees a chunk in each direction. Vanilla is not consistent about where it goes: the base tree placement ends with the biome filter and the survival-checked variant appends its block predicate after it, while the plains trees put the predicate first, so both orders ship.

What every one of them is handed is a PlacementContext, which carries the level, the generator and — the field the biome filter needs — the placed feature the chain started from, and reads the block state at a position and a heightmap through the level.

Eighteen modifier types are registered, and this page has named ten of them — the seven in the chain and three more beside it. The shapes account for all eighteen. Six implement PlacementFilter — the four in the chain, SurfaceRelativeThresholdFilter, which keeps a position only if its height falls within a range of a heightmap reading, and RandomChancePlacement, which keeps it on a fractional chance. Three are RepeatingPlacements and two set Y. Four move or replace a position: InSquarePlacement scatters within the chunk, OffsetPlacement shifts it, EnvironmentScanPlacement steps up or down to a block a predicate accepts or drops it, and FixedPlacement swaps it for those of the absolute positions it names that lie in its chunk. The last three fit no shape at all: CuboidPlacement fans a position out into a box, RandomlySelectedPlacement runs one of its modifiers at random, and CountOnEveryLayerPlacement, deprecated, does its own scatter and cave-layer scan inside PlacementModifier.modify.

A feature that is a tree of features

Six of the fifty-eight registered features exist only to choose between other placed features or to run them in turn, writing no blocks of their own, which is how a data pack builds decoration out of decoration rather than out of algorithms: RandomSelectorFeature walks a weighted list rolling each entry’s chance and falls back to a default; SimpleRandomSelectorFeature picks a uniform index; WeightedRandomSelectorFeature draws from a weighted list; RandomBooleanSelectorFeature flips a coin between two; SequenceFeature places every entry in order and stops at the first failure, reporting failure itself; and OverlayFeature places every entry whatever the others did, reporting success if any entry succeeded. The plains oak is one of these — a random selector between a fancy oak, a fallen oak and a plain oak with bees. (One more writes nothing and chooses nothing: NoOpFeature is a deliberate blank, and it is what an empty slot in a data pack is spelled as.)

All six reach FeaturePlacer.place, directly or through PlacedFeature.place, and never FeaturePlacer.placeWithBiomeCheck — which is exactly why a BiomeFilter inside a nested placed feature is an error rather than a no-op. The filter needs the context’s top feature, and only the biome-check entry sets it. That is why the “checked” tree placements carry no biome filter and the biome-level ones do.

What a feature may write, and where it may read

ChunkStatus.FEATURES is the only step in the generation pyramid with a positive block write radius, and the radius is one — so a tree may cross into a neighbour, and nothing else in generation may cross into anything (two steps may write, and only two).

What that permission is worth is decided one write at a time. Nothing checks a feature’s origin before it runs; each individual write is checked by WorldGenRegion.ensureCanWrite, which logs — and pauses, in a development environment — and does not write. A canopy that would reach two chunks out is therefore truncated, not moved and not abandoned: half a tree, each refused block one line in the log. Reading is looser and then suddenly much stricter. A read outside the write zone is logged at error level and still happens; a read past the step’s declared dependency radius — eight chunks at this step, and only of chunks at ChunkStatus.STRUCTURE_STARTS — throws instead of loading, which is what makes cascading worldgen structurally impossible rather than merely discouraged (a read too far crashes, a read too wide is only logged).

A radius of one in both directions means a chunk can go on changing after it has decorated: any of its eight neighbours that decorates later may write into it, and nothing in the dependency graph says that is safe. The graph fixes what a step may assume of its neighbours — ChunkStatus.FEATURES asks them only to have reached ChunkStatus.TERRAIN, and no step may ask a neighbour for its own status — not which of two neighbours decorates first, which is whichever generation task reaches it first; so a tree near the border finds the other chunk’s tree already standing, or not, by the order the two decorated in. Nor does the graph say who is inside the chunk at once. What makes it safe is that a dimension’s decoration steps run one at a time on its one worldgen executor, so no two neighbours are ever inside the centre chunk at once (dispatch, and why the parallelism is smaller than the thread names).

The supporting value types are worth naming because they sit on a ladder of three rungs, and each rung is how much world its types are trusted with. On the first, IntProvider and FloatProvider get a random source and nothing else. On the second, HeightProvider.sample and VerticalAnchor.resolveY get a WorldGenerationContext, which is three integers — the world’s minimum Y, its height and its sea level. Only the third sees the world, and two families share it. A BlockStateProvider gets the level as well as a random source and a position, and picks the block to write from them; BlockPredicate extends BiPredicate<LevelAccessor, BlockPos>, which is why a placement can ask what block is under the sapling — and why BlockPredicateFilter is the chain’s general test of what is already there. Every one of those types but VerticalAnchor is its own registry of dispatched types, and every registry is mostly instances: sixteen block predicates, three of them the any_of, all_of and not combinators over the rest; six height providers, most of them distribution shapes over one range; ten state providers, which pick a block from a fixed state, a weight, a noise or a rule, or adjust the one another provider picks. The head of each family is explained once, here; the members are a catalogue this book declines (what this book skips).

Questions players ask

Does a sapling grow the same way a worldgen tree does? No — it skips this whole page. Decoration is one door into a feature and is everything above; /place feature is another, and places a feature outright with no placement layer at all; and a sapling’s growth is a third, beside the other growing blocks, the bonus chest and the End’s own features: SaplingBlock.advanceTree, from a random tick or from bone meal, runs on the Server thread and call Feature.place on the ServerLevel directly, so there is no placement chain, no biome filter and no write guard (WorldGenLevel.ensureCanWrite is an interface default that is always true, and nothing on this path asks it). It also hand-manages the sapling block, which is a story of its own (trees).

Where are the actual features? Fifty-eight feature types are the longest tail in the part — icebergs, geodes, dripstone clusters, lakes, springs, coral, huge fungi, the End spikes — and none of them is a mechanism this page has not already explained: each is one Feature.place working from its own parameters. The book names the framework and declines the catalogue (what this book skips); the two that do something structurally different are named where they bite, OreFeature on chunk anatomy for holding a section open across many writes, and TreeFeature on trees.

For a 1.21-era reader. ConfiguredFeature, FeatureConfiguration and FeaturePlaceContext are gone: a Feature carries its own parameters, and its instances are the data registry worldgen/feature. PlacedFeature.placeWithBiomeCheck is now FeaturePlacer.placeWithBiomeCheck, PlacementModifier.getPositions is now PlacementModifier.modify, and RandomOffsetPlacement is now OffsetPlacement.

Where to look

ChunkGenerator.applyBiomeDecoration is the driver and holds the whole scenario: the two reseedings, the 3x3 biome union and the sorted index loop. Read FeatureSorter.buildFeaturesPerStep next, because it is the sort the hook turns on, with ChunkGenerator.validate beside it, which the client calls while opening a world to meet the cycle early. Then the fold: FeaturePlacer.placeWithBiomeCheck and PlacementModifier.modify are the whole of it, and PlacementFilter and RepeatingPlacement are two of the shapes a modifier can take and HeightmapPlacement an example of a third — BiomeFilter last, since it is the one that needs the context the entry point sets. PlacementContext is what a modifier is trusted with and the arguments of Feature.place what a feature is; reading the two side by side shows that both are handed the same level and generator, and that only the modifier is told which placed feature it serves. Finish at WorldGenRegion.ensureCanWrite, which is where a tree gets truncated, and at RandomSelectorFeature for a feature made of features. One door the page does not open: BlockStateProvider, the family that picks what many features write, a tree’s trunk and leaves among them.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Trees

Verified against Minecraft 26.3 · Part XII · One sapling grows: five pluggable parts over one algorithm, a ceiling the crown’s size was decided before, and the dark-oak sapling that will never grow on its own.

Plant a single dark-oak sapling, try to feed it bone meal, and nothing happens: the bone meal is not even spent, and left alone the sapling never grows. Nothing is wrong with the sapling; there is simply no tree for it to become. TreeGrower holds three weighted lists of features per species — the trees a single sapling can become, the mega trees a 2×2 of them can become, and a list that stands in for the first when a flower is near — and TreeGrower.DARK_OAK fills in exactly one of them, the mega trees. The single-sapling list is left empty, so TreeGrower.growTree finds nothing to place, and TreeGrower.canGrow, which bone meal asks first, reads the same empty list as a refusal unless the sapling is part of a 2×2. The best-known growth rule in the game is implemented as an absence.

Which is a good introduction to this page, because the whole tree kit works like that: the forty-five shipped features that are a TreeFeature are one algorithm with five slots in it, and almost everything you can say about how a cherry differs from a mangrove is a statement about what is in the slots. (TreeFeatures declares fifty-seven keys, because it also holds the fallen trees, the huge mushrooms and the huge fungi, none of which is a TreeFeature.) Features and placement is how a tree gets a position and whether it is attempted at all; this is what happens after Feature.place is entered.

The cast

classits slotwhat varies
TreeFeaturethe algorithm — a record, so final, one implementation, no subclasses — and the nine fields it runs on: five of them are the parts below, three hold BlockStateProviders (features and placement) for the trunk, the foliage and the soil block laid under each trunk column by every trunk placer but UpwardsBranchingTrunkPlacer, and one is the ignore vines flagthe fields, never the algorithm
TrunkPlacerwrites the logs, returns where crowns hang10 registered types
FoliagePlacerwrites the leaves around one attachment12 registered types
RootPlacerwrites roots, and may lift the trunk off the ground1 registered type
FeatureSizethe clearance profile — a horizontal radius per height2 registered types, TwoLayersFeatureSize and ThreeLayersFeatureSize
TreeDecoratorruns afterwards over what was placed11 registered types
FoliagePlacer.FoliageAttachmentthe only channel from trunk to crown: a position, a signed radius nudge, a height nudge no shipped trunk placer sets, and the trunk’s width on each axis, from which is the trunk under me two-by-two is read—

Five of those rows are the slots — trunk, foliage, roots, size and decorators — and every one of the five is a codec-dispatched type in a built-in registry: TrunkPlacerType, FoliagePlacerType, RootPlacerType, FeatureSizeType and TreeDecoratorType. So a data pack composes trees freely and cannot add a new kind of placer (the data-driven type pattern).

One algorithm, five slots

sequenceDiagram
    participant TF as TreeFeature
    participant TP as TrunkPlacer
    participant FolP as FoliagePlacer
    participant RootP as RootPlacer
    participant WGL as WorldGenLevel
    participant TDec as TreeDecorator

    rect rgba(0, 0, 0, 0.04)
    Note over TF,WGL: nothing written yet
    TF->>TP: getTreeHeight, a base plus two draws
    TF->>FolP: foliageHeight, then foliageRadius, from that height
    TF->>TF: out if the tree would leave the build height
    loop the clearance scan, layer by layer
        TF->>TP: isFree, across the profile's radius
    end
    TF->>TF: out if clipped and no minimum allows it
    TF->>RootP: placeRoots, simulated in full first
    RootP-->>TF: false, and the tree is abandoned
    end
    RootP->>WGL: or roots, and the moss above them
    TF->>TP: placeTrunk, with the clipped height
    TP->>WGL: logs
    TP-->>TF: a list of foliage attachments
    loop one per attachment
        TF->>FolP: createFoliage, clipped height and both crown numbers
        FolP->>WGL: leaves, each at distance 7
    end
    TF->>TDec: place, with the logs, leaves and roots sorted by Y
    TDec->>WGL: hives, vines, podzol, propagules
    TF->>TF: updateLeaves, rewriting the distance of every leaf it reaches

Everything in the shaded band happens before a block is written, which is where all three ways out are; below it nothing is undone, and the crown was sized before the scan measured the room.

The two crown numbers are FoliagePlacer.foliageHeight and FoliagePlacer.foliageRadius, and both are what TreeFeature hands to every FoliagePlacer.createFoliage call, beside the clipped height; every block any slot writes goes to the WorldGenLevel the feature was handed. Four things in that diagram are the page’s real content.

The crown is sized before the ceiling is measured. TreeFeature samples the trunk placer’s proposed height, derives FoliagePlacer.foliageHeight and FoliagePlacer.foliageRadius from that number, and only then runs the clearance scan. The scan’s answer, the clipped height, is passed on to TrunkPlacer.placeTrunk and FoliagePlacer.createFoliage — but the two crown numbers travel beside it, already decided. It is a real asymmetry that no shipped tree can express: the only species vanilla lets survive a clipping is the fancy oak, and FancyTrunkPlacer derives its cluster count from the clipped height, so a clipped fancy oak gets a smaller crown, not a bigger one. The one placer whose foliage height reads the tree height at all is SpruceFoliagePlacer, and no spruce declares a minimum clipped height, so a spruce under an overhang abandons itself instead.

Clipping usually kills the tree instead. FeatureSize.minClippedHeight is the only thing that permits a clipped tree at all, and in vanilla exactly one tree declares it: the fancy oak, at four. Every other species abandons itself the moment the scan comes back short. The scan returns two below the first blocked layer, so an obstruction at head height yields a negative number and nothing survives it.

The scan asks the trunk placer what “free” means, not the feature size. FeatureSize supplies only the radius to test. TrunkPlacer.isFree is air, anything in the replaceable-by-trees tag, or an existing log — which is how a new tree grows up through an old one — and it delegates to a virtual TrunkPlacer.validTreePos (tags is where that tag test comes from), so UpwardsBranchingTrunkPlacer quietly widens the definition with its own can grow through block set. A vine anywhere in the scanned column also fails, unless the feature sets TreeFeature.ignoreVines.

Nothing rolls back. There are three places a tree can abandon itself — the build-height check, the clipped-height check, and RootPlacer.placeRoots returning false — and all three happen before a single block is written. After TrunkPlacer.placeTrunk begins there is no undo, and TreeFeature reports success even for a tree that was truncated to a stump.

The trunk placers

The base contract is three numbers — TrunkPlacer.getTreeHeight is a base height plus two independent random draws — and one method that writes logs and returns attachments.

typehow it differs
StraightTrunkPlacerone column, one attachment one block above the top log
ForkingTrunkPlacerleans near the top, then grows a side branch in a second random direction — and if that direction happens to equal the first, the branch is skipped and the draw is spent anyway. The main fork’s attachment carries a radius nudge of +1
GiantTrunkPlacera 2×2 column, four dirt blocks beneath it, and only the (0,0) column placed on the very top layer. Its attachment sets double trunk, as DarkOakTrunkPlacer’s does
MegaJungleTrunkPlacerthe giant trunk, plus side branches laid along a random angle every few levels, each attachment nudged −2
DarkOakTrunkPlacera leaning 2×2 trunk whose lean is two minus a draw from three, so two steps, one or none with equal chance, plus a ring of downward log stubs on a one-in-three roll per position — placed relative to the original trunk, not the leaned one. Its main attachment sits on the top log rather than above it
FancyTrunkPlacersee below
BendingTrunkPlacerrises, nudges once or twice, then walks horizontally for a sampled bend length — and emits an attachment at every position of the arc above a minimum height, including ones where the log was not placed
UpwardsBranchingTrunkPlacera straight column that rolls a probability after each log and, on success, runs a diagonal staircase branch outward, attaching foliage at every branch log. The one placer that widens what counts as free
CherryTrunkPlacerone or two side branches that random-walk toward a computed endpoint, choosing vertical or horizontal per step by the remaining ratio, with the log axis rotated sideways for the horizontal runs — and, when the drawn count is three, a third that is the trunk going on straight up. It derives one more branch-height provider in its constructor that no codec ever sees, which is why the codec insists the declared range spans at least two blocks
PoplarTrunkPlacera straight column with one to four single sideways logs stuck out at one level, a sampled number of logs below the top, and its one attachment a block above those stubs — in a shipped poplar, down inside the trunk, which the crown then wraps. It draws a fresh shuffle of the four directions for every level and uses one

FancyTrunkPlacer is the one worth watching, because it is the only trunk placer that plans before it writes. It works out a crown position per level from a circle equation, then walks the line from trunk to crown twice: once with placement switched off, purely to ask whether every block on the way is free, and again for real only if it was. Each branch’s base sits below its crown, by a fixed share of the distance out, and one that would sit above the trunk top is clamped to it, which is the whole “branches rise as they go out” look. And it carries one computation that cannot do anything: the number of crown candidates it tries per level is a minimum taken against one, over an expression that is never below one, so the answer is always one — and the named density constant that expression multiplies therefore has no effect on any tree of any height.

The foliage placers

A foliage placer gets one attachment and, beside the clipped tree height, which none of them reads, three numbers: a height, a radius, and an offset it samples itself from a configured IntProvider — how far above the attachment its rows start. Its two real degrees of freedom are how the radius changes with height and which positions inside a row it skips, and the skip test is asked in signed coordinates, running from minus the radius to plus it, then folded to absolute values before the block is placed — so a placer that wants to treat one corner differently from its mirror image has to override the signed form, which one placer does, or write rows of its own that never reach it, which one other does.

typehow it differs
BlobFoliagePlacerthe plain oak blob: radius tapers by half the row index, corners clipped on a coin flip and always clipped on the row at y = 0
FancyFoliagePlacerthe blob’s subclass, but the skip test is a genuine circle rather than a corner roll
BushFoliagePlacerthe blob with a much steeper taper — the full row index, not half of it
SpruceFoliagePlacerthe saw-tooth: a radius that grows a block per row and resets to its minimum — nothing the first time, one after — whenever it reaches a ceiling that is itself climbing. The only placer whose offset adds rows rather than lifting the crown
PineFoliagePlacerone cone, and the only placer that overrides FoliagePlacer.foliageRadius — it adds a draw scaled by the trunk height on top of the configured radius
AcaciaFoliagePlacernot a loop at all: three explicit rows at two heights, with a cross cut through the flat plate. Its declared foliage height is a constant zero
DarkOakFoliagePlacertwo explicit rows, or three or four when the trunk is 2×2, wider with it, and the only placer that overrides the signed skip test with a working one — on the widest row of a two-by-two tree it skips the nine positions whose signed coordinates are each −r, r or r + 1 (the grid runs from −r to r + 1): the four true corners and the five one block in from them on the positive side, whose mirror images are kept
MegaJungleFoliagePlacerregistered as jungle_foliage_placer, not mega_jungle. A circle plus a hard Manhattan cap that skips anything seven or more blocks out
MegaPineFoliagePlacerthe only one that iterates absolute world Y, so it can make its taper jagged by widening every other row
RandomSpreadFoliagePlacernever places a row. It fires a configured number of shots at a box, each coordinate the difference of two draws, so the leaves cluster toward the attachment and thin out. Its skip test is unreachable dead code, and it ignores the offset the base class sampled for it
CherryFoliagePlacertwo narrowing cap rows, a stack of full-radius rows, then the only two uses of the hanging-leaves row helper. It punches probabilistic holes: an edge hole on the row at y = −1, the wider of its two hanging-leaves rows, and on every row of radius three or more an unconditional corner removal plus a probabilistic diagonal band
PoplarFoliagePlacera diamond rather than a square: each row keeps what lies within a Manhattan distance of the centre, two diagonally opposite quadrants reaching a block further than the other two, a coin flip per crown choosing the pair, and a configured chance of a hole at each rim position. It runs rows of its own and overrides both inherited skip tests to throw, and it is the only placer that writes logs — a cross of sideways logs laid through one row of a wide enough crown, over leaves it has just placed

Two of the contract’s parameters are dead in all twelve implementations: FoliagePlacer.createFoliage’s tree height, and the TreeFeature that FoliagePlacer.foliageHeight receives. Nobody reads either.

Roots, and the tree that plants itself by failing

RootPlacerType registers one type. MangroveRootPlacer is the only root placer in the game, and the base class exists for it: RootPlacer.trunkOffsetY is what lifts the trunk clear of the mud (one to three blocks for mangrove, three to seven for the commoner tall_mangrove), and RootPlacer.aboveRootPlacement carries the one optional decoration a root gets — AboveRootPlacement, the moss carpet that lands on top of one — while the mangrove’s own parameters sit in MangroveRootPlacement.

It simulates the whole root system before writing anything. Starting from the trunk position it recurses outward in each of the four horizontal directions; each step’s candidates are straight down, sideways, or both, depending on how far out the walk already is and a skew roll. The termination rule reads backwards: the recursion returns true only when it has run out of placeable candidates, and reaching the maximum root length returns false — which propagates all the way out and abandons the tree. A mangrove is therefore planted only where its roots find solid ground before they run out of length.

One asymmetry inside it: a root that lands in mud is written from the muddy provider instead, and that branch skips the base implementation entirely — so muddy mangrove roots never get their moss carpet.

The decorators, and the pass that runs around them

TreeFeature accumulates four sets as it writes — roots, logs, leaves and decorations, each filled through a consumer the feature hands down to whichever placer is writing — and passes the first three to each TreeDecorator as a TreeDecorator.Context, which sorts all three ascending by Y. That sort is the reason five different decorators can say “the lowest log” and mean the bottom of the set — usually the soil block laid under the trunk, which goes into the log set too. A decorator returns nothing, so one that finds no valid spot is indistinguishable from one that succeeded.

Eleven types, in four groups. Seven hang something off the tree, six of them in a space that was empty: TrunkVineDecorator and LeaveVineDecorator (vine curtains), PaleMossDecorator, CocoaDecorator, AttachedToLeavesDecorator — which blacklists an exclusion box around each placement so the propagules cannot crowd each other — and BeehiveDecorator, which puts its nest in an air block beside a log and populates the block entity with two or three bees on the spot (block entities); the seventh, ShelfMushroomDecorator, wants a replaceable spot rather than an empty one, with no water in or beside it, and works a fallen log as well as a standing trunk. One changes a block the tree has already placed: CreakingHeartDecorator shuffles the tree’s logs and converts one that is completely surrounded by other logs — a random such log, not the first. Two write on the ground around the tree: AlterGroundDecorator (the podzol discs under a mega spruce, which reach several blocks beyond the trunk) and PlaceOnGroundDecorator (leaf litter, over an inflated box) — and PaleMossDecorator, besides its hanging moss, lays a patch on the ground at a configured chance (four in five in both shipped pale oaks), a whole moss-patch feature placed at the trunk’s foot whose blocks go into none of the tree’s sets. And one is not a tree’s at all: AttachedToLogsDecorator belongs to FallenTreeFeature.

Then the last of the feature’s own steps, and it is the one that reaches furthest. TreeFeature.updateLeaves runs a bucketed breadth-first walk out from the log set and rewrites BlockStateProperties.DISTANCE on every block with that property it reaches inside the tree’s bounding box. Three consequences follow, and the first two are visible in game. A neighbouring tree’s leaves caught inside the box get rewritten too. Blocks in the prevents-nearby-decay tag report distance zero and act as extra roots for the walk. And the decoration and root sets are marked as occupied before the walk starts, which changes nothing a player sees — no shipped decorator’s or root’s block would carry the walk anyway — and matters to the shape update that follows. Anything the walk cannot reach within six steps keeps the BlockStateProperties.DISTANCE of 7 the foliage provider gave it — already decaying — and falls apart on its first random tick (random ticks).

None of that clearance machinery is shared with the rest of decoration — the scan is TreeFeature’s alone — but the final shape update is StructureTemplate.updateShapeAtEdge, the pass that fixes block shapes at the edge of a placed structure, and every block the four consumers write goes in with one flag word: update neighbours, update clients, and known shape (the flag word).

Five species, side by side

trunkfoliagerootsclearancedecorators
oakstraight, 4 + two drawsblob, radius 2—two layers—
fancy oakfancy, base 3fancy, radius 2, offset 4—two layers, min clipped 4—
dark oakdark oak, 6 + drawsdark oak, radius 0 — all the width is hardcoded in the placer—three layers— (pale oak adds moss, and a creaking heart)
cherrycherry, 7, one to three branchescherry, radius 4, two hole chances and two hanging-leaves chances—two layers— (a 5% bee-nest variant exists)
mangroveupwards branching, per-log branch probabilityrandom spread, 70 shotsmangrove, trunk lifted 1–3 (3–7 for tall_mangrove)two layersvines, propagules, a 1% bee nest

The columns nobody expects to matter are where the personality lives: dark oak’s configured leaf radius is zero, and mangrove’s foliage placer is the one that does not place rows.

Questions players ask

Why does a bone-mealed oak sometimes come out enormous? Because TreeGrower draws the tree from a weighted list — a plain oak most of the time, a fancy oak one time in ten — and separately checks for a flower within a 5×3×5 box, which swaps in the list of bee-nest variants. The same mechanism is why a spruce sapling grown as a 2×2 is a mega pine half the time rather than a mega spruce.

Why does a player-grown pale oak have no creaking heart? TreeGrower.PALE_OAK’s mega tree is the bone-meal variant of the feature, which is the one with no decorators on it at all. The moss and the heart only arrive on a worldgen pale oak.

Why does a mangrove propagule grow underwater? Sapling growth is the other entry into this machine: SaplingBlock hands off to TreeGrower.growTree, which hand-manages the saplings on the way in. Whether it grows one sapling or a 2×2 of them, it replaces each with whatever the fluid there would be, so a waterlogged propagule grows into water, and it puts them all back if the feature fails.

Do leaves know which tree they came from? No. Nothing in the placed tree records which feature grew it; a leaf’s only per-block state is BlockStateProperties.DISTANCE, BlockStateProperties.WATERLOGGED and the persistent flag, the first written by the feature’s own breadth-first pass and the second taken from what was already in the world. A log carries a BlockStateProperties.AXIS the placer that wrote it chooses, and that is the whole of it. FoliagePlacer.tryPlaceLeaf also refuses to overwrite a leaf a player placed, by testing the persistent flag.

For a 1.21-era reader. TreeConfiguration is gone: TreeFeature is a record that holds the nine fields itself, and each shipped tree is one TreeFeature in Registries.FEATURE, with no configured feature around it. TreeGrower’s secondary chance and its six optional slots are gone too; three weighted lists do their work.

Where to look

TreeFeature.place is short and is the whole algorithm: the two crown numbers taken before the scan, the scan, the three abandonment points, and the four sets. Read TreeFeature.CODEC beside it for the nine fields, and FeatureSize.getSizeAtHeight with TwoLayersFeatureSize for the clearance profile the scan tests against. Then one placer per slot, and the most instructive are not the simplest: TrunkPlacer.isFree for what free means and who gets to widen it, FancyTrunkPlacer for the only trunk placer that plans before it writes, FoliagePlacer.createFoliage with FoliagePlacer.shouldSkipLocation for the row-and-skip contract and DarkOakFoliagePlacer for the one working override of it, and MangroveRootPlacer.placeRoots for a recursion whose success condition reads backwards. Finish at TreeFeature.updateLeaves, which reaches further than anything else on the page, and at TreeGrower.growTree for the other door in. One door the page does not open: TreeFeatures, where all fifty-seven shipped keys are declared in one file.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Structure placement

Verified against Minecraft 26.3 · Part XII · A village is decided: a lottery that never looks at the world, a layout that is deferred and then run by the method that deferred it, an absence stored as a hole, and a command that generates chunks to answer a question.

Type /locate structure #village and the answer usually comes back at once, from a couple of thousand blocks away, in a direction you have never been — naming a chunk nobody has visited. It can do that because whether a village could be here is pure arithmetic on the world seed. No biome is consulted, no terrain is sampled, no chunk is read. Divide the chunk coordinates by a spacing, seed a random source from the level seed and the grid cell, draw two offsets, and compare. Everything the world has a say in — the biome, the ground height, whether the layout fits — happens afterwards, and can still say no.

A structure is a thing the generator decides to build at a place rather than from it. This page is the framework all sixteen structure types share: the decision, the caching, the reference scan, the way terrain bends around it, and the moment blocks are finally written. What builds the pieces is one of two assemblers — jigsaw and templates for villages and their relatives, hand-built structures for the other fifteen types.

The cast

classthe decision it ownswhen
StructureSetwhich structures share a grid, with weights, and which StructurePlacement lays that grid outdata pack, Registries.STRUCTURE_SET
StructurePlacementwhere the grid falls, dispatched through BuiltInRegistries.STRUCTURE_PLACEMENT, which holds each kind’s MapCodec, like any data-driven type (the pattern). RandomSpreadStructurePlacement is the spacing-and-separation lottery; ConcentricRingsStructurePlacement is strongholdsworld start, then per chunk
ChunkGeneratorStructureStatewhich sets are possible in this dimension at all, and the stronghold ring positionsonce per dimension — the filter on the Server thread, the ring searches on the worker pool
Structurethe settings wrapper: allowed biomes, spawn overrides, the decoration step, the terrain adjustment — and Structure.findGenerationPoint. Its StructureType is what the sixteen concrete subclasses are registered asRegistries.STRUCTURE, then ChunkStatus.STRUCTURE_STARTS
StructureStartthe answer: a structure, the chunk it started in, a PiecesContainer, a reference count and a cached boxstored on the chunk
StructureManagerthe per-level view of starts and references. Two unrelated things carry that shape of name and both are reached from the level: this one is ServerLevel.structureManager, while ServerLevel.getStructureTemplateManager returns the server’s .nbt template loader (jigsaw and templates)worldgen and the Server thread
StructureCheckthe presence cache — three caches over a partial-NBT reader — and the thing /locate asksServer thread only, unsynchronised
Beardifierhow much the terrain bends, as a density termbuilt with the NoiseChunk at ChunkStatus.TERRAIN

Five decisions, on five different clocks

flowchart TD
    W["world start: filter the sets, place the rings"]
    W -->|"after EMPTY"| S1["STRUCTURE_STARTS: the lottery, then the layout"]
    S1 -->|"next status"| S2["STRUCTURE_REFERENCES: scan the 17x17 around"]
    S2 -->|"BIOMES"| N["TERRAIN: the Beardifier bends the field"]
    N -->|"next status"| F["FEATURES: the pieces write their blocks"]

Five decisions, one at world start and four on the chunk-status ladder, with the statuses between them on the arrows: a structure is decided at the second status and writes no block until the biome and terrain statuses are past.

The odd thing about that ladder is where it starts. ChunkStatus.STRUCTURE_STARTS is the second status a chunk passes through, two before ChunkStatus.BIOMES (the pyramid, drawn is the ladder and the dependency rules it enforces) — so a structure is decided before the biomes and the terrain it will sit in exist. Everything the structure needs to know about the world it asks for directly, from the generator, rather than reading it out of a chunk.

Which chunk: arithmetic, and the two places a biome still gets in

ChunkGenerator.createStructures walks the possible structure sets. For a village that means RandomSpreadStructurePlacement.getPotentialStructureChunk: divide the chunk coordinates by the spacing, seed a WorldgenRandom from the level seed, the grid cell and the set’s own salt, and draw two offsets inside the cell. This chunk is the village chunk only if the draw lands exactly here. RandomSpreadType decides whether the draw is uniform or triangular, and AbstractSpreadingStructurePlacement — the class both placement types the shipped sets use extend — adds in AbstractSpreadingStructurePlacement.isStructureChunk a frequency roll, an AbstractSpreadingStructurePlacement.FrequencyReductionMethod and a deprecated AbstractSpreadingStructurePlacement.ExclusionZone that lets one set repel another.

Two qualifiers, and both matter. The other placement type the shipped sets use is not like this at all: ConcentricRingsStructurePlacement positions strongholds by asking BiomeSource.findBiomeHorizontal for real biome positions, on the worker pool, at world start. And even for villages a coarse biome test has already happened once — when ChunkGenerator.createState built the ChunkGeneratorStructureState and dropped every set no biome in this dimension can host.

Which structure, and whether the biome allows it

The set has entries with weights, so a second WorldgenRandom picks one. Then Structure.findValidGenerationPoint runs Structure.findGenerationPoint and filters it through Structure.GenerationContext.isValidBiome — the biome is sampled at the proposed point, after the lottery has already chosen the chunk.

If that fails — most often the biome, but also an empty start pool, a missing start jigsaw, or a structure that would sit too close to the world height limits — the entry is removed, its weight subtracted, and the roll repeated. A village slot whose first pick fails its biome test therefore still gets whichever of the other villages the ground allows, where a single-candidate set would get none. If every entry fails, the loop drains and the cell stays empty: the slot still exists, the village does not.

The layout is deferred, and the centre is not

For fifteen of the sixteen types, Structure.findGenerationPoint does not return pieces. It returns a Structure.GenerationStub, and the stub holds the child expansion as an unexecuted consumer. Structure.generate calls Structure.GenerationStub.getPiecesBuilder as soon as the stub has passed its biome test, so on the generation path the deferral lasts no time at all. What the deferral is actually for is StructureCheck.canCreateStructure, which calls Structure.findValidGenerationPoint and asks only whether the result is present. So a presence question costs a centre and never a layout: the child expansion runs once, on the chunk that is really generating, and never for the hundreds of candidate chunks a search walks past. The sixteenth is the mineshaft: MineshaftStructure.findGenerationPoint builds its whole piece graph before it returns, so a map that hunts a mineshaft lays one out, on the Server thread, for every candidate in unexplored ground that passes the set’s frequency roll (hand-built structures).

For a jigsaw, what is not deferred is the centre: the start template, its rotation and its ground height are all resolved before the stub comes back — which is why a presence question is not free either, merely much cheaper than a village.

One name in this package reads as the framework for all of that and is reached by nothing: PostPlacementProcessor. The live post-placement hook is Structure.afterPlace.

The presence cache, and the hole that proves an absence

StructureCheck is the cache in front of all of this, and it is three caches over a partial-NBT reader: chunk → structure → reference count (which is what makes “unreferenced only” searches possible), structure → chunk → would-generate, and a capped set of the chunks storage held no starts for, which spares the disk a second read of them. On a miss it reads the chunk off disk through ChunkScanAccess, which answers a question about a chunk without loading it and is one of the three joins that block the Server thread (the three places that do wait), pulling only the data version and the structure starts — a field-selected read that never builds the rest of the tag (a whole file need not be read) — and data-fixing that fragment alone. It is Server-thread-only and unsynchronised, which is why both routes in hop to the Server thread to feed it, through ServerLevel.onStructureStartsAvailable: ChunkStatusTasks.generateStructureStarts on the worldgen executor when a chunk generates its starts, and the loading pyramid’s own ChunkStatusTasks.loadStructureStarts when a chunk arrives from disk with starts already in it.

Absence is stored as a hole, not as a marker. An invalid start is never written at all: ChunkGenerator.tryGenerateStructure calls StructureManager.setStartForStructure only for a valid start, and StructureStart.INVALID_START is dropped on the floor. What lets a partial scan prove absence is that every saved chunk carries a starts compound unconditionally, empty or not — so a structure simply missing from that map is a definite “not here”. The INVALID ids that do turn up in old saves are legacy, and StructureCheck skips them while loading.

Which chunks have to be told

At ChunkStatus.STRUCTURE_REFERENCES, ChunkGenerator.createReferences scans the 17×17 chunk square around each chunk and records the packed position of every start whose bounding box overlaps it. Discovery is outside-in: a village never walks its own pieces to announce itself, and this is why the steps that read structures — this one, which reads the starts of the 17×17 around it to write the references, and TERRAIN and FEATURES, which read starts back through them — require structure starts eight chunks out; BIOMES, between them, reads none and declares the same radius, so four of the ten do.

The box that scan tests is not always the box the assembler produced. Structure.adjustBoundingBox inflates it by twelve blocks on every side the moment TerrainAdjustment is anything but none — and that inflated box is what the reference scan, StructureManager.getStructureAt and every spawn override that asks for the full box see. The margin the beardifier needs is therefore also the margin in which a pillager outpost counts as “here” for its spawn override; a village declares no spawn override at all. A 128-block cage keeps a 17×17 scan sufficient, and it is enforced when the data pack loads, not when the structure generates: a jigsaw whose maximum distance plus the terrain margin exceeds JigsawStructure.MAX_TOTAL_STRUCTURE_RANGE fails validation.

The ground bends before the ground exists

Beardifier.forStructuresInChunk reads those references and turns the nearby pieces into Beardifier.Rigid boxes plus their junctions. It is built with the NoiseChunk at the start of NoiseBasedChunkGenerator.buildTerrain, the worker-pool job behind ChunkStatus.TERRAIN (what each status hands on), and rides in its sampling context under Beardifier.CONTEXT_KEY — so the beardifier is in place before the first sample of the density field it bends. No blocks are edited. The flat shelf under a village is a term added to the scalar field before anything samples it, and TerrainAdjustment picks the shape: for its pieces, only two of its five values use the kernel the name beard refers to — the two beard modes. Every jigsaw junction uses that kernel too, whatever the mode, at half the weight a beard-mode piece gets, which is where the smooth shoulders under village streets come from. For their pieces, bury and encapsulate use a plain linear distance falloff instead. The rigid filter applies only to jigsaw pieces, which have a projection to test; a hand-built piece contributes unconditionally.

Many structures never reach any of that. A structure that names no terrain_adaptation defaults to TerrainAdjustment.NONE and is filtered out before its pieces are looked at, and that is twenty-three of the fifty-two shipped structure files — the desert pyramid and the mineshaft among them, which is why neither leaves a shelf. The hand-built pieces that do reach the branch belong to the stronghold and the nether fossil.

Then the blocks arrive, one chunk at a time

At ChunkStatus.FEATURES, ChunkGenerator.applyBiomeDecoration places structures at their declared decoration step, before that step’s features — inside the same step loop that runs decoration (the trace: a chunk decorates). StructureStart.placeInChunk derives a reference position from piece zero — piece order is semantic, not cosmetic, and every PosRuleTest measures its distances from that point — and calls StructurePiece.postProcess on every piece overlapping this chunk’s writable area. Every chunk the structure touches does this with its own box, so a house straddling four chunks is written in four slices, at four different times, and a piece’s StructurePiece.postProcess must write only inside the box it is handed.

What /locate asks, and what it answers with

Occasionally the command stops for a second before answering, and that is the search’s disk reads and chunk generation being paid for on the Server thread. StructureCheck re-runs the start-point and biome test — the grid arithmetic having already produced the candidate chunk — and answers StructureCheckResult.CHUNK_LOAD_NEEDED for every candidate in unexplored terrain that passes it, and the search then loads that chunk to structure starts, synchronously, out to a radius of a hundred grid cells for the command and a cartographer’s map, and fifty for a dolphin and a map found in a chest. The eye of ender passes a hundred too, but its one target is the stronghold, and a ring placement’s search takes no radius: it tries each ring position nearer than the best found so far. An eye of ender, a dolphin and an explorer map all reach the same code and can all pay the same pause.

What comes back is not the structure. ChunkGenerator.findNearestMapStructure returns StructurePlacement.getLocatePos, which is the start chunk’s minimum block plus the placement’s own offset, and the eye of ender takes the same answer. A stronghold’s portal room can be well over a hundred blocks from it, and the stronghold does keep a pointer at the room that the search never reads (hand-built structures).

An exploration map asks a sharper version of the same question, and the reference count is what makes it answerable. StructureStart.getMaxReferences is one, and ExplorationMapFunction defaults skip_existing_chunks to true, so a map asks for an unreferenced structure and takes a reference when it finds one — which is exactly what the first of StructureCheck’s three caches stores, and why two maps usually do not send two players to one monument. It is a default and not a guarantee: the three buried-treasure tables, in shipwrecks and both ocean ruins, set the flag to false and will happily send both.

Where to look

Structure.generate is the entry and the whole shape in one method: it calls Structure.findValidGenerationPoint, which is the lottery’s answer filtered by biome, and hands back the stub. Read Structure.StructureSettings beside it for what a structure declares. Then the placement side — StructureSet, AbstractSpreadingStructurePlacement.isStructureChunk and RandomSpreadStructurePlacement.getPotentialStructureChunk — which is the grid arithmetic, with ConcentricRingsStructurePlacement as the shipped one that does not work that way. ChunkGenerator.createStructures and ChunkGenerator.createReferences are the structure’s first two chunk statuses, in that order, and StructureStart.placeInChunk is its last. StructureCheck.checkStart is the cache, and ChunkScanAccess under it is the partial read that makes it cheap. Finish at Beardifier.forStructuresInChunk and TerrainAdjustment, which are how the whole arrangement reaches the terrain. Two doors the page does not open: BuiltinStructures and BuiltinStructureSets, where the keys of the fifty-two shipped structures and their twenty-one grids are declared.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Jigsaw and templates

Verified against Minecraft 26.3 · Part XII · A village assembles itself: pieces that find each other through connector blocks, a priority queue instead of a stack, and a growth limit that takes the right pool away a piece before it stops the queue.

A village stops somewhere. Follow a street out from the town centre and the houses run out and the path ends in a stub of dirt path with nothing on it. Nothing measured the street and nothing counted the buildings. That stub is a terminator, and it comes from the street pool’s fallback pool — which JigsawPlacement.Placer appends to the candidate list at every depth, behind the pool the piece asked for. A street ends wherever the street pieces stop fitting. What the depth limit — a size the structure declares, six for a village against the twenty JigsawStructure.MAX_DEPTH allows — does is stop offering the asked-for pool at all, so at the limit the fallback is the only thing left, and stop queuing whatever a piece there places. The edge of a village is a substitution first, and a stop condition only one piece further out.

This is the assembler one of the sixteen structure types uses — the jigsaw — together with the .nbt template system that turns each of its pieces into blocks, which is also what a structure block reads and writes. Everything outside the assembler is structure placement: the lottery that chose this chunk, the reference scan, the beardifier, and the moment StructurePiece.postProcess is called. The other fifteen types use a different assembler and reach this page only for the templates (hand-built structures).

The cast

classwhat it holdswhen
JigsawStructureten data-pack fields: the start pool, the start jigsaw name, a depth, a start height, a heightmap to project onto, a maximum distance, the pool aliases, the expansion hack, the dimension padding and the liquid settingsdata pack, Registries.STRUCTURE
StructureTemplatePoola weighted list of StructurePoolElements and a fallback pool — the two fields its codec hasdata pack
StructurePoolElementone candidate: a single template, a legacy single, a list, a placed feature, or nothing — a dispatched type like the processors and the rule tests below (the pattern)data pack
JigsawPlacement.Placerthe body of the assembly loop, and the priority queue that loop drains; the VoxelShape of free space (shapes and collision) travels with each queue entryChunkStatus.STRUCTURE_STARTS, on the worldgen executor
JigsawBlockthe connector. Five things are written on one: its own name, the target name it looks for, the pool to draw a neighbour from, the final state it turns into once the assembler is done with it, and a JigsawBlockEntity.JointType deciding whether rotation must match, which matters only on an up- or down-facing one. Two priorities sit beside them, belowin the template
PoolElementStructurePieceone accepted candidate, with its junctionsbuilt at ChunkStatus.STRUCTURE_STARTS, saved with the chunk
StructureTemplatea parsed .nbt file: block palettes, entities, and the jigsaw blocks in itloaded by StructureTemplateManager
StructurePlaceSettingsrotation, mirror, the chunk box, liquid handling and an ordered list of StructureProcessorsper piece, per chunk

A village assembles, from town centre to blocks

sequenceDiagram
    participant ChunkG as ChunkGenerator
    participant JS as JigsawStructure
    participant JP as JigsawPlacement
    participant JPP as JigsawPlacement.<br/>Placer
    participant PESP as PoolElement<br/>StructurePiece
    participant STemp as StructureTemplate

    Note over ChunkG,STemp: ChunkStatus.STRUCTURE_STARTS, on the worldgen executor
    ChunkG->>JS: Structure.generate, the lottery already won
    JS->>JP: addPieces, from findGenerationPoint
    JP-->>JS: a town centre on the ground, and a stub
    JS->>JP: the stub's consumer, once the biome test passes: the free-space shape
    JP->>JPP: tryPlacingChildren, on the centre first
    loop until the priority queue drains
        JPP->>JPP: a piece's jigsaw blocks, shuffled, then by selection priority
        alt below the limit
            JPP->>JPP: the target pool's elements, then the fallback's
        else at the limit
            JPP->>JPP: the fallback's elements only
        end
        JPP->>JPP: the attach test
        JPP->>ChunkG: getFirstFreeHeight, unless both pieces are rigid
        JPP->>JPP: collide with the free shape, then subtract the box
        JPP->>PESP: addJunction, on the parent and on the child
        JPP->>JPP: queue the child by placement priority, unless its parent was at the limit
    end
    JS-->>ChunkG: a StructureStart holding every piece
    rect rgba(0, 0, 0, 0.04)
    Note over ChunkG,STemp: ChunkStatus.FEATURES, once per chunk the village touches
    ChunkG->>PESP: postProcess, through StructureStart.placeInChunk
    PESP->>STemp: placeInWorld, through its pool element, clipped to this chunk
    end

A village is a queue drained in memory at the second status and written chunk by chunk at ChunkStatus.FEATURES; at the depth limit a piece is still queued, and only the fallback pool is offered to it.

The alternative box and the loop’s last arrow are the page’s opening in one frame: nothing in the loop counts houses or measures a street, and the depth limit changes two things: which pool a jigsaw block is offered, and whether what a piece at the limit places is queued at all, which it is not. JigsawPlacement.addPieces does the centre and the stub, Structure.GenerationStub.getPiecesBuilder runs the stub once it has passed its biome test, and JigsawPlacement.Placer.tryPlacingChildren does everything inside the loop; the sections below take the loop’s steps in order.

The pools

A StructureTemplatePool is the unit of choice, and its codec has exactly two fields — all 245 shipped pool files carry those two and nothing else. The weighted elements list is the candidates. The fallback pool is a second pool appended behind them, tried whenever nothing in the first list fits — unless that list holds an empty element, which ends the search wherever the shuffle put it, fallback and all — and the only thing tried at the depth limit. The third thing you might expect on the pool is not there: StructureTemplatePool.Projection is a field of each element, so one pool can mix them. It decides how Y is chosen: rigid keeps the parent piece’s vertical offset when the parent is rigid too, and terrain matching asks the generator for the ground height and brings a gravity processor with it.

Five kinds of element can sit in that list. SinglePoolElement is one template. LegacySinglePoolElement is the same with a wider ignore rule, which drops the template’s air. ListPoolElement is several placed as a unit. EmptyPoolElement is a deliberate nothing. And FeaturePoolElement places a PlacedFeature instead of a template — which is how village trees arrive (features and placement).

PoolAliasBinding and PoolAliasLookup sit on top: one structure can swap which pool a name resolves to, per placement, resolved once from a positional random source before the assembly loop starts. PoolAliasBindings registers three kinds — DirectPoolAlias names a replacement outright, RandomPoolAlias draws one from a weighted list, and RandomGroupPoolAlias draws a whole group of substitutions together so they agree. Trial chambers are the only vanilla structure that uses any of them, and what they swap is which pool the spawners draw their mobs from.

The assembly loop

JigsawPlacement.addPieces builds a VoxelShape of free space around the start piece and hands it to JigsawPlacement.Placer. From then on the algorithm is: take a placed piece, look at its jigsaw blocks, and for each one try to hang something off it.

Five details in that loop are the ones worth watching.

It is a priority queue, not a stack, and two different priorities steer it. Within one piece, its jigsaw blocks are shuffled and then sorted by selection priority, highest first, so a mapmaker can decide which connector on a house gets first refusal. Across pieces, JigsawPlacement.Placer.placing orders the queue by the source jigsaw block’s placement priority, with insertion order breaking ties — not depth-first — so a template can have the piece hung on one of its connectors expanded before everything queued at a lower priority.

Attachment is a name match plus a geometry match. JigsawBlock.canAttach requires the two jigsaw blocks to face each other and the source’s target name to be the candidate’s name, or the candidate to have none; an aligned joint additionally requires the rotations to match, while a rollable one does not — a difference only an up- or down-facing pair can show.

The ground decides what fits, without a chunk being read. Whenever the source or the target piece is not rigid — which is every village street — JigsawPlacement.Placer asks ChunkGenerator.getFirstFreeHeight for the ground under the source’s jigsaw block and puts the candidate’s box there, and that box — raised first by the expansion hack where the structure sets it (below) — is what the collision test below then tests. ChunkGenerator.getFirstFreeHeight samples the density graph rather than reading blocks (density functions), which is the whole reason the assembly can run at ChunkStatus.STRUCTURE_STARTS, before any terrain has been written.

Collision is against a shrinking shape, not against a list. One VoxelShape of free space is built around the start piece and then travels down the queue inside each JigsawPlacement.PieceState, and every accepted piece subtracts its own box from it — so the next candidate is tested against what is genuinely left. A candidate that intersects is simply not built, and the candidate’s next connector is tried, then its next rotation, then the next candidate. There is one branch: when a jigsaw block points into its own piece’s box, the child is tested against a shape of that piece’s box instead of the shared one, and every inward child of the piece shares it — which is how a village street carries its houses, whose entrances point into the street’s own box.

A junction is recorded on both sides. Each connection writes a JigsawJunction into the parent piece and the child, which is what lets Beardifier treat junctions as their own terrain contribution rather than inferring them from the boxes (structure placement).

The boxes drawn round the loop

Of the ten data-pack fields on JigsawStructure, three are spatial limits on everything above — and none of them is the depth limit, which is the size field the hook turns on. JigsawStructure.MaxDistance is the horizontal and vertical reach from the centre, and the free-space shape is built to exactly that box — so a piece the queue would otherwise accept is refused for being too far out. It is also the field the data-pack load validates: JigsawStructure.MAX_TOTAL_STRUCTURE_RANGE is 128, and a horizontal reach plus the twelve blocks a terrain adaptation needs must fit inside it, or the pack fails to load rather than generating a structure the 17×17 reference scan could miss. DimensionPadding shrinks that box at the top and the bottom, in blocks, and rejects the start piece outright if the centre itself will not fit inside the padded world — which is how a structure declines to generate near the build limits instead of being clipped by them. And the expansion hack stretches a candidate at most sixteen blocks tall upward before the collision test, until it has room above its floor for the tallest piece the pools its own inward-facing jigsaws point at could need — in a village, a street raised for the houses that will stand inside its box. A street that fits can therefore be rejected for the houses it would carry.

One more field is not about space at all. LiquidSettings decides whether the blocks a piece writes keep the water that was already there — apply_waterlogging re-floods what it can, ignore_waterlogging leaves the piece dry — and apply_waterlogging is the default, overridable per element as well as per structure.

The shipped data is the clearest thing about the last three of those — the expansion hack, the padding and the liquid settings. Of the fifty-two structure files the game ships, twenty-four turn the expansion hack on (the five villages, the pillager outpost and the eighteen abandoned camps), and exactly one sets either of the other two: trial chambers, which pads ten blocks off the top and the bottom because it generates deep and must not punch through, and turns waterlogging off because a flooded chamber is not the room it was drawn as. No shipped pool element overrides the liquid setting at all.

The processors, and what the shipped lists use

The processors are the interesting layer, because they are shared with the other assembler and with the structure blocks a player can use. A StructureProcessorList is the data-pack object — a registry element that is just an ordered list of them — and each entry is a dispatched type (the pattern).

RuleProcessor applies ProcessorRules, and each rule holds two block tests with different subjects: an input predicate against the template’s own block and a location predicate against the block already in the world. Both are RuleTests, a family of ten ways to match a block — by state, by block, by tag, by height, or by a block or a state on a random roll, with always_true and the all_of, any_of and not combinators over the rest — and a PosRuleTest follows, its own family, measuring position against the reference the structure’s piece zero fixed. A replacement state and an optional RuleBlockEntityModifier come last, and the first rule that matches wins. BlockRotProcessor deletes a fraction of the blocks. GravityProcessor drops them to a heightmap. ProtectedBlockProcessor refuses to overwrite anything in a tag. CappedProcessor lets another processor change at most a sampled number of the blocks. BlockIgnoreProcessor skips a named list of blocks, and its three presets name the structure block, air, or both — never structure void, which never reaches a template in the first place, because a structure block excludes it when it saves. And JigsawReplacementProcessor is the one that cleans up after the assembler: it swaps each jigsaw block for the state named in its final-state string, or removes it entirely. The assembly graph is invisible in the finished village unless SharedConstants.DEBUG_KEEP_JIGSAW_BLOCKS_DURING_STRUCTURE_GEN is set.

Which of those a data pack uses is a much shorter list than the registry. Forty processor lists ship, and between them they name four types: rule thirty-five times, protected blocks seven, block rot six, capped four. Gravity and jigsaw replacement never appear in one, because the projection and the assembler add them; the ruined portal’s stack never appears either, because it is assembled in Java (hand-built structures).

From a piece to blocks

Nothing above has written a block. When ChunkStatus.FEATURES finally calls StructurePiece.postProcess on a PoolElementStructurePiece, the element assembles a StructurePlaceSettings — the chunk box, the rotation, the ignore processor, then JigsawReplacementProcessor, then the element’s own processor list, then the projection’s. LegacySinglePoolElement, which is what every vanilla village piece is, then pops its ignore processor and re-appends a wider one at the end of that list, so for the pieces a player sees the ignore step runs last and drops the template’s air as well as its structure blocks. StructureTemplate.placeInWorld runs every block in the template through StructureTemplate.processBlockInfos.

A StructureTemplate is a parsed .nbt file: StructureTemplate.Palettes of StructureTemplate.StructureBlockInfo, an entity list, and StructureTemplate.JigsawBlockInfo for the connectors. Placing it writes what falls inside the box, loads block-entity data (block entities) and stamps a fresh loot seed into containers rather than a table’s contents (loot tables) — which is why a village chest’s contents are decided when you open it, not when the village generated.

Where a template comes from

StructureTemplateManager loads templates in a fixed order — the world’s generated directory, then the gametest source when one is set, then data packs — through two or three TemplateSources: a DirectoryTemplateSource reading files off disk, a second one parsing the .snbt text form the game tests keep, and a ResourceManagerTemplateSource reading the pack stack like any other resource. TemplatePathFactory is what turns an id into a path when a template is saved. The first source that answers wins, which is what lets a structure block’s saved file shadow a data pack’s, and the folder every one of them looks in is structure, singular.

That first source is also the seam with the block a player can use. A structure block in save mode calls StructureTemplate.fillFromWorld over its box — excluding structure void, which is how a saved template gets its holes — and, saved from its screen, writes the result into the world’s generated directory; a redstone pulse only replaces the copy the template loader holds, which generation reads all the same. In load mode it hands a StructurePlaceSettings to the same StructureTemplate.placeInWorld a village piece uses, with rotation, mirror, an integrity roll that is a BlockRotProcessor, and a seed. /place template is the command form of the same call. StructureBlockEntity holds all of that state, and ServerboundSetStructureBlockPacket is how the screen edits it; the jigsaw block’s editor is the same arrangement one class along.

Questions players ask

Why is one village bigger than another? Because the size field is an outer bound and almost never the thing that stops a branch. What usually ends one is every candidate in the street pool failing the collision test, which hands the branch to the fallback’s terminators — so a street that happens to run downhill into free space grows further than one that turns back on itself. The expansion hack sharpens it: a piece that would fit is rejected for the children it would need room for.

Can I watch the assembler run? Yes, two ways, and a third only a developer sees. The jigsaw editor runs in both directions — ServerboundSetJigsawBlockPacket and ServerboundJigsawGeneratePacket let a creative-mode operator run the assembler live against a loaded ServerLevel, and JigsawBlockEntity syncs its fields back the other way — and /place jigsaw does the same from a command, with a pool, a target and a depth. Neither sends the client anything shaped like a structure: it sees the jigsaw block’s fields and the blocks as they are written. The third is DebugSubscriptions.STRUCTURES, which ships every piece’s bounding box to the debug renderer (debugging the running game).

Why do the same houses appear in different rotations? Because rotation is chosen per piece and applied to the block states as they are written, not to a pre-rotated template. Whether a neighbour on an up- or down-facing connector may differ in rotation is the joint type’s decision; two side-facing connectors fix it by facing each other.

For a 1.21-era reader. A structure block now saves into the world’s generated/<namespace>/structure/, not structures/ — the singular folder a data pack’s templates were already read from.

Where to look

JigsawPlacement.addPieces sets the start piece and the free-space shape; JigsawPlacement.Placer under it is the loop’s body, and reading its one long method straight through is the page — the shuffle, the two priorities, JigsawBlock.canAttach, the collision test, the junction on both sides. JigsawBlock.canAttach and JigsawBlockEntity.JointType are the attachment rule in isolation. StructureTemplatePool is two fields and worth opening for that alone, with StructurePoolElement and SinglePoolElement for what a candidate is and StructureTemplatePool.Projection for how Y is chosen. Then the writing half: StructureTemplate.placeInWorld and StructureTemplate.processBlockInfos, with StructurePlaceSettings beside them for the ordered processor list, and RuleProcessor with ProcessorRule as the one processor worth reading in full. JigsawReplacementProcessor is what makes the assembly graph disappear. Finish at StructureTemplateManager, for the sources and why a structure block’s file wins. One door the page does not open: StructureBlockEntity, which is the whole editor’s state in one class.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Hand-built structures

Verified against Minecraft 26.3 · Part XII · A stronghold is generated: a piece grammar written in Java, a graph assembled at an imaginary height and moved down afterwards, and a whole structure thrown away and rebuilt because it had no portal room.

Every stronghold has exactly one end portal. Not usually, not almost always — exactly one, in every stronghold in every world, and the grammar alone does not guarantee it. The portal room is weighted heavily, capped at one placement, and forbidden near the entrance; and if the maze finishes without one, StrongholdStructure clears the whole builder, adds one to the seed and generates the entire stronghold again. It is the only structure in the game that regenerates itself until it likes the result.

Jigsaw and templates traces a village, and a village is a jigsaw: pieces come from a data-pack registry and find each other through connector blocks. That is one of the sixteen structure types. The other fifteen use an older assembler that is still the majority of the code — thirty classes and about 10,000 lines under levelgen/structure/structures, against about 1,550 for the whole jigsaw package, aliases included. Strongholds, mineshafts, nether fortresses, ocean monuments, woodland mansions, end cities, ruined portals, igloos, shipwrecks, ocean ruins, desert pyramids, jungle temples, swamp huts, buried treasure and nether fossils are all built this way.

Everything around the assembler is shared, and belongs to structure placement: the lottery, the presence cache that StructureCheck is, the reference scan, Beardifier and the per-chunk write. This page is only the part where the pieces come from.

The idea

There is no pool and no registry of pieces. A piece is a Java class that knows how to write its own blocks, and it grows the structure by constructing its own neighbours.

classits role
StructurePiecethe base, and the reason the system holds together: a mutable BoundingBox, an orientation, a mirror, a rotation, a depth, and a registered StructurePieceType — which is how a piece comes back off disk, and the reason every one of these classes needs a loader beside it
StructurePiece.placeBlockthe conventional write path — converts to world coordinates, drops anything outside the chunk box it was handed, applies the piece’s mirror and rotation to the block state, and schedules a tick for whatever fluid is at the position after the write. Not a choke point: the structure classes call LevelWriter.setBlock on the level directly two dozen times
StructurePiece.BlockSelectora stateful per-block state chooser, and the entire visual character of a structure
StructurePiecesBuilderaccumulates the pieces, can move all of them vertically at once, and is what every piece’s StructurePiece.addChildren is handed: its StructurePiecesBuilder.findCollisionPiece is StructurePiece.findCollisionPiece, a linear scan returning the first overlapping box — there is no spatial index, and the list being scanned is the list being built
TemplateStructurePiecethe bridge to the .nbt machinery, for structures that are procedural in layout and templated in content
ScatteredFeaturePiecethe base for one-shot surface buildings, with two ground-finders
SinglePieceStructurethe forty-line Structure that places exactly one of those

Three things about that base class do most of the work.

Orientation is not independent of mirror and rotation

StructurePiece.setOrientation derives both from the facing direction, and a south-facing piece is expressed as a left-right mirror rather than a 180° rotation. That trick is why nearly every hand-written piece in this package is written once, in a north-facing local frame, and comes out correct in all four horizontal facings.

Local Y is measured from the box floor

StructurePiece.getWorldX, StructurePiece.getWorldY and StructurePiece.getWorldZ map local coordinates into the world, and because Y is relative to the floor, moving a finished graph vertically is free. When the orientation is null the transform is the identity, which is how a mineshaft’s one room and its crossings write in world coordinates.

StructurePiece.addChildren is not a framework hook

Its default body is empty and nothing in the framework ever calls it; every call site is a structure’s own generation code. The recursion is arranged by each family for itself, in one of two shapes. Strongholds and nether fortresses use a shuffled work queue: a new piece goes into the builder and onto the start piece’s pending list, and the structure drains that list by repeatedly removing a random index and expanding it, so growth is breadth-ish and unbiased. Mineshafts use inline recursion and expand each new piece immediately, so the first branch of a crossing is fully grown before the second is attempted.

The vocabulary a piece writes with

The rest of the base class is what a piece says things in: StructurePiece.generateBox fills a local box while distinguishing edge cells from interior ones, and StructurePiece.generateAirBox, StructurePiece.generateMaybeBox, StructurePiece.generateUpperHalfSphere, StructurePiece.fillColumnDown and StructurePiece.createChest — which leaves a loot-table key and a seed rather than items (loot tables) — are the rest. StrongholdPieces.SmoothStoneSelector is the canonical block selector: on a box edge it rolls cracked, mossy or infested stone brick and otherwise plain, and interior cells become cave air. One small object is the whole look of a stronghold. JungleTemplePiece.MossStoneSelector is the other one.

A stronghold, built twice if it has to be

All of this runs at ChunkStatus.STRUCTURE_STARTS (the pyramid, drawn), inside the same Structure.GenerationStub consumer the jigsaw assembler runs in — so the whole graph is built in memory, on the worldgen executor, with no world access and no blocks written. The stub’s generator is an either, and one structure takes the branch nothing else takes: MineshaftStructure.findGenerationPoint hands it a builder it has already filled instead of a consumer to run later, so a presence check that reaches a mineshaft candidate lays the whole graph out too, on the Server thread (structure placement).

sequenceDiagram
    participant ChunkG as ChunkGenerator
    participant SStr as StrongholdStructure
    participant SPie as StrongholdPieces
    participant SPB as Structure<br/>PiecesBuilder

    ChunkG->>SStr: Structure.generate
    loop until the start piece has recorded a portal room
        SStr->>SPB: clear
        SStr->>SStr: reseed, the world seed plus one per try
        SStr->>SPie: resetPieces, the static weight table
        SStr->>SPB: addPiece, a new start piece
        SStr->>SPie: addChildren, on the start piece
        loop while the start piece's pending list is not empty
            SStr->>SPie: addChildren, on a piece removed at random
            SPie->>SPie: pick by weight, never the previous type
            SPie->>SPB: findCollisionPiece, a linear scan
            SPB-->>SPie: nothing overlaps, so construct it
            SPie->>SPB: addPiece, and onto the pending list too
        end
        SStr->>SPB: moveBelowSeaLevel, every piece at once
    end
    SStr->>SPB: build, into a StructureStart
    SStr-->>ChunkG: the StructureStart, written at FEATURES

The whole stronghold is grown in memory by one method on StrongholdStructure, and the outer loop is the one that throws it all away: it ends only when a try’s start piece holds a portal room.

The structure only ever asks a piece to grow; the piece does the picking, the collision test and the adding, and each piece it adds joins the start piece’s pending list for a later pass.

It is built at an imaginary height. The start piece is constructed at a fixed Y — sixty-four for strongholds and fortresses, fifty for mineshafts — with no idea where the ground is. Every collision test, every staircase descent and the floor guard that refuses a box at or below Y 10 happens in that frame. Only afterwards does StructurePiecesBuilder.moveBelowSeaLevel shift the whole graph so its top sits below sea level. Nether fortresses use StructurePiecesBuilder.moveInsideHeights to land in a band, and a mesa mineshaft uses StructurePiecesBuilder.offsetPiecesVertically to sit between sea level and the surface. This is the payoff for local-Y-from-the-floor.

The weight table is static, and so is the piece the loop is forced to place next. StrongholdPieces.resetPieces — the reset in the diagram — refills a remaining-piece list with the whole table, zeroes the placement counts in the static weight table and clears a one-shot “place this type next” override that only the start piece ever sets, and all three are reached through static fields on StrongholdPieces, reset from inside a generation lambda on the worldgen executor. The nether fortress shares less: its lists live on its start piece, and only the placement counts in its static weight tables are shared, reset when a start piece is constructed. Two strongholds laid out at once would interfere; what keeps it from happening is the worldgen executor, which runs one task at a time per dimension, and only the overworld has strongholds in the shipped data, so only /place structure racing generation could. It is the sharpest contrast with the stateless jigsaw path.

Growth stops when the budget is spent or no door can take a piece. StrongholdPieces.STRONGHOLD_PIECE_WEIGHTS pairs each piece class with a weight and a maximum placement count: most corridors and turns are unlimited, a room crossing may appear six times, a library twice, a portal room once — and the library and portal room additionally refuse to appear before a certain depth. The picker makes up to five weighted attempts, rejecting whichever type was placed immediately before, and falls back to a filler corridor. Two bounds sit beside the budget — a depth cap of fifty, and a square reaching 112 blocks from the start piece’s corner on each axis, outside which no new piece may begin — and inside them the picker returns nothing once every limited type has hit its limit.

Collision is a brake too, and some pieces negotiate. Each candidate constructor computes its box and asks StructurePiecesBuilder.findCollisionPiece; a hit means the candidate simply is not built. A mineshaft corridor tries decreasing lengths until one fits, and a stronghold library falls back from its tall variant to its short one.

And the loop’s exit condition is a piece of bookkeeping that looks like a feature. The portal room’s entire StructurePiece.addChildren body is a record of itself on the start piece, which is how StrongholdStructure knows whether to run again. That record is also reachable as StrongholdPieces.StartPiece.getLocatorPosition, an override that returns the portal room’s position where the base method returns the centre of the piece’s box — so a stronghold is the one structure in the game that knows where its own landmark is, and nothing in 26.3 calls the method (what /locate answers with).

The whole-graph move that ends the loop is also deprecated: StructurePiecesBuilder.moveBelowSeaLevel, StructurePiecesBuilder.offsetPiecesVertically, TemplateStructurePiece.move and the siting helper the mansion and the end city share are all marked so, and the jigsaw package declares nothing deprecated of its own. The start heights have names — StrongholdPieces.MAGIC_START_Y and NetherFortressPieces.MAGIC_START_Y at sixty-four, MineshaftPieces.MAGIC_START_Y at fifty — and one discarded random draw is load-bearing: MineshaftStructure.findGenerationPoint opens by drawing a double and throwing it away, and without that draw every mineshaft a seed generated from then on would be laid out differently.

The four families

familyhow the pieces come to existmembers
procedural piece graphsthe pieces write their own blocks and construct their own neighbours — the pattern in its pure formStrongholdPieces, MineshaftPieces, NetherFortressPieces
grid and graph solversa layout is solved first and pieces are emitted afterwards, so neither ever calls StructurePiecesBuilder.findCollisionPiece — the layout is the collision guaranteeWoodlandMansionPieces, OceanMonumentPieces
template-backed piecesprocedural placement, .nbt content, and therefore the same processors and the same StructureTemplate.placeInWorld the jigsaw path usesEndCityPieces, RuinedPortalPiece, OceanRuinPieces, ShipwreckPieces, IglooPieces, NetherFossilPieces, WoodlandMansionPieces
one-shot surface buildingsno graph and no children: one box, dropped on the ground, over ScatteredFeaturePiece, or — for buried treasure — a ground scan of its ownDesertPyramidPiece, JungleTemplePiece, SwampHutPiece, BuriedTreasurePieces

The nether fortress is the most elaborate of the first family: it runs two weight tables and a mode switch, where a castle entrance is a one-way door out of bridge mode into castle mode, and only a T-balcony can fall back, on a one-in-eight roll per branch. WoodlandMansionPieces is in the table twice because it genuinely is: its layout is solved and its rooms are then stamped from .nbt files.

Beside each of those piece families sits a Structure subclass — RuinedPortalStructure, DesertPyramidStructure, OceanRuinStructure, ShipwreckStructure, WoodlandMansionStructure, NetherFossilStructure and the rest — which is the settings wrapper and the entry point. For the jungle temple that is all, a SinglePieceStructure of forty lines, and the swamp hut’s, the igloo’s, buried treasure’s and the ocean ruins’ are nearly as thin; the other ten add something of their own — a siting rule, a scan for ground, a generation loop, a rebuild at load, or work after the blocks are down. Two are worth stopping on. RuinedPortalStructure draws the decay setup, below. And DesertPyramidStructure, a SinglePieceStructure too, has one of the two live Structure.afterPlace overrides (the other, WoodlandMansionStructure’s, fills the space under the mansion with cobblestone down to the ground): once the pyramid’s blocks are down it collects every candidate position its pieces recorded, shuffles them from a positional random source, turns five to seven into suspicious sand and the rest into plain sand — which is why a pyramid’s archaeology is the same in two worlds with one seed and never the same twice within one.

What a Java piece keeps that a template cannot

Three of the families hold state the framework has no place for, and each case is a consequence of a piece being an object rather than a file. The ocean monument’s rooms are never saved: they are held privately on the main building, never reach the builder, and their save method is empty anyway — StructureStart.loadStaticStart carries a hardcoded type check that calls OceanMonumentStructure.regeneratePiecesAfterLoad, which reads position and orientation from the save and rebuilds every room from the world seed, so the monument comes back identical rather than merely present. Every other structure’s pieces are read back from what they wrote, a template piece’s box recomputed from its template on the way. A saved bounding box is not always where the structure is: buried treasure rewrites its own box while placing, igloos are built at a hardcoded Y 90 and re-seated at write time from the live heightmap, with only their template position put back, so a reloaded igloo’s box is recomputed at Y 90, and shipwrecks latch a flag so the second chunk does not move them again — for those types the box is a placement hint. Two pieces go further and deliberately widen the chunk they were given, a ruined portal and a nether fossil both encapsulating the writable area so they are placed whole rather than sliced — the portal from the chunk holding its centre, the fossil from every chunk it touches; since BoundingBox is mutable and shared between the pieces of one start, that widening leaks, harmlessly today because both structures have exactly one piece. And a template-backed piece still cleans up after a connector it never used: TemplateStructurePiece.postProcess scans what it placed for jigsaw blocks and replaces each with its final state, so the jigsaw block inside each of five ruined-portal templates resolves quietly instead of connecting to anything. Beardifier’s projection test is the only place at runtime where the two assemblers are told apart.

Almost nothing here is data-driven, and that is the point. Piece choice, weights, budgets, layout rules and adjacency are Java, bar a few knobs a structure’s JSON sets: the ruined portal’s weighted setups, the ocean ruin’s odds of a large ruin and of a cluster, and the shipwreck’s beached flag, which picks its template list. Registries.STRUCTURE still supplies the settings wrapper (structure placement) and the templated families read .nbt files, but a data pack cannot add a room to a stronghold — which makes this the book’s clearest counter-example to the data-driven type pattern: a registry whose instances are Java grammars (the pattern).

Where the families bend the idea

The mansion is grown and then tidied to a fixed point. Corridors are recursed out from the entrance on an 11×11 grid, rooms are stamped alongside them, and then an edge-cleaning pass runs repeatedly until nothing changes, filling any cell with enough occupied neighbours. That pass is why a mansion is a solid block of building rather than the thin maze the corridor walk produced. Rooms are then greedily merged into 2×2, 1×2 and 1×1 units, with type, id and flags packed into a single integer per cell — and a room that ends up with no corridor edge becomes a secret room, reachable only from above. A mansion may also have two floors instead of three: the third needs a second-floor one-by-two room with a door to hang its staircase on, and if there is none, or no free direction to grow into, the third-floor grid is blanked entirely.

The ocean monument carves its maze backwards. It wires a lattice of rooms fully connected, then repeatedly closes a random opening and keeps the closure only if both sides can still reach the entrance room, using a depth-first reachability walk with an increasing scan counter in place of a visited set. Rooms are then fitted by a list of room-shape fitters in fixed order, first match wins, so the large double rooms get first refusal and the plain room is the fallback.

End city sections collide as groups, not as pieces. Each candidate section is generated into a scratch list and tagged with one shared random StructurePiece.genDepth — used as a group identity, not a depth. The section is accepted only if every collision it finds is with a piece carrying the parent’s tag; one foreign overlap discards the entire candidate list atomically. A bridge tags its first span minus one, a tag no parent carries, so the tower hung off its far end may not overlap it, and the section’s shared tag replaces the mark before the bridge itself is checked; the ship becomes likelier the deeper a section sits in the city’s recursion, with at most one per city.

Ruined portal decay is mostly a processor stack. The ageing, the gold-block gaps, the lava-to-magma substitutions and the mossiness are StructureProcessors assembled per portal and rebuilt at load from the settings the piece saves, so decay reproduces exactly on reload; the netherrack spread, the netherrack columns under the portal, the vines and the leaves are the piece’s own code. What makes it unusual is that the stack is built in Java and appears in no data pack at all (the processors): BlockAgeProcessor — which is the ageing and the mossiness at once — LavaSubmergedBlockProcessor and BlackstoneReplaceProcessor are RuinedPortalPiece’s alone. Which placement a portal gets — on the surface, partly buried, underground, in a mountain, on the ocean floor or in the nether — is settled from the setups its JSON lists before any piece exists: a weighted draw for the standard and mountain portals, which list two, and the one setup for the other five.

Where to look

StructurePiece is the whole framework and repays reading straight through: the mutable BoundingBox, StructurePiece.setOrientation for the mirror trick, StructurePiece.getWorldY for the floor-relative frame, and StructurePiece.generateBox and StructurePiece.BlockSelector for the vocabulary a piece writes with. StructurePiece.addChildren is the method to read for what it does not do. Then StrongholdStructure beside StrongholdPieces — the regeneration loop, StrongholdPieces.resetPieces and StrongholdPieces.STRONGHOLD_PIECE_WEIGHTS in one sitting — and StructurePiecesBuilder for the builder StructurePiece.addChildren is handed and StructurePiecesBuilder.moveBelowSeaLevel for the move. After that pick one family per shape: MineshaftPieces for inline recursion, WoodlandMansionPieces or OceanMonumentPieces for a solver, RuinedPortalPiece for the template-backed kind, and ScatteredFeaturePiece with SinglePieceStructure for the one-shot buildings. One door the page does not open: OceanMonumentStructure.regeneratePiecesAfterLoad, the only structure whose pieces are rebuilt from the seed at load rather than read back.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Creating a world

Verified against Minecraft 26.3 · Part XII · Create New World: a seed typed in, Superflat chosen, a layer deleted, an experiment switched on, and the settings object that comes out the other end.

You click Create New World and the game stops on Preparing for world creation… for a moment. Then a three-tab screen: a name, a game mode, a seed box, a world-type button, and buttons for game rules, data packs and experiments. You type a seed, cycle the type to Superflat, open Customize and delete the dirt layer, switch on an experiment, set one game rule, and press Create.

That opening pause is what this page is about. Before the screen can draw a single widget the game has already run a complete server-side data-pack load — the same WorldLoader.load a dedicated server runs at startup (the world load) — mostly on the worker pool, with the Render thread parked on BlockableEventLoop.managedBlock, running queued tasks, the load’s own among them, while it waits (the event loop), until it finishes. The settings the screen exists to edit, a WorldGenSettings, were built halfway through that load, out of registries the load had just filled. Every widget on the World tab is an edit to them — the name, the game mode, the difficulty, Allow Commands and the game rules go somewhere else entirely, into the LevelSettings and the GameRules that ride beside it. Two of the buttons throw the whole load away and run it again whenever they change the pack list. And Create barely touches them: by the time you press the button, the world’s generation settings have existed in memory for as long as the screen has, and Create only packs them into a fresh WorldGenSettings.

Everything else in Part XII reads that object. This page is where it comes from, where it goes, and the dedicated server’s and Re-Create’s routes to one.

The cast

classwhat it ownsits thread
WorldGenSettingsthe whole answer: a WorldOptions and a WorldDimensions, and nothing else. It is a SavedDatabuilt on the worker pool in every load, and again on the Render thread by Create; read on the Server thread, and by ChunkStatusTasks.generateStructureStarts on the worldgen executor
WorldOptionsthe seed, generate structures, bonus chest and a legacy string — four fields, all immutable, each with-method returning a new one—
WorldDimensionsa map from LevelStem key to LevelStem, each a dimension type plus a ChunkGenerator. Refuses to exist without an overworld—
WorldLoaderthe data-pack load every path shares, and the seam (WorldLoader.WorldDataSupplier) where the caller decides what the settings areworker, with hops to the thread that called it
WorldCreationContextthe settings plus the loaded registries and ReloadableServerResources they were parsed against — the screen’s whole worldRender thread, replaced wholesale
WorldCreationUiStatethe widget-visible state, and the listener list that keeps the tabs agreeing with it — CreateWorldScreen registers sevenRender thread
WorldOpenFlowsevery route from the world list into a running server, each one a chain of methods that can interpose a confirmationRender thread
MinecraftServertakes the finished object out of the WorldStem and hands it to SavedDataStorage, which is what puts it on diskthe hand-off in its constructor, on the caller’s thread — the Render thread for a single-player world; the write at its first save, on the Server thread

Five stages, and only one of them is the screen

flowchart TD
    subgraph WL["mostly the worker pool"]
        A["1 · load the packs and the registries"] --> B["2 · the caller's callback decides the settings"]
        B --> C["3 · recipes, loot and functions read against them"]
    end
    C --> D["4 · the screen edits the settings"]
    D -->|"the pack list changed"| A
    D -->|"Create"| E["5 · bake, write level.dat, start the server"]

Stages one to three are one WorldLoader.load call, mostly off the Render thread, and the screen is stage four — the only stage that can send the whole load back to the start.

The ordering that matters is stage 2 before stage 3, and it is the reason this page exists at all. WorldLoader.load takes the settings-building callback as a parameter and calls it in the gap between loading the world and dimension registries and loading the reloadable ones — the recipes, the loot tables and the rest — so the registry set those and the functions are then parsed against includes the dimension registry that callback produced. The seed and the dimension list are settled before a single recipe is read, and they are settled by a lambda the caller supplied, which is the only reason the client’s create screen, the client’s world-opener, the dedicated server and the game-test server can share one loader.

The registry pass that callback sits behind is RegistryDataLoader.DIMENSION_REGISTRIES, and it is the last registry pass before the callback — the reloadable registries follow it, inside ReloadableServerResources (when a world opens). It holds exactly one registry, Registries.LEVEL_STEM, loaded in its own pass because its entries need every worldgen registry already in hand — and that single-entry list is the dimension/ folder of a data pack.

The object, and what is not in it

WorldGenSettings has two fields. WorldOptions holds the seed, a generate structures flag, a bonus chest flag and a string that only a very old save carries. WorldDimensions holds the map of LevelStems. There is no world name in it, no difficulty, no game mode, no game rule and no data-pack list — those are LevelSettings and GameRules, and level data and rules says which file each of them ends up in.

It does not end up in level.dat either. WorldGenSettings extends SavedData and carries its own SavedDataType, so it is written to data/minecraft/world_gen_settings.dat — and the game rules to data/minecraft/game_rules.dat — by the ordinary saved-data machinery rather than by the level-data writer. That is a different writer on a different thread at a different moment, which is the last thing this page traces.

A seed is not a number the box gives you. WorldOptions.parseSeed trims the text, returns nothing at all for an empty string, parses a long if it can, and otherwise returns the Java string hash of what you typed — which is why a seed of glacier is a seed and a seed of 99999999999999999999 is the hash of that text rather than the number. And nothing at all is not zero: WorldOptions.withSeed turns an absent seed into WorldOptions.randomSeed, one draw from a fresh RandomSource. The seed field’s responder calls WorldCreationUiState.setSeed on every keystroke, so an empty box is re-rolling a new random world every time you touch it.

Every widget is an edit to a live object

WorldCreationUiState is not a form. It holds the WorldCreationContext itself, rebuilds it whenever a generation setting changes, and on every change of any kind walks a listener list so that each widget re-reads what it should now show. The state is also opinionated about what it returns: WorldCreationUiState.getDifficulty reports hard in hardcore whatever the button last set, WorldCreationUiState.isAllowCommands reports true in a debug world and false in hardcore, and WorldCreationUiState.isBonusChest reports false in both. The buttons are disabled to match, but the state would lie to them anyway.

The world-type button is the destructive one. WorldCreationUiState.setWorldType calls WorldPreset.createWorldDimensions and replaces all the dimensions with the preset’s, so leaving Superflat and coming back to it discards whatever the Presets screen chose, but not a layer deleted from the stack the button gave you: that was deleted from the world preset registry’s own object (below) and comes back with it, until a pack change reloads the registry. Seven world presets ship as JSON under data/minecraft/worldgen/world_preset/, and the button reaches six of them: it cycles the normal tag, which holds five — Default, Superflat, Large Biomes, Amplified and Single Biome — and holding Alt swaps it for the extended tag, which is those five plus debug_all_block_states. The seventh, WorldPresets.FLAT_ALL_DIMENSIONS, is in neither tag, so the world-type button never cycles to it: on the client only CreateWorldScreen.testWorld selects it, behind a TW button the title screen adds when SharedConstants.IS_RUNNING_IN_IDE, while the game-test server builds every test world from it and a dedicated server’s level-type can name it.

Customize is rarer than it looks. PresetEditor.EDITORS is a two-entry map: WorldPresets.FLAT opens CreateFlatWorldScreen and WorldPresets.SINGLE_BIOME_SURFACE opens CreateBuffetWorldScreen, whose one choice becomes a FixedBiomeSource — the source a superflat and a Debug Mode world use too. For the other five presets the button is inactive. Both editors end the same way, in WorldCreationContext.DimensionsUpdater lambdas that call WorldDimensions.replaceOverworldGenerator — the overworld only. Nothing in the create screen can edit the nether or the end.

The layer editor edits the generator you already have

FlatLevelGeneratorSettings is the odd object among the settings this page handles, where WorldOptions, WorldDimensions and WorldPreset offer nothing to change. Its layer list — of FlatLayerInfo, a block and a height — is mutable, its lakes and features flags are set by void methods, and FlatLevelGeneratorSettings.getLayersInfo hands out the live list. PresetEditor passes CreateFlatWorldScreen the settings of the current overworld generator when that generator is already a FlatLevelSource — the same object, not a copy, and, while the stack is the one the world-type button gave you, the world preset registry’s own — and the Remove Layer button removes an entry from that list directly and calls FlatLevelGeneratorSettings.updateLayers.

So the Cancel button on the layer editor does not undo a layer deletion. Cancel only skips the WorldCreationContext.DimensionsUpdater that would build a new FlatLevelSource; the list it would have been built from has already changed. The Presets screen is the well-behaved half of the same screen: PresetFlatWorldScreen reads and writes the layer stack as a text string and hands back a new settings object through FlatLevelGeneratorSettings.withBiomeAndLayers. Nine flat presets ship as JSON, one for each key FlatLevelGeneratorPresets registers a value for; the tenth key, FlatLevelGeneratorPresets.TEST_WORLD, is declared, never given a value, and read by nothing in the game.

One thing the flat generator does not do is place all its own blocks. FlatLevelGeneratorSettings.adjustGenerationSettings walks the built layer stack and, for every layer whose block fails Heightmap.Types.MOTION_BLOCKING’s opacity test, replaces it with a null and re-adds it as an inline FillLayerFeature placed feature in the TOP_LAYER_MODIFICATION decoration step. That test is the BlockTags.BLOCKS_MOTION_IN_HEIGHTMAP tag or a fluid, so the water in Water World stays terrain; what leaves it are the air of The Void and the snow layer on top of Snowy Kingdom, which arrive as features.

An experiment is a data pack, so switching one on reloads everything

ExperimentsScreen looks like a toggle list and is a filtered pack browser: it walks the repository’s available packs and keeps only those whose Pack.getPackSource is PackSource.FEATURE — a built-in pack that carries a feature-flag section and deliberately refuses to be selected automatically (the repository and its packs). Three ship — minecart_improvements, redstone_experiments and trade_rebalance — one per non-vanilla flag in FeatureFlags, and what a flag then gates is feature flags. Pressing Done rewrites the repository’s selection and lands in CreateWorldScreen exactly where the data-pack screen lands, in CreateWorldScreen.tryApplyNewDataPacks.

That method has a fast path and a slow one. If the enabled-pack list comes back unchanged, WorldCreationUiState.tryUpdateDataConfiguration swaps the configuration in and nothing reloads. Otherwise CreateWorldScreen.applyNewPackConfig puts a validating message on screen and runs WorldLoader.load again from the top — and it has to carry your settings across a registry set that is about to be replaced. It does that by serialising them: WorldGenSettings.CODEC encodes the current options and dimensions to JSON using the old registries as context (where the registry context comes from), and re-parses that JSON against the new ones. Every Holder in the object — every biome, every noise settings, every structure set the flat generator overrides — is written out as an id and looked up again. If the new packs have no world preset or no biome, or the re-parse fails, the future completes exceptionally and the player gets a retry-or-reset confirmation instead of a screen.

The two routes into that method differ in one boolean, and in the screen a failure returns to. CreateWorldScreen.tryApplyNewDataPacks shows ConfirmExperimentalFeaturesScreen only when the requested flags are experimental and the caller was the data-pack screen. Toggling an experiment in the Experiments screen skips it — that screen carries a red warning line of its own instead.

Data packs added here do not go into a world folder that does not exist yet. CreateWorldScreen.getOrCreateTempDataPackDir makes a temporary directory prefixed mcworld-, the pack browser is pointed at that, and the directory is copied into the new world’s datapacks folder by CreateWorldScreen.createNewWorldDirectory at the very end. It is then deleted on every exit including that one: CreateWorldScreen.removeTempDataPackDir runs on the line after the create callback returns.

What Create does

sequenceDiagram
    participant CWS as CreateWorldScreen
    participant WCUS as WorldCreationUiState
    participant WOF as WorldOpenFlows
    participant MC as Minecraft
    participant IS as IntegratedServer
    participant Disk as Disk
    rect rgba(0, 0, 0, 0.04)
    Note over CWS,Disk: Render thread
    CWS->>WCUS: read the context one last time
    WCUS-->>CWS: WorldOptions and the selected WorldDimensions
    CWS->>CWS: bake the dimensions, and read the lifecycle off them
    CWS->>WOF: confirmWorldCreation, with that lifecycle
    WOF-->>CWS: proceed, or a warning screen first
    CWS->>Disk: the world directory, with the temp datapacks copied in
    CWS->>WOF: createLevelFromExistingSettings
    WOF->>MC: doWorldLoad
    MC->>Disk: level.dat, through a temp file
    MC->>IS: new, with the WorldStem and the screen's GameRules
    IS->>IS: the constructor hands WorldGenSettings to its SavedDataStorage
    end
    rect rgba(0, 0, 0, 0.04)
    Note over IS,Disk: Server thread, first save
    IS->>Disk: world_gen_settings.dat and game_rules.dat
    end

The client writes level.dat before the server exists; the settings reach disk later, from the server’s own first save, in two files of their own under data/minecraft.

Three details in that order are worth stopping on. CreateWorldScreen.onCreate bakes the dimensions to decide the lifecycle and the PrimaryLevelData.SpecialWorldProperty, but the WorldGenSettings it stores holds the unbaked selection — the bake is what runs, the selection is what is saved. The warning is skipped when the world is not a re-create and the baked registries are stable, and WorldDimensions.checkStability asks that question per key — anything under a fourth key is experimental by construction — but it asks a different question of each of the three. WorldDimensions.isStableNether requires the nether’s dimension type, a NoiseBasedChunkGenerator on the vanilla nether settings and a MultiNoiseBiomeSource on the vanilla parameter list, and WorldDimensions.isStableEnd the End’s type, the vanilla End settings and a TheEndBiomeSource. The overworld’s check tests the dimension type too, and then the parameter list only if the biome source happens to be a MultiNoiseBiomeSource at all. So Amplified passes on non-vanilla noise settings, and Superflat and Single Biome pass by not having a multi-noise biome source to fail on. The upshot is that no preset the button can reach ever raises this warning: the only shipped one that fails is Flat All Dimensions, which gives the nether and the end flat generators — and the world-type button never cycles to it. In practice the warning is a data-pack warning. And level.dat is written by the client, in Minecraft.doWorldLoad, before the Server thread exists — while the settings file is written by the server after it starts, because MinecraftServer’s constructor is the first thing to hand the object to SavedDataStorage.

The game rules take a route of their own to the server. CreateWorldScreen.onCreate copies the screen’s GameRules into an Optional that travels through CreateWorldCallback, WorldOpenFlows.createLevelFromExistingSettings and Minecraft.doWorldLoad to the MinecraftServer constructor, which builds a fresh rule set from the saved-data default and then overlays the screen’s values on top.

The same object, built two other ways

The screen is one of several programs that produce a WorldGenSettings — the demo world and the developer-only DEBUG recreate button make one through WorldOpenFlows.createFreshLevel, and the game-test server makes one too — and setting it beside the dedicated server and Re-Create is the clearest way to see which parts of this page are the subject and which are only its interface. The dedicated server never sees a screen at all; Re-Create sees this one and feeds it an answer.

client create screendedicated serverRe-Create
who builds itCreateWorldScreen.onCreateMain.createNewWorldDataCreateWorldScreen.onCreate again, from what WorldOpenFlows.recreateWorldData read
the seedthe seed box, per keystrokelevel-seed, in the DedicatedServerProperties constructorcopied from the old world’s settings
the dimensionsa WorldPreset plus screen editslevel-type as a world-preset id; the legacy names default and largebiomes parse as ids first, match no preset and give normalthe old world’s saved LevelStem map
customisingPresetEditor, overworld onlygenerator-settings JSON, parsed by FlatLevelGeneratorSettings.CODEC and only when the preset is WorldPresets.FLATthe create screen again
game rulesWorldCreationGameRulesScreenone, and it is a legacy key: announce-player-achievements sets GameRules.SHOW_ADVANCEMENT_MESSAGESread back from game_rules.dat
whenonly if the folder is newonly if there is no level.datalways a new folder

Both seed paths are the same method. DedicatedServerProperties calls WorldOptions.parseSeed on level-seed and falls back to WorldOptions.randomSeed, exactly as the seed box does — so an empty level-seed draws its random seed the moment the properties file is parsed, whether or not a world is about to be created. An unrecognised level-type is a warning in the log and the normal preset, not a failure.

Re-Create is the interesting column. WorldOpenFlows.recreateWorldData reads the old world with a deliberately empty LevelStem registry, so the dimensions come from the saved settings rather than from any pack, and its caller in the world list hands CreateWorldScreen.createFromExisting a LevelSettings and a context. The result is a new world folder with the old seed pre-filled. Nothing in the family edits an existing world’s WorldGenSettings in place: EditWorldScreen offers a rename, an icon reset, a folder button, a backup, the backups folder and Optimize World, and not one generation setting.

The rest of the family

Two more corners of client/gui/screens/worldselection matter to this page. The rows of SelectWorldScreen’s WorldSelectionList are LevelSummary objects read by LevelStorageSource.readLightweightData, an NBT parse that deliberately skips the Data/Player and Data/WorldGenSettings subtrees — the second of them named by PrimaryLevelData.OLD_WORLD_GEN_SETTINGS, all that is left of the old key — so that listing a hundred worlds never costs a settings parse. And WorldOpenFlows.openWorld is a chain of methods — itself, then level data, version compatibility, the upgrade, the world stem, stem compatibility, a bundled resource pack, disk space, and finally Minecraft.doWorldLoad — nearly every one of which can stop and put a confirmation screen in the way, which is the shape of every route from the world list’s own buttons, and of neither the demo world’s nor a server’s.

For a 1.21-era reader. The seed has left level.dat. WorldGenSettings is a SavedData with its own file, data/minecraft/world_gen_settings.dat, and the game rules likewise; PrimaryLevelData keeps the old key name only as the constant PrimaryLevelData.OLD_WORLD_GEN_SETTINGS.

Where to look

Start with net/minecraft/world/level/levelgen: WorldGenSettings is short and tells you the whole shape, WorldOptions and WorldDimensions are the two halves, and WorldDimensions.bake is the method that turns a selection into a registry. Then net/minecraft/server/WorldLoader — one method, and the spine every path shares. Only then net/minecraft/client/gui/screens/worldselection, in the order WorldCreationContext, WorldCreationUiState, CreateWorldScreen and WorldOpenFlows, with net/minecraft/client/gui/screens/CreateFlatWorldScreen and net/minecraft/world/level/levelgen/flat beside them. The comparison is DedicatedServerProperties.createDimensions and Main.createNewWorldData, and the destination is the opening of the MinecraftServer constructor.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

XIII · Commands and data packs

Verified against Minecraft 26.3 · Part XIII · A string typed into a chat box becomes a call with typed arguments, on a queue, with a permission attached — and four whole systems are built on top of that.

Type a slash. The text turns grey and aqua and red as you type, a hint appears behind the cursor, and pressing Enter sends the string — the parse the client just did is thrown away. On the server the same string is parsed again, against a tree whose nodes carry permission requirements, and becomes a piece of work on a queue rather than a Java call — a heap-allocated deque of entries that one flat loop drains, which is why pending commands can be deleted part-way through an execution and why a function calling itself does not blow a Java stack. Everything else in this part rides that machinery: an advancement is a subscription delivered by a trigger and edited by /advancement, a scoreboard is a number that /scoreboard and execute store write, as the game’s own criteria do, a dialog is a data-pack form opened by /dialog, and a game test is a data-pack test run by /test. None of those four needs any of the others to be understood. What they need from this part is the parse and the queue; before you start names the other parts they lean on most.

The nine packages the atlas gives this part are three groups. The machinery: net/minecraft/commands and server/permissions. The catalogue: net/minecraft/server/commands, one file per command and a few helpers beside them. The four systems: net/minecraft/advancements, world/scores with server/bossevents, server/dialog with its client/gui/screens/dialog screens, and net/minecraft/gametest. Counted the way the atlas counts everything, that is 481 files and 44,083 lines — of which the catalogue alone is 111 files and 13,357 lines, most of them a thin lambda over machinery another page owns. So what a command does once dispatched is usually another part’s page, and Brigadier and commands carries an index of the main doors; the statistics, which are criteria, are in what this book skips.

The shape of the part

Part XIII is a stack of three floors, and for a watcher the dependency runs one way: all four systems on the top floor need both of the floors below, and none of them needs another. The code is less tidy than the lecture order: the lower floors reach up into all four top-floor systems — a selector’s advancements= and scores= options, the scoreboard’s own argument types, a dialog’s id, and /test’s registration — and one advancement predicate reads a team, sideways.

flowchart TD
    subgraph U["four systems"]
        direction LR
        L6["6 · Advancements"] ~~~ L7["7 · Scores, teams<br/>and stored data"] ~~~ L8["8 · Dialogs"] ~~~ L9["9 · Game tests"]
    end
    U -->|"is built on"| X
    subgraph X["execute"]
        direction LR
        L4["4 · The execution engine"] ~~~ L5["5 · Functions and macros"]
    end
    X -->|"is built on"| P
    subgraph P["parse"]
        direction LR
        L1["1 · Brigadier and commands"] ~~~ L2["2 · Permissions"] ~~~ L3["3 · Entity selectors"]
    end

The part as three floors, numbered to the watch order — parsing turns a string into a call, execution turns the call into work on a queue, and the four systems on top each have a command of their own. An arrow runs from a floor to the floor it is built on, the reverse of the prerequisite-first arrows on most other landing figures, so that the top floor is drawn on top, and nothing on the top floor points at another box there.

The four pages on the top floor are peers, not a sequence: watch them in any order, or only the ones you care about. The two floors below them are not optional for any of the four.

Before you start

The server tick from Part III. Two pages in this part turn on where in that order a thing sits — command functions near the top, an advancement’s flush at the end of a player’s own tick — and neither is legible without the order itself.

Codecs, NBT and JSON and the data-driven type pattern from Part II. Dialogs and game tests are the pattern’s clearest two instances — a form and a test suite, both reduced to JSON dispatching on a registry of types — and the pattern page is where the pattern is set out.

The connection from Part IX, for the boundary between the Netty threads and the Server thread, which the command packets and the suggestion request each cross their own way.

Contexts and predicates from Part VII, if you are here for advancements: a trigger’s conditions are loot conditions, evaluated against a loot context, and that page owns the machine — as it owns /execute if predicate, whose test a selector’s own predicate option runs too.

Watch in this order

  1. Brigadier and commands — three parsers for one string, and a tab-completion whose fast path never leaves the machine. Also: which sixty-six of the four hundred and fifty-four argument-node declarations ask the server by hand, and why they feel like all of them.
  2. Permissions — a permission is not an integer, an operator does not have everything, and a permission failure is reported as a typo.
  3. Entity selectors — a selector is a compiled query, and thirteen of its twenty-one options do more than filter: they shape the query plan. Why @p crosses dimensions, why sort=nearest is what takes your limit away, why one permission is checked twice, and which dimension /kill @e[limit=1] picks.
  4. The execution engine — a command engine with no Java recursion, and the two game rules that bound one instead. A fan-out that materialises one player at a time, and a /return that deletes work out of a queue rather than unwinding a stack.
  5. Functions and macros — what a .mcfunction file becomes, in two steps, the second of which usually does nothing. The one that fails silently every tick, forever.
  6. Advancements — the game’s general-purpose “tell me when the player does X”, built as a per-player subscription table that shrinks as criteria are met, and refilled by exactly two things. The tree is laid out on the server and shipped.
  7. Scores, teams and stored data — one number per thing, one query language for any tag, a boss bar, and the execute store seam that joins all three. Why fake players exist.
  8. Dialogs — a data pack puts a form on your screen, possibly before you are in a world at all. The values are read at the moment of the click and not before.
  9. Game tests — the game’s own test suite, as a data pack. There is no GameTest annotation, a batch is keyed by an environment and a dimension, the shipped jar contains exactly one test, and a failed one is left standing in the world on purpose.

Where the part stops

21% of the part’s lines are named on no page in the book, over two-fifths of them in the catalogue — itself not quite a third of this part by line — and most of the rest in the advancement triggers and predicates. Nearly all of that is one more instance of something a page above already draws. The nearly fifty commands nobody names — TeleportCommand, WorldBorderCommand, WeatherCommand and their neighbours — are each one more row for the door table on Brigadier and commands: a registration and a call into another part. The forty advancement triggers the advancements page does not explain and the concrete predicates beside them are instances of the shapes advancements states and declines to enumerate. The argument types are the same again: Brigadier and commands names the families rather than the sixty-two registered types — a type being a kind of argument and a node one use of one in one command’s tree, which is why the same page counts 454 declarations of those.

What is genuinely not here is a lecture on the three commands that are algorithms rather than doors — SpreadPlayersCommand‘s scatter, CloneCommands’ overlap handling, FillCommand’s replace modes. Each is a self-contained routine that no part’s page is about, and three unrelated routines do not make one lecture; they are named here so that a reader knows the omission is deliberate.

Reference this part uses

Packets for this part’s own traffic. The two systems that only report have no serverbound packet at all between them — the scoreboard’s five and the boss bar’s one run one way — while everything a player does comes back: the command and its suggestions, a dialog’s click, an advancement tab, a command block’s settings from the block or the minecart, and the two the test blocks’ edit screens send. Registries and the data-driven type pattern for the six type registries dialogs and tests dispatch on, and loot context parameter sets for the sets an advancement trigger and its reward run in. Diagram lanes for the abbreviations these figures use, and the glossary for Brigadier, selector head, world-limited, criterion, objective, macro, boss bar, dialog and game test.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Brigadier and commands

Verified against Minecraft 26.3 · Part XIII · You type /give @p diamond_sword[minecraft:damage=5] into the chat box: three parsers see that string, two of them throw their answer away — and not one of the completions you accepted along the way left your machine.

Open the chat box and type a slash. Before you have finished the word, the text is coloured, a grey hint has appeared behind the cursor and a completion popup is open — and every bit of that was produced by a real Brigadier dispatcher running on your own machine, with real parsers built against your own registries, from a tree the server sent you when you joined. The client is not pattern-matching strings. It parses the whole line on every keystroke, throws the parse away, and sends the string.

Which raises the question this page exists to answer: if the client can parse the command, why is completing an item id instant and completing a loot table not? Because a loot table is data the client was never sent. The tree the client rebuilt says, node by node, which nodes ask the server, and an argument over a registry the client lacks asks it too. Vanilla’s command code declares 454 argument nodes, and only 66 of them serialise as ask the server — so the local path is the rule, not the exception. What makes the round trip feel ubiquitous is which completions take it: they are mostly the ones over data the client was never sent.

The cast

classwhat it decidesside
Commandsthe one server-side CommandDispatcher, every registration, and what a failed parse is calledserver
CommandSourceStackwho is running this, from where — position, rotation, level, entity, permissions, output sink. Immutable; a change returns a copyserver
CommandBuildContextthe registries an argument type parses against, so a data pack’s biome is completable with no code changeboth
ArgumentTypeInfosthe wire description of an argument type: a ArgumentTypeInfo.Template that can be written to a buffer and instantiated on the far sideboth
SuggestionProvidersthe four providers that have a name on the wire — ask_server, available_sounds, summonable_entities and post_effects. Everything else serialises as ask_server tooboth
SharedSuggestionProviderthe interface both sides’ sources implement, so one argument type can complete on either. It extends PermissionSetSupplier, and its static SharedSuggestionProvider.suggest family is where most completion happensboth
ClientSuggestionProviderthe client’s implementation of it: the tab list, the looked-at block and entity, and the one method that sends a packetclient
CommandSuggestionsthe widget over it — the highlighter, the usage hint, the popup and the parse cacheclient
BrigadierExceptionsinstalled once into Brigadier’s global exception provider, which is why a parse error is a translatable Componentboth

Brigadier itself — the dispatcher, the tree of literal and argument nodes, each with a requirement predicate and optionally executable — is Mojang’s parsing library and lives outside the game’s packages. Everything on this page is what Minecraft builds on top of it.

Three parsers see one string

sequenceDiagram
    box Client
    participant CSug as Command<br/>Suggestions
    participant CPL as ClientPacketListener
    participant CSP as ClientSuggestion<br/>Provider
    end
    box Server
    participant SGPL as ServerGamePacket<br/>ListenerImpl
    participant Cmds as Commands
    participant GC as GiveCommand
    end

    CSug->>CPL: getCommands — the client's own dispatcher
    CSug->>CSug: updateCommandInfo — parse 1, every keystroke
        opt a node, or a registry the client lacks, asks the server
        CSug->>CSP: customSuggestion
        CSP->>SGPL: ServerboundCommand<br/>SuggestionPacket
        SGPL-->>CPL: ClientboundCommand<br/>SuggestionsPacket
        CPL->>CSP: completeCustomSuggestions — a stale id is dropped
    end
    CPL->>CPL: sendCommand, on Enter — parse 2, for signatures only
    CPL->>SGPL: ServerboundChatCommandPacket — the string
    SGPL->>SGPL: tryHandleChat — illegal characters, on the Netty thread
    rect rgba(0, 0, 0, 0.04)
    Note over SGPL,GC: the server thread
    SGPL->>SGPL: parseCommand — parse 3, the one that is used
    SGPL->>Cmds: performCommand
    Cmds->>GC: the registered lambda
    GC->>GC: giveItem — Inventory.add, then sendSuccess
    end

Three parses of one /give — two on the client whose answers are thrown away, and the server’s, the only one that runs; the round trip in the frame is taken only when a node asks the server or an argument needs data the client lacks, and /give has neither.

The three parsers of the title are in there: the client’s, running on every keystroke; the client’s again, once, when you press Enter; and the server’s, which is the only one whose answer is used. Each arrow below is a decision.

The client parses first, and the parse never leaves the machine. CommandSuggestions.updateCommandInfo runs the whole string through the client’s dispatcher on every keystroke — the one ClientPacketListener.getCommands hands it, rebuilt from the tree the server sent — and that parse produces the red tail, Brigadier’s smart-usage hint and the completion list. What is sent is the string.

Item, block-state and component completion never leaves the machine either. ItemArgument and BlockStateArgument are registered as context-aware singletons — meaning the wire form carries no data of its own and the far side builds the parser against its own CommandBuildContext, which is The tree on the wire below — so the client holds a real ItemParser and builds a real BlockStateParser against its own registries. Item ids, data components and block properties complete locally, and so does every argument type whose suggestion method reads a synced registry.

A node that asks for suggestions by hand almost always asks the server. This is the part the shape of the code invites you to get backwards — not that the round trip is common, but that opting in to a provider is what costs you one. SuggestionProviders.getName returns the registered name for a SuggestionProviders.RegisteredSuggestion and ask_server for everything else, so any node whose suggestions come from a plain lambda serialises as a request. Of the 73 vanilla nodes that attach a provider at all, seven name one of the three providers that mean anything on the wire — three available_sounds, two summonable_entities and two post_effects — and the other 66 become ask_server. That is how /function, /datapack, /bossbar, /scoreboard, /team, /schedule and /whitelist complete. The other 381 argument nodes attach nothing and fall back to their argument type’s own suggestions — which is why /give, the command in the line at the top of this page, never asks the server anything. A second route reaches the same packet: ClientSuggestionProvider.suggestRegistryElements failing to find a server-only registry — loot tables, advancements, recipes — and falling through. Two mechanisms, one packet.

One interface is why an argument type never knows which side it is on. SharedSuggestionProvider is implemented by CommandSourceStack on the server and by ClientSuggestionProvider on the client, and it is what an argument type’s suggestions ask of the source, when they ask it anything, so the same code runs on either. Most of it is static helpers — the SharedSuggestionProvider.suggest and SharedSuggestionProvider.suggestResource families, and the coordinate suggesters that offer you the block you are looking at — and all but one of its instance methods are the questions the two sides answer differently, among them the tab list, the selected entities, the relevant coordinates, and SharedSuggestionProvider.customSuggestion, which is the packet on the client and a completed empty future on the server.

Replies are matched by id, so a stale answer never flashes. The reply, ClientboundCommandSuggestionsPacket, lands on ClientPacketListener.handleCommandSuggestions, which forwards it to the provider. ClientSuggestionProvider.customSuggestion cancels the in-flight future and increments a counter; ClientSuggestionProvider.completeCustomSuggestions compares the reply’s id against that counter and drops anything older. The reply itself is truncated to a thousand entries, silently — no marker, no message.

Enter parses a second time on the client, for one reason. ClientPacketListener.sendCommand, which ChatScreen calls, runs SignableCommand.of to find out whether any argument is a SignedArgument. /give has none, so the plain packet — ServerboundChatCommandPacket, the string alone — goes; a /msg would take a timestamp, a salt and the last-seen message set, sign each signable argument and send the signed variant. A command the player did not type — a dialog button, a chat click event — goes through ClientPacketListener.sendUnattendedCommand instead and is parsed once or twice more before anything is sent (permissions). A sign is the exception that proves the rule: its click command reaches the client only as the sign’s text, and is never checked there, because the server runs it when the sign is used, and only if the sign’s allow_op_features flag is set.

The inbound packets cross the thread boundary differently, and neither takes the ordinary hop. ServerboundCommandSuggestionPacket is handed, on the Netty thread, to a ServerCommandSuggestionsProvider, which keeps only the newest request and answers at most one a tick, parsing it on the Server thread; the command packets do real work on the Netty thread first: ServerGamePacketListenerImpl.tryHandleChat refuses illegal characters there and only then hands the rest to the Server thread. The hop the other packets take is the connection (the server tick counts them, and chat and signing says what each of those checks catches). What matters for a command is only that the validation which can disconnect you runs before the parse does — so a command whose text is illegal never reaches the dispatcher at all.

The authoritative parse is the server’s — ServerGamePacketListenerImpl.parseCommand — with a CommandSourceStack from ServerPlayer.createCommandSourceStack carrying the real permission set, and Brigadier consults each node’s requirement during that parse. Execution then is not a Java call: Commands.performCommand flattens the parse into a context chain and hands it to Commands.executeCommandInContext, which is the execution engine.

/give itself is unremarkable and instructive. Its lambda calls GiveCommand.giveItem: EntityArgument.getPlayers resolves the selector, ItemArgument.getItem hands over the ItemInput that was built during parsing, ItemInput.createItemStack validates it, and the stacks go through Inventory.add with anything that will not fit dropped on the floor (items and stacks). Success goes, through CommandResponseTracker.sendFeedback, to CommandSourceStack.sendSuccess — which takes a supplier, so the message is never built when nobody will see it — and broadcasts to admins under two game rules.

Where a command’s output goes

CommandSourceStack carries a CommandSource — a message sink, three questions and a fourth question with a default, and no state — and its implementations are the whole answer to why a command block does not spam chat. MinecraftServer (the console) and RconConsoleSource accept both success and failure, and RconConsoleSource.shouldInformAdmins defers to the broadcast-rcon-to-ops property. BaseCommandBlock’s source accepts success only under GameRules.SEND_COMMAND_FEEDBACK and informs admins only under GameRules.COMMAND_BLOCK_OUTPUT, which is what the second of those game rules is for; a command block with Track Output off gets no source of its own. Each player has one, and so does /debug function’s tracer. CommandSource.NULL refuses everything, and the game hands it to several sources with nobody to talk to — a sign running its own click command, a text component being resolved, that command block. The command block is CommandBlock, whose own redstone edge, tick and chain walk decide when it runs, and the CommandBlockEntity that holds a BaseCommandBlock: a stored string and a source built for each run.

Arguments that are recipes, not values

Three argument families do not produce a value at all. They produce something evaluated against the CommandSourceStack at run time, which is why one parsed command means different things at different links of an /execute chain.

Coordinates holds relativity, not a position. WorldCoordinates is the implementation — three WorldCoordinates, each a value plus a is this relative flag — and Coordinates.getPosition resolves it against the source, so ~ means something different at every link and a single parsed argument yields N positions in a forked execution. LocalCoordinates (^ ^ ^) is the interesting one: it builds a basis from the source’s rotation and its EntityAnchorArgument.Anchor, so it is the only Coordinates shape that depends on eye height — it is not itself an argument type, and both Vec3Argument and BlockPosArgument can produce one. SwizzleArgument parses an axis subset (xz), and /execute align is its only user.

EntitySelector is a compiled query, not a parse tree — thirteen final fields with no grammar in them, assembled by EntitySelectorParser from EntitySelectorOptions and resolved against a CommandSourceStack much later, which is why one parsed selector yields a different set at every link of a chain. ScoreHolderArgument and GameProfileArgument reimplement the same selector-or-literal fork for their own value types. The grammar, the twenty-one options, the two-phase permission check and the two data structures a selector can be resolved against are entity selectors.

FunctionArgument reads an id and nothing else, deferring the lookup to execution — which is what lets a function be compiled against a null server (functions and macros).

The parser under the parser

Five argument types and the whole SNBT reader are not hand-written StringReader walks. They are grammars, written against net/minecraft/util/parsing/packrat — Mojang’s own parser-combinator framework, with Term as the combinator algebra, Dictionary and NamedRule binding named productions, Scope as the typed capture environment, and CachedParseState as the memo table keyed by position and rule. That memo table is the packrat in the name: a backtracking grammar that would otherwise re-parse the same prefix once per alternative looks it up instead.

The reason it matters to a command page is ErrorCollector and SuggestionSupplier: the grammar produces completions as a by-product of failing. A hand-written argument type can only suggest at a token boundary it thought to check; a grammar knows every terminal that could have continued the parse, which is why /clear @s minecraft:diamond_sword[… still completes mid-token. The consumers are exactly nine — ComponentArgument, NbtTagArgument, ResourceOrIdArgument, StyleArgument, ItemPredicateArgument, ComponentPredicateParser, and TagParser / SnbtGrammar / SnbtOperations on the NBT side (codecs, NBT and JSON).

The tree on the wire

What the packet carries

Commands.sendCommands makes a deep copy of the dispatcher’s tree, filtered by each node’s requirement for that player’s source (Commands.fillUsableCommands), and serialises it. An argument node carries the registry id of its ArgumentTypeInfo plus whatever the template writes: usually nothing (SingletonArgumentInfo writes zero bytes), often a flags byte from ArgumentUtils.createNumberFlags or EntityArgument’s single / players-only pair, sometimes a registry key. The client then builds real parsers from those templates against its own CommandBuildContext, which is why a data pack’s biomes and dialogs are parseable on the client for free.

Six argument types for one idea

One family is worth naming because a reader meets it constantly and never under one name. ResourceArgument, ResourceKeyArgument, ResourceOrIdArgument, ResourceOrTagArgument, ResourceOrTagKeyArgument and ResourceSelectorArgument are six argument types over the same idea — an id resolved against a registry, most of them through the CommandBuildContext — differing in what they hand back (a Holder, a bare ResourceKey, an inline literal, a tag as well as an element) and in whether they accept a glob. Only ResourceSelectorArgument does, which is why /test run * works and /give @s * does not. Beneath them, the five classes of commands/synchronization/brigadier are the wire descriptions of Brigadier’s own primitive types — StringArgumentSerializer and the four ArgumentTypeInfos for integer, long, float and double; with the boolean, which needs no class of its own, they are the only argument types in the game the game did not write.

What is registered, and what is on the wire

Commands.validate is what keeps that honest — though only in development: Bootstrap calls it under SharedConstants.IS_RUNNING_IN_IDE alone, so a shipped client never runs it. It throws if any registered argument type is missing from ArgumentTypeInfos. The two numbers a reader will want do not count the same thing: forty argument-type classes sit directly in net/minecraft/commands/arguments, with more in its blocks, item, coordinates and selector subpackages, while sixty-two entries are registered in ArgumentTypeInfos — a class may register several: the six resource types above are twelve entries, ResourceOrIdArgument registering seven through its nested subclasses.

Two surprises, and what /reload does not send

Two things about ClientboundCommandsPacket surprise people. It has exactly one call site, PlayerList.sendPlayerPermissionLevel, so the tree and the op-level entity event are always sent together — on join, respawn, a dimension change, op and deop, and the singleplayer toggles for cheats and guest command access, and not after /reload. And an unknown argument type deletes the node and everything under it: a modded type reaching a vanilla client decodes to a null stub, ClientboundCommandsPacket.NodeResolver substitutes a bare RootCommandNode, the children are resolved and attached to that throwaway root, and the parent then skips any child that is a RootCommandNode — so the node and everything under it vanish from the tree the player can see. The packet is not rejected, as long as the unknown type wrote no bytes of its own and its node names no suggestion provider. What the filtering means, and the second elision that rides the same packet, is permissions.

One more thing crosses the wire from here and belongs to nobody else: ClientboundCustomChatCompletionsPacket, a server pushing arbitrary completions for ordinary chat messages into the chat box, add / remove / set.

/reload builds a whole new Commands and a whole new dispatcher inside ReloadableServerResources and tells nobody. The consequence is narrower than the folklore on either side: both server-side parses read through MinecraftServer.getCommands and pick up the new dispatcher immediately, so a newly added function does tab-complete after a reload — that completion is an ask_server round trip. What goes stale on the client is the tree’s shape and its flags, which no vanilla data pack can change — and the tree is not re-sent, because its only sender is the permission-level call above (the resource system owns the rest of what /reload does).

Commands that are a door to somewhere else

Most of net/minecraft/server/commands — the atlas has its size — is a thin lambda over machinery another page of this book owns. A reader looking for “how does /locate work” wants the mechanism page, so here is the index.

commandwhat it really reacheswhere that lives
LocateCommandthree barely related parts: LocateCommand.locateStructure can drive world generation on the server thread, because deciding whether a structure is at a chunk means asking the structure check; LocateCommand.locateBiome asks the biome source and never reads a stored palette; LocateCommand.locatePoi asks the POI indexstructure placement, biomes
FillBiomeCommandwrites the biome palette of the affected sections and resends them — the only command that edits a chunk’s biomeschunk anatomy
PlaceCommandfour doors: a feature with no placement layer, by id or inline, a whole structure, jigsaw assembly directly, and a structure template with rotation, mirror, integrity and a seedfeatures and placement, jigsaw and templates
LootCommand, ItemCommandsItemCommands.applyModifier runs a loot function over an existing stack — /item modify, and the from … <modifier> forms of /item replace, fill and override. A slot is named in the numbering player anatomy decodes, and /item also takes a range of them or a slot source by id or inline. Both take a table or modifier through ResourceOrIdArgument, so an inline literal works where an id doescontexts and predicates for which set each runs in, loot tables for the functions themselves
EnchantCommand, ExperienceCommandthin faces over two systemsenchanting, hunger and experience
ExecuteCommand, FunctionCommandnot commands so much as the front end of the enginethe execution engine, and functions and macros for what /function hands it
ScoreboardCommand, TeamCommand, TriggerCommand, DataCommandsthe commands that write the scoreboard and stored NBT, beside execute storescores, teams and stored data

And one class of command a reader will look for in a shipped game and not find. Commands registers DebugConfigCommand, RaidCommand, DebugPathCommand, DebugMobSpawningCommand, WardenSpawnTrackerCommand, SpawnArmorTrimsCommand and ServerPackCommand only when SharedConstants.DEBUG_DEV_COMMANDS or SharedConstants.IS_RUNNING_IN_IDE is set, and ChaseCommand behind a flag of its own. DebugConfigCommand is additionally registered only in a dedicated server’s command set, which matters: it is the only vanilla caller of the play-to-configuration transition and back (protocol phases).

Signed arguments, in one paragraph

MessageArgument is the only signed argument in the game — the sole implementor of SignedArgument — and seven command classes register it under ten literals a player can type: /ban-ip, /ban, /me, /kick, /say, /msg with its /tell and /w redirects, and /teammsg with /tm. All of SignableCommand, ArgumentSignatures, ArgumentVisitor and CommandSigningContext exists to serve them: ArgumentVisitor.visitArguments walks a parse to find which arguments need a signature, and the map carries them afterwards. A command with signable arguments sent unsigned is refused outright when the server enforces secure profiles, and a signature that does not match the parse breaks the player’s whole message chain (chat and signing).

Where to look

Commands for what exists and what a failed parse is called; CommandSourceStack for what a command knows; ArgumentTypeInfos for the catalogue of argument types and their wire forms; EntitySelectorOptions for the grammar players write; CommandSuggestions for everything that happens while you type; and ClientboundCommandsPacket for the one place the two sides agree on a shape.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Permissions

Verified against Minecraft 26.3 · Part XIII · You are a level-four operator, you type /msg, and the server’s own answer to may this player send a chat command is no — because an operator’s permission set grants exactly one thing that is not a command level.

Op yourself to four, the highest rung there is, and ask the server whether you hold Permissions.CHAT_SEND_COMMANDS — one of five named capabilities the class Permissions holds, each of them a Permission.Atom. It says no. Not because you are restricted — because the set you were given is a LevelBasedPermissionSet, and its answer to any permission that is not a command level is false, with one hard-coded exception. The chat atoms live only in the client’s set, which the server never sees and never sends. There are two permission universes in this game, and they overlap only where the client runs the server’s checks, its rungs and its entity-selector atom, against its copy of your op level.

That is the shape of the model, and the thing to unlearn: a permission is not an integer. The five levels are there, numbered nought to four and stored as integers in ops.json, but a command node does not ask for a number. It asks a PermissionSet a question, and a set is free to answer however it likes.

The cast

classwhat it decideswhere it lives
Permissionwhat is being asked for: a Permission.Atom (a named capability with an Identifier) or a Permission.HasCommandLevel (a rung)both jars
Permissionsnine permissions as constants — four rungs and five atoms; the client mints one more of its own. The plural class is the catalogue; the singular one is the shapeboth
PermissionLevelthe five rungs — all, moderators, gamemasters, admins, owners — and PermissionLevel.isEqualOrHigherThanboth
PermissionSetthe answer. An interface with one abstract method, PermissionSet.hasPermissionboth
LevelBasedPermissionSetthe ordinary answer: an interface with five constants, one rung eachboth
PermissionSetUnionthe OR of several sets, and the rule that a union may not contain a unionboth
PermissionCheckwhat a node demands: PermissionCheck.Require or PermissionCheck.AlwaysPassboth
PermissionProviderCheckthe Predicate Brigadier holds, over anything implementing PermissionSetSupplierboth
ChatAbilitiesholds the client’s own set, built by subtraction from local reasonsclient only

net/minecraft/server/permissions holds every row above but ChatAbilities, which is the client’s and sits with the client’s chat code: a small package that changes how everything above it is written.

A question, an answer, and a check

Three types do three jobs that a single integer used to do, and keeping them apart is what makes the rest legible.

classDiagram
    class PermissionProviderCheck {
        PermissionCheck test
    }
    class PermissionCheck {
        <<interface>>
        boolean check(PermissionSet)
    }
    class Require["PermissionCheck.Require"] {
        Permission permission
    }
    class AlwaysPass["PermissionCheck.AlwaysPass"]
    class PermissionSet {
        <<interface>>
        boolean hasPermission(Permission)
    }
    class Permission {
        <<interface>>
    }
    class Atom["Permission.Atom"] {
        Identifier id
    }
    class HasCommandLevel["Permission.HasCommandLevel"] {
        PermissionLevel level
    }
    class LevelBasedPermissionSet {
        <<interface>>
    }
    class ChatAbilities

    PermissionProviderCheck --> PermissionCheck : the check
    PermissionCheck <|.. Require
    PermissionCheck <|.. AlwaysPass
    Require ..> PermissionSet : asks the source's set
    Require --> Permission : the question
    Permission <|.. Atom
    Permission <|.. HasCommandLevel
    PermissionSet <|-- LevelBasedPermissionSet
    PermissionSet <|.. PermissionSetUnion
    ChatAbilities --> PermissionSet : holds one

The three roles as types — the check a node holds at the top, the question it asks, and the set that answers; a hollow arrowhead reads as “is a kind of”, a solid arrow as “holds”, and the one dotted arrow without a hollow head is the single call that joins them.

Permissions holds nine, and the split is four to five: four Permission.HasCommandLevel constants — one per rung above zero — and five Permission.Atoms, of which one is the entity-selector gate and the other four are the chat abilities below.

The question is data: both shapes are records, both are codec-dispatched over BuiltInRegistries.PERMISSION_TYPE, and Permission.CODEC accepts an atom written as a bare identifier as well as the full dispatched form. The answer is behaviour: PermissionSet has one abstract method, so every set in the game above is a lambda or a small object, and there is no set-of-permissions data structure anywhere but Permissions.CHAT_PERMISSIONS and the client’s chat code that copies it into ChatAbilities. The check is what a command node carries: Commands.hasPermission wraps a PermissionCheck in a PermissionProviderCheck, and that predicate is what Brigadier consults.

Ninety-seven call sites of Commands.hasPermission, which is every requirement predicate on every command node in the game. Ninety-six are server-side command registrations; the ninety-seventh is on the client, in ClientPacketListener’s node builder.

Two things about LevelBasedPermissionSet decide most of this page. It is an interface with five constants, not a class carrying a level, so LevelBasedPermissionSet.GAMEMASTER is a singleton and comparing two sets is comparing two references. And its LevelBasedPermissionSet.hasPermission tests a Permission.HasCommandLevel against its own rung, grants Permissions.COMMANDS_ENTITY_SELECTORS from gamemaster upward as a special case written into the method, and returns false to every other atom. An operator does not have everything; an operator has a number and one exception.

Union is where that bites, and the rule is what its name says: PermissionSet.union falls through to PermissionSetUnion, which ORs its operands, so a set holding an atom and a set holding a rung together satisfy both. The exception is the case the server uses. LevelBasedPermissionSet.union of two level-based sets never builds a PermissionSetUnion at all, and it does not take the higher of the two: both branches of the override return the lower-levelled set, so it is a minimum. CommandSourceStack.withMaximumPermission is that union, which for these sets takes the minimum and so works as the ceiling its name promises — which is how a function body is held down to gamemaster however high the caller sits (functions and macros).

Where a set comes from

MinecraftServer.getProfilePermissions is the whole of it — in singleplayer through its override, IntegratedServer.getProfilePermissions — and it returns a LevelBasedPermissionSet, never a union, never an atom set. It is consulted afresh every time ServerPlayer.permissions is called; nothing is cached on the player.

Its cascade is short. Not on the operator list at all, and you get LevelBasedPermissionSet.ALL — rung zero, and marked deprecated where it is declared. On the list, and the entry’s own set wins — ServerOpListEntry holds a LevelBasedPermissionSet, built when the file was read from the integer the file stores, which for an entry /op wrote on a dedicated server is the op-permission-level property as it stood then (the management API’s operator methods can write any level). Singleplayer’s override answers first. The owner gets LevelBasedPermissionSet.OWNER when the world allows cheats and rung zero when it does not; any other player gets LevelBasedPermissionSet.GAMEMASTER when the world allows cheats and the host has switched on Command Access for other players, rung zero when cheats are on without it, and the cascade above when cheats are off.

The integer survives at every edge of that model, which is worth knowing before you go looking for a permissions file. ops.json stores a number. server.properties stores a number. The op level reaches the client as one of five values on ClientboundEntityEventPacket. And PermissionLevel.byId clamps rather than failing, so an ops.json hand-edited to level 9 is an owner and level −1 is rung zero. What does not exist anywhere is a data pack that grants a permission: PermissionCheck.CODEC has exactly one consumer in the whole game, ArgumentUtils.serializeNodeToJson writing the generated command report, and both type registries are bootstrapped in code by PermissionTypes and PermissionCheckTypes.

The requirement is consulted inside the parse

This is the design decision a server administrator feels most often, and it is not a message-formatting choice. Brigadier evaluates a node’s requirement predicate while walking the tree, so a node you may not use is a node that is not there. Commands.getParseException then reports an empty parse range as an unknown command and a non-empty one as an unknown argument.

An unopped player typing /give is told there is no such command. The server cannot tell them otherwise without a second parse, and it does not do one.

One permission is not settled by the parse alone. Permissions.COMMANDS_ENTITY_SELECTORS is tested at parse time and again when the selector is resolved against the world, because a parsed selector outlives its parse — which is exactly what an /execute chain relies on, and why entity selectors owns the two checks and the route that reaches only the second.

Two more consequences worth naming. Commands.LEVEL_MODERATORS gates nothing: the rung exists, is settable and is stored, and no vanilla command asks for it. And the ninety-seven gates divide four ways. Ninety-three name a level constant outright — sixty-eight gamemaster, sixteen admin, nine owner. Two are the ternary in SeedCommand and VersionCommand, which drop to Commands.LEVEL_ALL when the server is the integrated one, and those are the only two appearances of that constant in the game. The last two name a PermissionCheck rather than a rung: GameModeCommand.PERMISSION_CHECK, which the end of this page is about, and the client’s own synthetic restricted-command check.

What the client is allowed to believe

The client has permissions of its own, and they are not a copy of the server’s — no packet carries a PermissionSet. It has four sources of belief, and they behave differently enough to be worth separating. Three of them the server sent or the machine decided; the fourth is the server’s own constants, which the two sides literally share.

The op level, which arrives on an entity event and is mapped by LocalPlayer.handleEntityEvent onto one of the five sets — on an integrated server too, where the host’s own LocalPlayer learns its level the same way. Note the bottom rung: level zero maps to PermissionSet.NO_PERMISSIONS, not to LevelBasedPermissionSet.ALL. The two are indistinguishable in practice — rung zero satisfies no level and no atom either — but the client’s copy is a different object from the server’s.

The command tree, whose nodes were filtered for this player before being sent (Brigadier and commands for what the packet carries). Two elisions ride that packet and they mean different things. A node whose requirement the player failed is simply absent, and the filter is recursive, so a gated literal takes its whole subtree with it. A node that a no-permission source would fail is flagged (ClientboundCommandsPacket.FLAG_RESTRICTED) — “this needs some permission”, asserted independently of you. Commands computes that flag against a source it builds once from PermissionSet.NO_PERMISSIONS, and only for nodes that already survived your own filter.

Its own atoms. ClientPacketListener mints a single synthetic Permission.Atom for restricted commands and keeps two sources over its one dispatcher: the ordinary one, whose set is the player’s own OR-ed with that atom, so restricted nodes still highlight and complete; and a no-permission one. ChatAbilities holds the other client-only set, and it is built the opposite way round from everything else here — it starts from all four chat atoms granted and lets each ChatRestriction remove some. The four are what this client will do at all: send messages, send commands, accept messages from other players, and accept system messages; a client with the first taken away refuses before the server is ever asked (chat and signing). Three of the four ChatRestrictions that can remove them are decisions the machine you are sitting at makes — two settings of the chat option and a launcher flag — and the fourth, ChatRestriction.DISABLED_BY_PROFILE, comes from the account service: a user flag fetched with your profile. The client’s chat permissions are never granted by the game server; they are only ever taken away, and only ever from outside it.

The server’s own constants, read locally. The client runs server permission checks against its own set in several places — WorldOptionsScreen gates Allow Cheats in a hardcore world on Permissions.COMMANDS_OWNER and its game-rule, game-mode and difficulty buttons on Permissions.COMMANDS_GAMEMASTER, and KeyboardHandler gates two debug keys and reads a third — and almost all of those are the client asking its own copy a question the server will ask again later. One is shared outright. GameModeCommand.PERMISSION_CHECK, a PermissionCheck.Require for gamemaster exported from the command class, is read by its own command registration, twice by KeyboardHandler — the second is why F3+F4 refuses to open the switcher at all, with debug.gamemodes.error, rather than opening a greyed-out one — once by GameModeSwitcherScreen against LocalPlayer’s own set when it sends the switch, and once more by ServerGamePacketListenerImpl when the packet arrives. One constant, five references in four classes, two sides of the network: the one PermissionCheck both sides read; the other permissions they share are the rungs and the entity-selector atom.

Asking a question the client cannot answer

Put those two client sources together and you get the one thing the client can diagnose: it can tell “you would need permission for this” apart from “that is a typo”, for a command it was asked to send on your behalf. A dialog button and a chat click event both route through ClientPacketListener.sendUnattendedCommand, whose two callers are Screen.clickCommandAction and an adapter ClientPacketListener builds for itself. A sign does not. A sign’s click command reaches every client that loads the sign, as part of its text, but no client runs it: SignBlockEntity runs it on the server when the sign is used, and only when the sign’s allow_op_features flag is set, through a CommandSourceStack it builds itself at a hard-coded LevelBasedPermissionSet.GAMEMASTER. There is nothing for the client to vet, because the client never sends it. The figure is the route the other two take.

flowchart TD
    IN["an unattended command — a dialog button or a chat click"]
    IN --> P1{"parses with your set, restricted atom added?"}
    P1 -- no --> E1["PARSE_ERRORS"]
    P1 -- yes --> P2{"any signable argument?"}
    P2 -- yes --> E2["SIGNATURE_REQUIRED"]
    P2 -- no --> P3{"parses with NO_PERMISSIONS too?"}
    P3 -- no --> E3["PERMISSIONS_REQUIRED"]
    P3 -- yes --> OK["NO_ISSUES — sent, no screen"]
    E1 --> RUN["ConfirmScreen — its button sends it"]
    E3 --> RUN
    E2 --> SUG["ConfirmScreen — its button puts it in the chat box, or copies it"]

ClientPacketListener.verifyCommand’s three tests in their order, and where each answer leads — only the clean command leaves with no screen, and the signed one is never sent from here at all.

ClientPacketListener.verifyCommand parses the same string against both sources and reads the difference. Succeeding with the ordinary source and failing with the no-permission one means some node on the path was gated, or an argument used a selector — which is as much as the client can ever know, because it was never told which permission or whose. Between the two parses sits a test that has nothing to do with permissions: SignableCommand.hasSignableArguments. An unattended command cannot be signed, so one that needs a signature is refused outright — its ConfirmScreen offers to put the command in the chat box for you to send yourself, or, when a screen is to follow or chat commands are off, to copy it. Three of the four outcomes pop a confirmation screen; the fourth, ClientPacketListener.CommandCheckResult NO_ISSUES, sends with no screen at all. So an unattended command that is merely unusual is always shown to you first — but a clean one goes straight out, and a sign’s command never came this way to begin with.

Which answers the operator at the top of this page. Their /msg goes through, and it goes through because nothing on the server’s side of the wire ever asks for Permissions.CHAT_SEND_COMMANDS: the /msg node carries no requirement at all, the atom lives only in this client’s ChatAbilities, and the one participant that consults it is the machine the player is sitting at. The server’s no is real and it is never asked. That is what two permission universes buy — and where they overlap, at the rungs and the selector atom, is The server’s own constants, read locally above.

For a 1.21-era reader. PlayerList.setAllowCommandsForAllPlayers is gone: singleplayer answers through its own override, IntegratedServer.getProfilePermissions, which gives a guest gamemaster behind the host’s IntegratedServer.setGuestCommandAccess toggle. And the host’s LocalPlayer is no longer set directly by IntegratedServer.publishServer: it learns its level from the entity event like any other player.

Where to look

PermissionSet first — seventeen lines, and the whole model is in them. Then LevelBasedPermissionSet for the two special cases that decide everything, Permissions for its nine, and Commands.hasPermission for the one idiom every requirement in a command registration uses. MinecraftServer.getProfilePermissions for where a player’s set is decided, and ClientPacketListener.verifyCommand for the only place the client reasons about a permission it was never told about.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Entity selectors

Verified against Minecraft 26.3 · Part XIII · you type /kill @e[type=!player,distance=..8,sort=nearest,limit=1], and the characters inside the brackets decide, between them, which levels are searched and which of two entirely different data structures answers.

Put a command block at the Overworld origin and give it /tp @p 0 100 0. One player is two hundred blocks away across the Overworld; another is standing on the Nether roof directly “above” the block, at the Nether’s own 0, 128, 0. The command block teleports the one in the Nether. @p is not “the nearest player in this world”: it is the player whose raw x, y and z are nearest, chosen from the list of everybody on the server, compared as though the dimensions were stacked in the same coordinate space. (Type the same command yourself and you always win it — a player’s own source sits at distance zero from a player’s own position.) Nothing in a selector is confined to one level unless you write one of seven options that says so — and four of those seven, distance and the three box deltas, are also what decides whether the game walks every entity in the level or asks the chunk sections for a box.

That is the shape of the whole subject. A selector looks like a filter language, and it is one, but many of its twenty-one option names do more than filter: they write the query plan. This page is about which is which.

The cast

classwhat it decideswhen it runs
EntitySelectorParserthe reader, the grammar and thirty-one half-built fields besides it. It owns the selector’s own syntax; the brace grammars of scores and advancements are read by the handlers themselvesparse time
EntitySelectorOptionsthe name-to-handler map — twenty-one entries, filled once by Bootstrap.bootStrap and never againbootstrap
InvertableSetOptionStatethe three-state machine behind type=!zombie,type=!skeleton: one positive assertion or any number of negations and tags, never bothparse time
SetOnceOptionStateone boolean, for the four options with no parsed field to test for emptiness — limit, sort, scores, advancements. The other ten once-only options enforce it by checking their own fieldparse time
EntitySelectorthe compiled query: thirteen final fields, and no grammar left in thembuilt at parse time, run later
EntityArgumentfour argument shapes (single or many, entities or players) and the parse-time rejections that enforce themparse time and on the wire
CommandSourceStackthe only thing a selector can be resolved against — origin, level, server, permission setresolve time
LevelEntityGetterAdapterthe fork in the road: an EntityLookup walk, or an EntitySectionStorage box query (entity lifecycle owns both)resolve time

net/minecraft/commands/arguments/selector holds five classes, and every one of them is in the server jar and the client jar. That matters later.

Three stages, and the last one is not on the parser’s clock

flowchart LR
    S["the argument text"]
    subgraph P["PARSE — at parse time"]
        direction TB
        P1["the head sets the defaults"]
        P2["each option name finds its handler"]
        P3["the handler writes a field or adds a test"]
        P1 --> P2 --> P3
    end
    subgraph C["COMPILE — at parse time"]
        direction TB
        C1["thirteen final fields"] ~~~ C2["a box, or none"] ~~~ C3["an origin"]
    end
    subgraph R["RESOLVE — per execution"]
        direction TB
        R1["which levels"]
        R2["which of two structures"]
        R3["order, then cut to the limit"]
        R1 --> R2 --> R3
    end
    S --> P
    P --> C
    C --> R

The three stages a selector passes through — the first two while Brigadier parses, and the third on the Server thread each time a command asks for its argument; the three boxes under COMPILE are what it produces, not steps.

The first two stages happen while Brigadier walks the command tree; the third happens when the command’s lambda asks for its argument. In between, the selector is an ordinary immutable object sitting in a parsed context, which is why an /execute chain can resolve the same parsed selector once per source and get a different answer each time (the execution engine).

Parse: six heads and twenty-one names

EntitySelectorParser.parseSelector reads the character after the @ and sets four things — a result limit, whether non-players are in scope, an order, and sometimes a type. There are six, and no more: the default branch of that switch throws.

headlimitnon-playersorderextras
@aunboundednoarbitrarytyped to player
@eunboundedyesarbitraryadds an Entity.isAlive test
@n1yesnearestadds an Entity.isAlive test
@p1nonearesttyped to player
@r1norandomtyped to player
@s1yesarbitraryresolves to the source’s own entity

Only @e and @n add that test, and only LivingEntity.isAlive makes it mean anything — it is the override that adds “and has health left” to the base class’s “and has not been removed”. So a player sitting on the death screen is invisible to @e and @n, and still a target for @a, @p and @r — though twenty ticks after death the body leaves the level’s own player list, so a world-limited selector stops finding it.

Then the bracket. EntitySelectorParser.parseOptions reads a name, looks it up through EntitySelectorOptions.get, and hands the reader to the handler it finds. Twenty-one names are registered, counted by reading every EntitySelectorOptions.register call in EntitySelectorOptions.bootStrap.

optionwhat its handler doesrepeatable?
namecompares Nameable.getPlainTextNameone positive, or many negatives
teamcompares Entity.getTeam — the empty string means no teamone positive, or many negatives
gamemodecompares ServerPlayer.gameMode, and drops non-playersone positive, or many negatives
typean id sets the EntityTypeTest; a tag only adds a testone positive id, or many negatives and tags
tagreads Entity.entityTags — empty means no tags at allfreely
nbtserialises the candidate and compares with NbtUtils.compareNbtfreely
predicateruns a loot condition in LootContextParamSets.SELECTOR (contexts and predicates)freely
scoresa brace map of objective to MinMaxBounds.Intsonce
advancementsa brace map of advancement to done-ness, players onlyonce
limitsets the result cap, rejects anything below 1once, never on @s
sortpicks one of four ordersonce, never on @s
distancea MinMaxBounds.Doubles, rejects negatives, world-limitsonce
levela MinMaxBounds.Ints, rejects negatives, drops non-playersonce
x, y, zoverride one axis of the resolve origin, world-limitonce each
dx, dy, dzbuild the box, world-limitonce each
x_rotation, y_rotationangle ranges that wrap through 360once each

Three of the twenty-one are freely repeatable — tag, nbt and predicate — because they are the three registered as always available, with no state object behind them at all. That is why tag=a,tag=b is the idiom for “has both” and type=zombie,type=skeleton is a parse error rather than an empty result: InvertableSetOptionState moves to a terminal state the moment a positive id is accepted, and EntitySelectorOptions.get then refuses the whole option by name before its handler ever runs. Its other terminal state is the permissive one — after a negation or a tag, more negations and more distinct tags are allowed, which is why type=!zombie,type=!skeleton works and why two entity tags can be written together and AND.

Compile: what a box is, and where it comes from

EntitySelectorParser.getSelector runs once, at the end of the parse, and turns the pile of fields into thirteen final ones. Two of the thirteen are the interesting decisions.

The box. If any of dx, dy or dz was written, EntitySelectorParser.createAabb builds the box from those three, treating the missing ones as zero and adding one to each maximum — a dx=0 volume is one block wide, not zero. Otherwise, if distance was written and has a maximum, the box is a cube of that radius, again with one added to the positive corner. Otherwise there is no box. So distance=8.. — a minimum with no maximum — produces no box at all, and dx=3,distance=..64 ignores the distance entirely when choosing the box, because the delta branch wins.

The origin. If any of x, y or z was written, the position becomes a function that overrides those axes of the source’s position and keeps the rest. Otherwise it is the identity. This is applied per execution, which is what makes x=0 mean the same thing everywhere while every axis left unwritten follows the source at each link of a chain.

Everything else that was written is already a test in a list, in written order — with exceptions appended afterwards whatever order they appeared in. EntitySelectorParser.finalizePredicates adds the two rotation tests and the experience-level test last, and EntitySelector.getPredicate then appends up to three more at resolve time: the feature-flag test, the exact box test and the range test. Util.allOf evaluates them in that order and short-circuits, so the range test, a few multiplications, runs after an nbt comparison that serialised the whole entity.

Resolve: which levels, which structure, which order

flowchart TD
    A["EntitySelector.findEntities"] --> C{"a bare name or a UUID?"}
    C -- name --> N["PlayerList.getPlayerByName"]
        C -- UUID --> UE["Level.getEntity, level by level"]
    C -- neither --> D{"is it the source itself?"}
    D -- yes --> S["the source's own entity, if it passes"]
    D -- no --> E{"world-limited?"}
    E -- "yes: this level" --> B2{"non-players in scope?"}
    E -- "no: every level" --> B2
    B2 -- no --> PL["a player list, walked"]
    B2 -- yes --> H{"is there a box?"}
    H -- yes --> I["EntitySectionStorage — only the sections near the box"]
    H -- no --> J["EntityLookup — every visible entity, one by one"]
    I --> K["sort unless arbitrary, then cut to the limit"]
    J --> K
    PL --> K

How a compiled selector finds its entities — the players-only selector asks the same questions in the same order, so the figure asks whether non-players are in scope only where the two answers part; the three shortcuts at the top return at once, with no sort and no limit.

Three shortcuts come first. A bare player name goes to PlayerList.getPlayerByName, since a name always takes non-players out of scope; a UUID is looked up with Level.getEntity, level by level, since a UUID always brings non-players into scope; and @s tests the source’s own entity against the selector’s tests. None of the three is sorted or cut.

World-limited is a parse-time flag, not a runtime one. Exactly seven option handlers call EntitySelectorParser.setWorldLimited: distance, x, y, z, dx, dy and dz. Write none of them and EntitySelector.findEntities iterates every level the server has. /kill @e[type=item] is a three-dimension operation.

The two structures are genuinely different. With a box, Level.getEntities goes through EntitySectionStorage, which visits only the accessible non-empty 16-cubes the box overlaps once widened by two blocks sideways and four below. Without one, ServerLevel.getEntities goes through EntityLookup, which walks the level’s entire visible-entity map and calls EntityTypeTest.tryCast on each — EntityTypeTest being the small “is this the type I asked for, and give it to me typed” interface the whole entity-fetching API is generic over. There is no index by entity type. type=zombie narrows nothing structurally; it is a cast applied one entity at a time, ahead of the tests. Of the seven, only distance and the three deltas narrow the entity walk within a level, and only when they add up to a box; x, y and z only confine it to one level and move the box.

And the box path finds things the walk cannot. Level.getEntities also offers each ender dragon’s eight EnderDragonPart sub-entities to the type test and the predicate, and every part reports its parent’s type. So a box query in the End can return the dragon and up to eight more “ender dragons”; the same selector without a box returns one.

Players never get a box. Both player paths — ServerLevel.getPlayers for one level, PlayerList.getPlayers for the whole server — are linear walks of a list, and the box survives only as one more test. @a[distance=..8] costs what @a costs.

Sort is what takes the limit away. EntitySelector.getResultLimit returns the parsed limit only when the order is arbitrary, and the unbounded value otherwise, because a sort has to see everything before it can know what comes first. When it is the parsed limit, it reaches the level query as an early abort. So @e[limit=1] stops at the first match, and @e[limit=1,sort=nearest] collects every match in range, sorts the list and throws all but one away. @n and @p live in the second mode unless told otherwise: their heads set the nearest order, so they collect first and cut afterwards, and only an explicit sort=arbitrary puts them back in the first.

So the query plan is written by thirteen of the twenty-one names: the seven above, which world-limit the search and build or place the box; limit, which aborts it early; sort, which un-decides part of it by taking the limit away; and the four that drop non-players, level, gamemode, advancements and a positive type=player, which send the whole search to a player list. The other eight only filter what the plan returns.

One permission, checked in two places, for two different reasons

The gate is a single atom, Permissions.COMMANDS_ENTITY_SELECTORS, granted by LevelBasedPermissionSet from gamemaster upward as the one hard-coded exception in that interface (permissions). It is the only permission in the game checked in two different phases, it is read in seven places, all of them under commands/arguments, and they divide cleanly in two.

At parse time, EntitySelectorParser.allowSelectors asks the source and the answer becomes a constructor argument. If it is false, an @ throws EntitySelectorParser.ERROR_SELECTORS_NOT_ALLOWED immediately, and a bare name or UUID still parses. This is the check that matters for the three vanilla commands that take a selector-capable argument with no permission requirement at all — MsgCommand, EmoteCommands and TeamMsgCommand, so /msg, /tell, /w, /me, /teammsg and /tm. For an ordinary player this atom is the only gate on those.

MessageArgument alone treats a refusal as a formatting decision rather than an error: without the permission the message is taken as literal text, so an unopped /msg Bob @a sends those two characters. With the permission, an @ that is not a valid selector head is skipped and the scan continues — which is how an address like me@gmail.com survives — but a malformed selector body throws, and the whole command fails to parse.

At resolve time, EntitySelector.checkPermissions asks again, guarding on EntitySelector.usesSelector, which only EntitySelectorParser.parseSelector ever sets. A selector compiled from a bare player name is exempt. The second check is not redundant, because there is a whole route into the machinery that never passed the first one: EntitySelector.COMPILABLE_CODEC compiles a selector out of a text component — the selector content type, the score name field and the entity NBT data source — and does so with selectors unconditionally allowed, because a codec has no source to ask. The resolve-time check is what decides whether a /tellraw written by a data pack may enumerate entities, and it asks the source the component is being resolved against, never whoever wrote it.

Two more argument types write the same fork by hand

EntityArgument is not the only argument type that will take an @. ScoreHolderArgument and GameProfileArgument each reimplement the selector-or-literal fork for a value that is not an entity, and what they do on the literal side is the interesting half.

GameProfileArgument resolves to profiles, not entities. An @ builds a selector and is rejected outright if it includes non-players; anything else is read to the next space and looked up in the server’s name-to-id cache — so /ban, /whitelist and /op name a player who has never joined this session and may not exist on this server at all. That lookup is the only thing on this page that leaves the game’s own data.

ScoreHolderArgument resolves to ScoreHolders, which need not be alive. Its literal side has four branches in order — * for every tracked holder, a #-prefixed name taken as a bare string, a UUID searched across every level, and an online player — with the UUID and player branches falling back to a bare name and the wildcard alone throwing when there is nothing to return (scores, teams and stored data for what those names are for).

Both take the permission the same way this page’s own parse does, and both check the compiled selector rather than the text, as EntityArgument does — ScoreHolderArgument for its single-or-many shape, GameProfileArgument for players only.

Four argument shapes, enforced on the query and not on the text

EntityArgument comes in four shapes — one entity or many, players only or anything — and it enforces the difference on the compiled selector, during the parse, before the world is touched. A limit above one in a single-target slot is refused, and so are non-players in a players-only slot.

That is why /damage @e 1 complains before it has looked at anything: /damage, /ride and /data get entity all take the single-target shape, and @e is unbounded. /kill @e is fine, because /kill takes the many-entities shape and neither rejection can fire on it. And @s is exempt from the players-only test, so /msg @s parses and then finds nobody when the source is not a player.

The client parses selectors and cannot resolve one

This is what the cast’s five-classes-in-both-jars is for. The client parses a selector by two routes and can run neither.

EntityArgument.listSuggestions builds a real EntitySelectorParser against the client’s own permission set, parses as far as it can, swallows the exception and asks the half-finished parser for its suggestions — which is why completion inside a bracket knows which options are still legal; the tree it parses against is a real one the server sent (Brigadier and commands). And ComponentSerialization decodes a selector content type on the client with the same compiler the server uses.

What the client cannot do is run one: every find method takes a CommandSourceStack, and the client’s suggestion source is a ClientSuggestionProvider. The one place a client resolves a component that might contain a selector is ServerStatusPinger, whose ResolutionContext deliberately carries no source, so a server-list description containing a selector renders as nothing at all.

Questions players ask

Which dimension does /kill @e[type=item,limit=1] kill in? The overworld, unless there is nothing to kill there. With no world-limiting option the resolve walks MinecraftServer.getAllLevels, which iterates the server’s LinkedHashMap of levels in the order they were created — the overworld put in first, the rest in registry order — and an arbitrary-order limit reaches the level query as an early abort, so the walk stops at the first match it finds. Write sort=nearest and the bias disappears, because a sort has to see every level before it can rank anything.

What does the client suggest for an entity argument? Online player names, plus — from ClientSuggestionProvider.getSelectedEntities — the UUID of whatever your crosshair is on. Point at a cow, press tab, and it offers you that cow.

Is sort=random seeded? No. It is the JDK’s list shuffle, with no world seed and no RandomSource anywhere near it, so @r is not reproducible from a save.

Why does distance=..8 not return something 8.9 blocks away? Because the box is only a pre-filter. The cube built for a maximum of 8 spans −8 to +9 on each axis, deliberately larger than the sphere it approximates, and the exact test that follows is MinMaxBounds.Doubles.matchesSqr, which compares squared distances against pre-squared bounds and so never takes a square root.

For a 1.21-era reader. The parser’s crowd of hasNameEquals / hasNameNotEquals / hasGamemodeEquals booleans is gone, replaced by eight state objects — four InvertableSetOptionState and four SetOnceOptionState — that enforce the same rules structurally.

Where to look

EntitySelector first — thirteen fields and four find methods, and the design is in them. Then EntitySelectorParser.getSelector for the one place those thirteen are decided, and EntitySelectorOptions.bootStrap for the grammar players write. LevelEntityGetterAdapter is six methods long and is where the cost of every selector is settled. Note that the name EntitySelector is used twice in the game: this one, and an unrelated bag of predicate constants in world/entity that the mob AI and the hoppers use.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

The execution engine

Verified against Minecraft 26.3 · Part XIII · /execute as @a at @s run say hi on a server with four players: a command engine with no Java recursion, a fan-out that materialises one player at a time, and a /return that deletes work out of a queue rather than unwinding a stack.

Write a data pack that calls a function that calls itself, load it, and the server does not crash. It runs a very large number of commands, logs one line at info, and carries on. That is not a recursion limit — there is no recursion limit, and no depth game rule either. What stopped it was a budget: a count of units of work spent by one outermost command, whose ceiling is a game rule and whose exhaustion is the log line. It is that nothing in command execution uses the Java call stack.

Every construct that nests — /execute … run, /function, execute if function, /return run — is expressed as queued work on a heap-allocated deque, and the driver is a flat loop. A stack made of heap objects costs you nothing except the obvious, and buys you three things a real stack cannot give: you can inspect it, you can delete pending entries from it before they run, and a tracer can watch the work as it runs. /return and /debug function are built on that one decision.

The cast

classwhat it decidesnotes
ExecutionContextone per outermost command: the queue, the staging list, the budget, the fork limit, the tracerthe whole engine is in its ExecutionContext.runCommandQueue loop
CommandQueueEntrya Frame and an EntryAction. That is the entire unit of work—
Framenot a stack frame: a depth, a CommandResultCallback a /return feeds, and a Frame.FrameControl that knows how to delete this frame’s pending workone object shared by reference across a whole function body
BuildContextswalks the stages of a parsed chain, forking sources as it goesBuildContexts.TopLevel, BuildContexts.Continuation and BuildContexts.Unbound
ContinuationTaskthe lazy fan-out: emits one element’s entry, then re-queues itselfthe reason three or more players cost one element’s entry at a time rather than N at once
CallFunction / IsolatedCallthe only two things besides the top level that open a frameIsolatedCall’s /return cannot reach the caller
ExecutionCommandSourcethe interface the engine is generic over, which is why none of it mentions CommandSourceStackCommandSourceStack implements it
CommandResultCallbacka success flag and an integer. This pair is what “the result of a command” means everywhere in the gameCommandResultCallback.EMPTY short-circuits

net/minecraft/commands/execution is the whole engine and it is entirely server-side. Nothing here crosses the network; only the effects of commands produce packets. What arrives is a ParseResults and a CommandSourceStack that Brigadier and commands has already built: Commands.performCommand flattens the parse into a context chain and Commands.executeCommandInContext is the door — it reads the two game rules that bound an execution, installs the context in a thread-local and drives the loop.

The queue, four moments apart

/execute as @a at @s run say hi, four players online. The command starts as one entry — a BuildContexts.TopLevel holding the whole parsed chain, at depth 0 — and the figure is the queue at the four moments after that.

flowchart TD
    subgraph T2["1 · that entry ran"]
        B1["ContinuationTask over four players"]
    end
    subgraph T3["2 · the task ran"]
        direction LR
        C1["ExecuteCommand for A"] --> C2["ContinuationTask"]
    end
    subgraph T4["3 · A's say hi ran"]
        D1["ContinuationTask — B is still not made"]
    end
    subgraph T5["4 · the task ran again"]
        direction LR
        E1["ExecuteCommand for B"] --> E2["ContinuationTask"]
    end
    T2 --> T3 --> T4 --> T5

The whole queue at four moments — an arrow inside a panel runs from the head to the entry behind it, an arrow between panels is one entry run, every entry shown shares the top frame at depth 0, and ExecuteCommand here is the leaf task in commands/execution/tasks, not the /execute command’s class of the same name.

A fork does not create frames, and it does not create entries. BuildContexts.execute walks every non-execute stage inside a single queue entry, spending one cost unit per modifier stage — here as @a and at @s, not run — no matter how many sources that stage produces, and turning a one-element source list into an N-element one. Frames are opened in exactly three places: ExecutionContext.createTopFrame, CallFunction and IsolatedCall. A hundred-player fork opens none.

The queue is a stack with a staging buffer, and that is what makes it depth-first. An action does not push directly: it appends to ExecutionContext.newTopCommands while it runs, and ExecutionContext.pushNewCommands splices that list onto the head afterwards, in order. So whatever the current action spawned runs before whatever was already pending — the semantics of a call stack, out of an ArrayDeque. A queued action is handed the ExecutionContext itself. A custom executor or modifier is handed an ExecutionControl instead, the (context, frame) pair, through which it can queue an action, read or install a TraceCallbacks, and reach its current frame — which is how /return finds the frame it discards.

The fan-out is lazy, and the arithmetic is exact. ContinuationTask.schedule queues nothing for an empty list, one entry for one element, two entries for two, and for three or more queues exactly one: a ContinuationTask that emits the current element’s entry and then re-queues itself behind it. Because staging preserves order, element i and everything it spawns runs to completion before element i+1 runs — and, from three elements up, before it is even materialised. The queue cost is constant in N.

A chain can split across entries, and only a custom modifier does it. When the stage walk meets a CustomModifierExecutor it hands off and returns mid-walk; the rest of the chain resumes later as a BuildContexts.Continuation. That is how execute if function and /return run interrupt a chain that otherwise runs to its leaf inside one entry.

Deleting work, which is what /return is

/return does not unwind and does not throw. Frame.returnSuccess pushes the value sideways into the callback the caller installed on that frame, and Frame.discard splices the abandoned work out of the queue. There is no search.

The splice is one rule. Call the discarding frame’s own depth d: pop from the head while the entry’s depth is at least d. That works because the queue is depth-first, so entries deeper than a frame are always in front of that frame’s own remaining entries — which means the rule removes exactly the callee’s pending work plus the rest of this frame’s body, and nothing older. Depth-zero frames are the special case: their frame control clears the queue outright.

The laziness pays off here too. Discarding one ContinuationTask self-entry abandons every element not yet materialised, so /return out of a thousand-line function is the same cost as /return out of a two-line one.

Who installed the callback decides where a returned value goes, and two details settle it:

  • The single-source reduction on the /return run path lives in BuildContexts.execute, not in ReturnCommand, and it is gated twice — on return mode, and on the leaf not being a CustomCommandExecutor. So return run execute as @a run function foo queues one function call per player, unreduced — and the first to finish, by returning or by falling through, discards the rest.
  • The chaining runs the opposite way from “inner onto outer”: what is chained is the source’s own callback with the current frame’s return consumer, and on the /return run function path the outer frame’s consumer is chained into the inner frame.

A result is a flag and a number, and only a function tag sums them

The result of a command is always a CommandResultCallback pair: a success flag and an integer. The engine aggregates nothing, and there is one exception, which /function makes. A fork over N players delivers N independent results to N sources, so an execute store result whose target every forked source shares — one placed before the fork, or one naming a fixed holder — writes N times and the last one wins — there is no success count. The exception is /function on a tag, which sums its members’ results, and only when the caller installed a real callback and it is not under return run.

A command typed in chat has an empty frame callback and Commands.performCommand returns nothing — yet execute store still works on it, because the result also reaches the source’s own callback, which is what ExecuteCommand.wrapStores decorated (scores, teams and stored data). FallthroughTask makes a chain that produced no sources, or a called function that ended without /return, fail rather than return nothing, and every site that queues it is inside a return or a conditional.

Six classes implement the escape hatch for a command that wants the engine rather than Brigadier’s plain “return an int”, and between them they belong to four commands: /function, /return, /debug and /execute, whose if function is one. Two of the six extend CustomCommandExecutor.WithErrorHandling — FunctionCommand.FunctionCustomExecutor and DebugCommand.TraceCustomExecutor — which routes a thrown CommandSyntaxException to both the source’s error handler and its callback, so a failing /function under execute store still writes. The other four handle their own.

What actually stops a command

Three things end an execution before its work is done. One is an exception thrown out of an action, which Commands.executeCommandInContext passes up: Commands.performCommand tells the source the command failed, and ServerFunctionManager.execute only logs it. The other two are the budget running out and the queue overflowing, and they are not the same event.

The budget runs out. ExecutionContext.runCommandQueue checks at the top of every iteration, logs at info, and breaks. The queue is not cleared; it is simply abandoned with the context. Nothing reaches the player.

The queue overflows. ExecutionContext.queueNext trips when staged plus queued entries exceed ten million — a cap on queue length, not on depth, whatever the constant that names it suggests; ExecutionContext.handleQueueOverflow clears both lists and sets a latch that silently drops every subsequent queue attempt, and the driver then logs at error. Different level, different clean-up, and a latch the budget path has no equivalent of.

What the two numbers are

Both are game rules, and both are 65536 by default. GameRules.MAX_COMMAND_SEQUENCE_LENGTH is the budget — how many units of work one outermost command may spend before the budget path above fires — and GameRules.MAX_COMMAND_FORKS is the fork limit, how many sources one stage may contribute. Both are read once, by the outermost command, so a /gamerule changed part way through a long fan-out does not take effect until the next top-level command. (There is nothing dimensional in it: ServerLevel.getGameRules returns the server’s one GameRules instance, so no level has rules of its own to pick up.) The fork limit is checked per contributing source with a greater-or-equal comparison, so the effective ceiling is one below the configured value, and when it trips the handler returns without queueing even a FallthroughTask — a /return run chain that hits it yields nothing at all rather than a failure.

Depth is not on that list, and that is the answer to the function at the top of this page. Recursion is unbounded structurally: depth is used only to order discards and to indent the tracer. What bounds a self-calling function is the budget, transitively — every call it makes spends a unit — and what bounds a fan-out is the fork limit, with the budget again at one unit a leaf.

And a budget belongs to a context, not to a command. Commands.CURRENT_EXECUTION_CONTEXT is a thread-local: a command that starts another top-level execution while one is running queues its work into the running context, to run next, rather than making a new context, and the limits were read once by the outermost call. Its top frame sits one depth deeper than the running entry’s, except under a depth-zero entry, where it shares depth zero; every vanilla re-entry is a function call whose body opens a frame one level further down, so its discards never reach the outer queue. The thread-local is null again the moment a queue drains, so two commands run back to back each open their own context — which is why every function in #minecraft:tick gets a budget of its own (functions and macros).

Where a unit is spent

The budget is spent in exactly three places — BuildContexts on a modifier stage, CallFunction on a function call, and the leaf ExecuteCommand task on an executed command — and the first has a gate worth knowing. The increment happens only when the stage carries a non-null redirect modifier, and only after the custom-modifier hand-off has been ruled out. So a plain execute run costs nothing for its redirect, and the execute if function and /return run stages are free: neither custom modifier ever reaches the counter, though what they go on to run pays like anything else. A ContinuationTask is free too, and that is the +1 an N-way fan-out does not pay: N leaves cost N, the continuation that materialised them costs nothing, and each modifier stage that forked in the first place was charged one unit, however many sources it produced.

The two commands that are part of the engine

ExecuteCommand.scheduleFunctionConditionsAndTest is how execute if function works and it is the cleverest thing in the area: instantiate each function once, wrap every source in an IsolatedCall whose callback appends that source to a list, then queue a BuildContexts.Continuation over the same mutable list, which the isolated calls fill before the continuation reads it. The staging order is what makes that safe, and the inner CallFunction opens against the isolated frame, which is why a /return inside a condition function cannot reach the caller.

/debug function installs a DebugCommand.Tracer on the whole ExecutionContext rather than per frame, so it traces everything in that context. The command refuses to nest a trace and refuses return mode, and the tracer implements CommandSource as well, so a traced function’s chat output lands in the trace file alongside the call lines.

Questions players ask

Can a function yield? No. Work it queues drains inside the same driver loop, in the same tick, before the call returns. The only escape is /schedule (functions and macros).

Why did my command fail silently inside execute if? Because conditionals are fork nodes. execute as @s, execute at @s, execute if block and friends set the forked flag on ChainModifiers for the rest of the chain, and a forked source suppresses failure messages. Putting a harmless-looking conditional in front of a command converts its errors into nothing: in forked mode Brigadier catches a plain command’s error and returns zero, which is all a trace shows of it, though the engine’s own commands, a forked /function naming a missing function among them, still write their error to a trace through CommandSourceStack.handleError.

Does /return inside a fork stop the other sources? Not normally. A forked /function has its N calls queued at once, in a plain Java loop, and each opens its own frame at the next depth when it runs, so a /return in one discards only that callee. It is return run … that sets CallFunction.returnParentFrame against the caller’s own frame, making the inner discard run at the outer frame’s depth and delete the siblings.

Where to look

ExecutionContext and Frame first — two short files that explain the whole design. Then BuildContexts for how a parse becomes work, ContinuationTask for the laziness both the fan-out and the function body ride on, and ExecutionCommandSource for why none of it names a command source.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Functions and macros

Verified against Minecraft 26.3 · Part XIII · A macro function in #minecraft:tick is reached with no arguments: it fails, silently, twenty times a second, forever — nothing logged, nothing printed, nothing counted.

Put a $-prefixed line in a function, add that function to #minecraft:tick, and the game will call it every tick with no argument compound at all. Instantiating it raises a FunctionInstantiationException, ServerFunctionManager.execute catches that one exception with an empty body, and the tick moves on. Any other exception from the same call is logged at warn; this one is the single case the manager was written to swallow, because the /function command is expected to report it instead — and #minecraft:tick is not the /function command.

That is the sharp end of a model that is otherwise very tidy: a .mcfunction file becomes a runnable thing in two steps, and for the overwhelming majority of functions the second step does nothing at all. A third stage below is the hand-off — what the engine does with the result — and it belongs to another page.

The pipeline

The figure is the whole model: which kind of function a file becomes, and where the one silent failure leaves it.

flowchart TD
    F[(".mcfunction file")]:::disk --> C["1 · compile, at reload"]
    C --> K{"a dollar line?"}
    K -- no --> PT["PlainTextFunction"]
    K -- yes --> MF["MacroFunction"]
    PT -- "2 · instantiate: itself" --> R["InstantiatedFunction"]
    MF -- "2 · instantiate" --> X{"arguments given, parses?"}
    X -- yes --> R
    X -- no --> EX["FunctionInstantiationException"]
    EX --> SW["a command reports it,<br/>ServerFunctionManager swallows it"]
    R --> Q["3 · queued: CallFunction opens a frame"]
    Q --> E["the execution engine runs the lines"]

A function’s two steps and the hand-off — compiled once at reload, instantiated on every call, and only the macro branch can fail there; the failure is reported or swallowed depending on who made the call.

The two halves of that live in net/minecraft/commands/functions (the model) and net/minecraft/server (the two managers), and both are server-side but for StringTemplate, which a dialog’s command template also runs on the client (dialogs).

The cast

classwhat it decides
ServerFunctionLibrarythe reload listener: parses every file, holds the volatile function and tag maps, and the compile-time PermissionSet
ServerFunctionManagerthe runtime face: the two tags, the source they run as, and the one empty catch
CommandFunctionthe compiled, uninstantiated function — CommandFunction.fromLines is the compiler
PlainTextFunctiona macro-free function. It is both the compiled and the instantiated form, so instantiating one allocates nothing
MacroFunctionthe other case: parameters, an eight-entry LRU, and a re-parse per miss
StringTemplatethe $(name) syntax and what a valid variable name is
InstantiatedFunctionan id and an ordered list of UnboundEntryActions. That is the entire runnable representation
FunctionInstantiationExceptioncarries a Component, which is what /function shows when it reports the failure

1 · Compile, at reload

In: the raw lines of one file. Out: a CommandFunction, or nothing at all.

CommandFunction.fromLines walks the lines. A trailing backslash joins the next one (CommandFunction.shouldConcatenateNextLine), and a continuation at end of file is an error; blank lines and # comments are skipped; a leading / is a hard error with two different messages depending on whether you wrote one slash or two; a leading $ is a macro line, kept as a template; and anything else goes through CommandFunction.parseCommand, which produces a BuildContexts.Unbound. So a compiled function line is literally a parsed context chain plus its input string, waiting for a source. CommandFunction.checkCommandLineLength caps a line at two million characters.

A syntax error on any plain line fails the whole file, which is logged at error and then simply absent from the map. There is no partial function.

Two constraints make this stage unusual, and they are the same constraint seen twice. ServerFunctionLibrary parses every file in parallel on the reload’s background executor, and it does so against a source built by Commands.createCompilationContext with a null level and a null server. Only the last stage — collecting the parses, logging the files that failed, resolving the function tags and swapping in the maps — runs on the thread that applies the reload, and the maps are volatile, the library object being built on a background thread. An argument type that dereferenced the world during parsing would fail every function that used it — which is exactly why FunctionArgument reads an id and defers the lookup. No argument type tests the constraint: the four argument types whose parse reads the source at all consult only its permissions, and those come from the dedicated server’s function-permission-level property, gamemaster by default, gamemaster always in singleplayer, and owner on the game test server.

2 · Instantiate, per call

In: a CommandFunction and an optional argument compound. Out: an InstantiatedFunction, or a FunctionInstantiationException.

For a PlainTextFunction this step returns the very same object: nothing is allocated, nothing is parsed, and the overwhelming majority of functions in the world take this path.

A MacroFunction looks up each declared parameter in the argument compound, stringifies it, and uses the ordered list of strings as a cache key over an eight-entry LRU (MacroFunction.MAX_CACHE_ENTRIES). On a miss it substitutes into every macro line and re-parses it, and a parse failure there is the exception above. Note what the cache is keyed on: the values, in parameter order — so nine distinct argument tuples cycling round will miss every time.

Stringification is where macros surprise people, because it is not SNBT for everything. MacroFunction has explicit cases for float and double (a decimal format with up to fifteen fraction digits, so 1.0 becomes 1), for byte, short and long, and for strings (the unquoted value). Everything else — integers, compounds, lists — falls through to SNBT. Integers merely happen to render bare; byte, short and long need their own cases precisely because SNBT would suffix them.

StringTemplate also decides what a parameter may be called: inside $(…), letters, digits and underscore and nothing else — any letter or digit in Unicode’s basic plane counts — checked by StringTemplate.isValidVariableName at compile time. That rule is borrowed — a dialog’s input keys obey it too, which is what lets a dialog substitute its inputs into a command (dialogs).

Three smaller rules complete the model. One $ line anywhere makes the whole file a macro function, though its non-macro lines keep their already-compiled form and are never re-parsed. A $ line containing no substitution at all is a load-time error, not a plain command. And a macro function’s instantiated variants all share one synthetic id, derived from the parameter names rather than the values.

Then: queued, and gone

In: an InstantiatedFunction and a source. Out: entries on the execution queue.

CallFunction spends one cost unit, opens a frame at the next depth, and hands the function’s line list to ContinuationTask.schedule — the same call the fan-out over an entity selector makes. A hundred-line function and a hundred-player fork are therefore the same shape in the queue, which is what makes /return cheap: discarding one self-entry abandons every line not yet materialised. Everything after this point is the execution engine.

What calls a function, and when

ServerFunctionManager.tick runs in the first profiler zone MinecraftServer.tickChildren opens — commandFunctions, with only the suspension of every player’s packet flushing ahead of it — and so before the clocks, before the time sync, before any level ticks, and long before connections and players tick, which happen after the levels (the server tick). It no-ops entirely when the tick-rate manager is not running normally, so /tick freeze suspends the tick and load tags. #minecraft:load runs once after a reload or start, and #minecraft:tick runs every tick from a list snapshotted at reload and never consulted again, so nothing can join or leave the tick loop between reloads.

That list is where the failure at the top of this page lives. Nothing in ServerFunctionManager.tick carries an argument compound, so a macro function on the tag can never be instantiated; the manager’s one empty catch swallows the exception; and the snapshot means the function is tried again on the next tick and on every tick after it, for as long as the pack is loaded. There is no counter and no log line, and the way to see it is to call the same function from a command — /function, /debug function or execute if function — each of which reports what the tick tag does not.

Each function in the tick or load tag gets its own ExecutionContext — its own copy of the budget the engine spends, rather than one shared across the tag (the execution engine).

/schedule is the one way out of the current tick, and it books its callback into the server-wide TimerQueue that only the overworld’s clock advances (the level tick) — so a scheduled function fires once per tick, not once per dimension, and stands still while the world is frozen. What belongs to functions rather than to the clock is what ScheduleCommand refuses outright: a delay of zero, and a macro function named on its own — a tag holding one is accepted, and fails when it fires as silently as the tick tag’s.

Everything else that runs a function is a short list: /function itself, with /return run function; execute if function and /debug function, the two commands that are part of the engine (the execution engine); AdvancementRewards, whose CacheableFunction holds the id and resolves it once (advancements); RunFunction, one of the EnchantmentEntityEffects (enchantments); and TestEnvironmentDefinition.Functions for a game test’s environment setup (game tests).

The two permission verbs

A function body is one of the places a command source’s permission set is deliberately rewritten, and the game reaches the same answer down two differently-named routes.

ServerFunctionManager.getGameLoopSender takes the server’s own source — which is LevelBasedPermissionSet.OWNER — and calls CommandSourceStack.withPermission with gamemaster. That is a flat replacement: the tick and load tags run at gamemaster.

FunctionCommand (for /function and execute if function) and DebugCommand instead call CommandSourceStack.withMaximumPermission at gamemaster, which is PermissionSet.union — and for the level-based sets a player or the console carries, that union returns the lower of the two rather than the higher (permissions). So an owner’s source comes out at gamemaster on this route as well. Both verbs land on the same rung, and no route runs a function body’s source above gamemaster. Which commands the body’s lines may run was settled earlier, when they were parsed against the compile-time permissions above: Brigadier does not ask a node’s requirement again when it runs.

Where to look

CommandFunction for the compiler, and MacroFunction for the only part of it that runs per call. ServerFunctionLibrary for what a reload does off the thread that applies it, ServerFunctionManager for the tick hook and the one empty catch, and StringTemplate for the substitution rules a pack author will trip over.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Advancements

Verified against Minecraft 26.3 · Part XIII · “Stone Age”: a cobblestone lands in your inventory and the toast is on its way before the next tick ends — delivered by a subscription table that shrinks as you play, over a packet that never says what a criterion tests.

Mine a stone block. Nothing about advancements happens when the item enters the inventory. What happens is that the pickup’s ServerPlayer.take runs AbstractContainerMenu.broadcastChanges — the same diff that keeps your client’s inventory in sync (containers and menus) — which notices that a slot’s contents differ from its remembered copy, and reports the difference. Detection is a diff, not an event, and the advancement system is a subscriber to it.

That is the first of two things this system does backwards from expectation. The second is bigger: there is no global list of who is listening for what. Each player carries their own subscription table, it contains only the criteria that player has not yet satisfied, and it shrinks as they play. A veteran player is cheaper to run than a new one, and a save’s cost falls as its players play.

Which is also why advancements are the game’s general-purpose “tell me when the player does X” facility rather than only a goal list. The recipe book is unlocked by advancements. PlayerPredicate reads advancement progress back out as a loot condition. Of the fifty-eight registered triggers, fifty-seven are hooks something in the game fires, and the remaining one, minecraft:impossible, is never fired at all.

The cast

classwhat it decidesside
Advancementthe immutable definition — parent, DisplayInfo, rewards, criteria, requirements, and a pre-rendered display name built in the six-argument constructor, which is the [Title] every announcement quotesboth
AdvancementHolderthe id plus the advancement, with id-only equality — so a map keyed by holder survives a pack changing an advancement’s contentsboth
AdvancementTreethe parent/child graph, built by a fixed-point loop that refuses any advancement whose parent is not yet a node. An orphan is discarded, not re-rootedboth
Criterion / CriterionTriggera trigger plus a decoded CriterionTriggerInstance, and nothing else — a criterion has no name of its own, the name is the map key, and the trigger object is statelessserver
SimpleCriterionTriggerthe base class for all but one trigger, and the owner of the per-fire sweepserver
AdvancementRequirementsa list of lists of criterion names: an AND of ORs. AdvancementRequirements.size counts clauses, not criteriaboth
PlayerAdvancementsthe per-player subscription table and the dirty sets. The only class here with interesting stateserver
TreeNodePositiona full tidy-tree layout — three walks, threads, ancestors, shifts — run on the server, mutating AdvancementNode’s coordinates in placeserver

The shared model is net/minecraft/advancements, the triggers are in advancements/triggers and the predicates in advancements/predicates (with the entity half a level down). All 111 classes ship in both jars — the side column above says where a class does its work, not which jar carries it — and PlayerAdvancements sits in net/minecraft/server beside them. CriteriaTriggers registers fifty-eight triggers into BuiltInRegistries.TRIGGER_TYPES over forty-four classes; the gap is re-use, with PlayerTrigger alone accounting for six registrations.

From a slot that changed to a toast

minecraft:story/mine_stone has one criterion, get_stone, whose trigger is minecraft:inventory_changed and whose condition is a single ItemPredicate over the #minecraft:stone_tool_materials tag. It has no rewards at all.

sequenceDiagram
    box Server
    participant SP as ServerPlayer
    participant ACM as Abstract<br/>ContainerMenu
    participant ICT as Inventory<br/>ChangeTrigger
    participant PA as PlayerAdvancements
    end
    box Client
    participant CPL as ClientPacketListener
    participant CAdv as ClientAdvancements
    end

    rect rgba(0, 0, 0, 0.04)
    Note over SP,PA: the pickup, inside ServerPlayer.doTick, after the levels
    SP->>ACM: broadcastChanges, from ServerPlayer.take
    ACM->>SP: ContainerListener.slotChanged, on the player's own listener
    SP->>ICT: trigger — count all 43 slots first
    ICT->>ICT: SimpleCriterion<br/>Trigger.trigger
    ICT->>ICT: test the changed stack
    ICT->>PA: award, after the sweep
    PA->>PA: unregisterListeners, then the rewards
    PA->>PA: markForVisibilityUpdate — the root
    end
    rect rgba(0, 0, 0, 0.04)
    Note over SP,PA: the next tick's ServerPlayer.tick
    SP->>PA: flushDirty, second to last in the tick
    PA->>PA: updateTreeVisibility — the whole tree
    end
    PA->>CPL: ClientboundUpdate<br/>AdvancementsPacket
    CPL->>CAdv: update — rebuild, reconcile progress, add a toast

One stone pickup through the advancement system — the award happens inside the pickup, after the levels have ticked, and the one packet that tells the client waits for the next tick’s flush.

The paragraphs below take its arrows in order.

Counting comes before knowing whether anyone cares. The slot change reaches the trigger through the ContainerListener every ServerPlayer installs on its own menus, whose ContainerListener.slotChanged fires CriteriaTriggers.INVENTORY_CHANGED. InventoryChangeTrigger.trigger walks all forty-three slots — thirty-six inventory plus seven equipment — to compute the occupied, full and empty counts before it asks whether any criterion is listening. That is the floor cost of every change to a player’s inventory, forever. (The one fired unconditionally is CriteriaTriggers.TICK, once a tick for every player.)

The sweep itself is the cheap part. SimpleCriterionTrigger.trigger fetches this trigger’s map from PlayerAdvancements.getTriggerMapForType and returns immediately if it is null — and it is null once every criterion for that trigger is satisfied, because PlayerAdvancements.removeListener deletes the per-trigger map when it empties. When there is work, it builds one LootContext and reuses it for the whole sweep, evaluates the caller’s cheap matcher first and the player predicate only for matches, and does not allocate the results list until the first hit. Note the scope: one player. Nothing here is a broadcast.

Matches are collected, then awarded, because awarding calls PlayerAdvancements.unregisterListeners, which mutates the very map being iterated.

Completion is checked against the requirements, not the criteria. AdvancementRequirements.test is the AND of ORs. Here it is one clause of one name, so granting the criterion completes the advancement — which fires the rewards, the chat announcement (built by AdvancementType.createAnnouncement, broadcast to every player, gated on the display’s own flag and on GameRules.SHOW_ADVANCEMENT_MESSAGES) and the visibility dirty flag.

The rewards are AdvancementRewards.EMPTY here, as for most advancements outside the recipe book. The record holds experience, loot tables, recipes and a function, and the recipe field is why the recipe book works at all: every recipe advancement is generated with a RecipeUnlockedTrigger criterion and rewards naming the recipe, so earning it calls ServerPlayer.awardRecipes (recipes). RecipeUnlockedTrigger then closes the loop by letting other advancements observe an unlock — comparing the recipe key by reference identity, which is safe only because ResourceKeys are interned. The function reward is a CacheableFunction, which holds the id and resolves it once (functions and macros).

The table shrinks, and the two things that refill it

PlayerAdvancements.registerListeners subscribes a player only to criteria that are not yet done in advancements that are not yet done, and every award unsubscribes. The longer you play, the smaller your table, and the cost of the system to a save falls as its players play.

Two things run it backwards. /advancement revoke re-subscribes what it revoked, and a reload — /reload, or /datapack enabling or disabling a pack — re-subscribes everything unfinished in the new pack. Neither is something the game does on its own, which is why the table shrinks as you play rather than only ever shrinking.

Two oddities sit on the same machinery. ImpossibleTrigger has no trigger method at all — it is the one trigger implementing CriterionTrigger directly, and it exists so a node can anchor a tree while being ungrantable except by command, which vanilla uses for exactly one file: the invisible root of every recipe advancement. (It has nothing to do with /trigger, which is a scoreboard feature.) And PlayerAdvancements.checkForAutomaticTriggers is dead code in the strictest sense: it walks every advancement on every player load, but its whole body sits behind this advancement has no criteria at all, and Advancement’s criteria codec rejects an empty map outright. No loaded advancement can satisfy the guard, so the loop never does anything.

Visibility is per root, and it gates the wire

PlayerAdvancements.markForVisibilityUpdate dirties the root, and the flush — PlayerAdvancements.updateTreeVisibility, once per dirty root — re-runs AdvancementVisibilityEvaluator (the “how far past your frontier can you see” rule, with a depth of two) over that root’s entire subtree. Finishing one advancement in a large tree re-evaluates the whole tree, and only the nodes whose visibility flipped go on the wire.

PlayerAdvancements.flushDirty then sends one ClientboundUpdateAdvancementsPacket, with progress only for advancements in PlayerAdvancements.visible. Progress on something hidden, or beyond your frontier, accumulates server-side and reaches the client the moment it becomes visible.

Where the flush sits in the tick is worth pinning down, because it produces a real one-tick delay that nobody expects. Inside ServerPlayer.tick, AbstractContainerMenu.broadcastChanges is the fifth statement, CriteriaTriggers.TICK fires mid-tick, and PlayerAdvancements.flushDirty is second to last, with only a post-effects check after it. So everything awarded between those points — a minecraft:tick criterion, or an item that another advancement’s reward grants, whose diff runs inside that award — coalesces into one packet, and so does everything awarded earlier in the tick: a melee kill arrives in the packet drain of MinecraftServer.processPacketsAndTick, and a typed /advancement grant as a task the server runs before it, both before the levels tick at all.

But ServerPlayer.tick is not the last thing that happens to a player: it is only the first of the two brackets a player is ticked in (the two-phase tick). ServerGamePacketListenerImpl.tick calls ServerPlayer.doTick during the connection phase, which runs after the levels (the server tick) — i.e. after PlayerAdvancements.flushDirty has already run. CriteriaTriggers.LOCATION, which fires there every twenty ticks and is what most vanilla biome and structure advancements hang on, therefore always lands in the next tick’s packet — and so does any award a pickup earns, the stone above included, because ServerPlayer.take runs the diff there.

A criterion’s conditions are loot conditions

Each entity condition a trigger carries — the player test every simple trigger has, a kill’s victim — is one LootItemCondition, held in a registry holder and evaluated against a LootContext reached through EntityPredicate.createContext. So a trigger’s conditions are exactly the machinery of contexts and predicates, and that is where most descriptions of the system stop.

It is worth going one step further, because the predicate package is where four shapes were invented that the whole data-driven half of the game now reuses.

shapewhat it generaliseswhere else it turns up
MinMaxBoundsthe numeric range, with both a codec and a StringReader grammar3..7 means the same in a predicate, an entity selector and /random
CollectionPredicateone generic “N of these match”, composing CollectionContentsPredicate and CollectionCountsPredicateits only users are the seven component predicates in core/component/predicates
EntitySubPredicateevery part of an entity predicate as a registry element instead of a code branchits twenty-five kinds are each a record and a codec, a few with a builder beside them
DataComponentMatcherstesting a stack’s components without knowing what any of them aredata components

Two details change behaviour rather than shape. LootItemCondition.CODEC accepts either the id of a data-pack predicate or one inline condition — though vanilla’s own advancements take the inline form every time, so in a criterion the reference exists for pack authors rather than for the game. And EntityPredicate declares an explicit type-check-first, NBT-last ordering for its own sub-tests — a performance invariant hiding inside a predicate class.

The screen at the other end

The client’s half is five classes in net/minecraft/client/gui/screens/advancements, ClientAdvancements over in client/multiplayer, and the toast, and it is the payoff for everything the server did. It does no tree layout: it is drawing positions a data-pack reload decided, which is why the tree looks the same on every client connected to a server.

The layout itself is TreeNodePosition, run once per root by AdvancementTree.repositionNodes from ServerAdvancementManager’s constructor — which a reload runs on a background worker, after RegistryDataLoader has read the JSON (the resource system). It writes each AdvancementNode’s coordinates in place, and they ride the packet in ClientboundUpdateAdvancementsPacket.PositionedAdvancement. A root with no DisplayInfo is never laid out and never becomes a tab, and a display-less node in the middle of a tree is transparent — the layout skips it and adopts its children. One wrinkle in an otherwise deterministic algorithm: AdvancementNode.children is an unordered hash set, so sibling order inside a tidy-tree layout is hash-dependent.

AdvancementsScreen is a ClientAdvancements.Listener, and its AdvancementsScreen.onAdvancementsUpdated rebuilds every tab from the tree. ClientAdvancements.setListener makes that call on a new listener at once, which is how the screen catches up on open. Each packet is applied by ClientAdvancements.update: it removes and adds tree nodes, places each added one at the coordinates the packet carries, reconciles every progress it was sent against that advancement’s requirements, and — for each now done whose display asks for it — adds an AdvancementToast, which plays a sound only for a challenge. Then it makes the same call, so an open screen rebuilds after every packet. AdvancementsScreen owns the tab strip; AdvancementTab owns one root’s pan-and-scroll bounds, auto-centres on first render and clamps the drag; AdvancementWidget scales the server-decided coordinates by fixed factors, draws the connector lines to its parent, and wraps its own tooltip text.

AdvancementTabType is the one with a hard limit in it: four header geometries with room for eight tabs above, eight below and five on each side. A data pack’s twenty-seventh root is silently unreachable.

Two more things ride this boundary for almost nothing. ServerboundSeenAdvancementsPacket has a “closed screen” action that is serialised, deserialised and dropped. And Advancement.sendsTelemetryEvent is consumed only on the client, by WorldSessionTelemetryManager, and only for advancements in the minecraft namespace — which is the whole reason a flag rides a wire form that drops the criteria and the rewards (what this book skips).

What the package holds that this page does not name

Two families make up most of net/minecraft/advancements, and the answer to both is the same: the page explains the shape and the members are instances of it. The forty triggers it does not explain — KilledTrigger, FishingRodHookedTrigger, RecipeCraftedTrigger and the rest — are each a record, a codec and a CriterionTriggerInstance, registered by CriteriaTriggers and fired from wherever in the game the thing happens; knowing SimpleCriterionTrigger is knowing all of them. The concrete predicates under advancements/predicates are the same story one level down: StatePropertiesPredicate, EntityFlagsPredicate, EntityEquipmentPredicate, MobEffectsPredicate, DamageSourcePredicate and their neighbours are each a small record over one kind of thing, the entity ones instances of EntitySubPredicate, and the one that matters most to another part, LocationPredicate, is named where it does work, in contexts and predicates.

Questions players ask

Why does the client show “3/7” if it does not know what the criteria are? Because AdvancementRequirements is on the wire and AdvancementProgress.update reconciles against it. Advancement.STREAM_CODEC reconstructs the record with an empty criteria map and AdvancementRewards.EMPTY, so a client cannot know what any criterion tests, or that an advancement grants anything at all. And AdvancementProgress.getProgressText returns nothing at all when there is one clause, which is why a single-criterion advancement never shows “1/1”. A criterion’s whole per-player state is one class, CriterionProgress, and one field in it: a nullable timestamp. That is why the wire form can carry progress without carrying a single condition. (An AdvancementProgress with no requirement clauses cannot be complete — AdvancementRequirements.test returns false for an empty list rather than vacuously true — and every progress starts that way, decoded from the wire or the disk or built empty, until AdvancementProgress.update gives it its requirements, which both sides do at once.)

Does /reload roll back my progress? No, and the order is the point: a reload’s completion list saves every player before it reloads them (the resource system), so PlayerAdvancements.reload re-reads a file written moments earlier. What is genuinely lost is progress for any advancement the new pack has removed or renamed — logged once each, and invisible to the player except as a full reset packet — plus the selected tab, which is silently forgotten with no packet, so the client keeps a stale one.

When is progress written to disk? Only when the player is saved. There is no write on award: PlayerAdvancements.save runs from PlayerList.save, on disconnect, on every save of the world — the autosave, /save-all, shutdown — or from the reload above. The definitions come from data/<ns>/advancement/<id>.json through Advancement.CODEC — a file that fails to decode aborts the reload outright — and per-player state is one JSON file at players/advancements/<uuid>.json (LevelResource.PLAYER_ADVANCEMENTS_DIR), data-fixed on load through DataFixTypes.ADVANCEMENTS.

Why is /advancement grant usable as a conditional? Because a no-op is a hard failure: granting an advancement the player already has throws rather than reporting zero. AdvancementCommands.Mode — only, through, from, until — picks one advancement or collects its parents or children or both (its everything constant goes unused), and /advancement grant … everything, which takes every advancement directly, calls PlayerAdvancements.flushDirty before the loop with the packet’s “show advancements” flag true and after it with the flag false, which is the only purpose that flag has: suppressing a toast storm from the batch it just granted.

Why does the toast stay silent? Because AdvancementToast asks for a sound only when the advancement is an AdvancementType.CHALLENGE — one of the three AdvancementTypes, and the one that also gets its own title colour. For a task and a goal the method returns nothing at all, so the toast slides in in silence. The client can tell which it is because DisplayInfo carries the type; what it cannot tell is whether the advancement was meant to announce itself in chat, because that flag never goes on the wire. The serialiser packs three flags into an int and omits it, and the reader hard-codes it false while the codec defaults it true, so the client’s copy is wrong for the common case rather than merely unused.

Where to look

Advancement and AdvancementRequirements for the model; PlayerAdvancements for everything that happens; SimpleCriterionTrigger and InventoryChangeTrigger for the hot path; MinMaxBounds and CollectionPredicate for the predicate shapes that recur everywhere else; and AdvancementVisibilityEvaluator for the one rule nobody guesses right.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Scores, teams and stored data

Verified against Minecraft 26.3 · Part XIII · execute as @a store result score @s ticks_frozen run data get entity @s TicksFrozen: one command writing a scoreboard through a callback the inner command has never heard of, and two of the three data models execute store exists to join.

Look at the sidebar on a well-built server and some of the names in it are not players. #total, constant, .timer — rows belonging to nothing alive. That is not a hack layered on top of the scoreboard; it is the scoreboard working exactly as written, and one method override explains it.

Entity.getScoreboardName returns the entity’s UUID string. Player.getScoreboardName overrides it with the profile name. There is one flat map from string to a PlayerScores — a row of scores, holding players by name, mobs by UUID, and anything else you care to type. From that single override follows the whole of scoreboard folk practice: why fake players exist (nothing checks that a key belongs to an entity), why a mob’s score can never appear in the tab list (whose holder is built from a profile name), and why renaming a player orphans their scores.

execute store is why four systems share this page. It is the only construct in the game that takes the result of an arbitrary command and writes it somewhere, and it has exactly three sinks — a score; a path into a block, an entity or a storage; and a boss bar’s value or maximum. Each of those is a named thing on this server that a number can be written into, and each brings its own model with it: the scoreboard, which is the game’s general-purpose number per thing; the NBT path language, a query language for reaching into any tag at all, over command storage, which is a place to put a tag belonging to no block and no entity; and the boss bar, which turns out to be the scoreboard’s shape one floor up. Teams are the fourth, and they are here because they live in the scoreboard’s package and its class — read by systems that have nothing to do with scores.

The instinct they share is worth stating before the classes, because it explains three otherwise-odd decisions: the server is the only participant that knows anything. The client is sent scores it can draw and nothing else — not an objective’s criteria, not a score’s lock bit, not which objectives exist. There is no serverbound packet in this whole system. Every write a player asks for is a command.

The cast

classwhat it decidesside
Scoreboardsix maps and nothing else, plus ten empty hooks for a subclass to fill. A pure data structure, used unmodified by the clientboth
Objectivecriteria, display name, render type, number format, auto-update — and a back-pointer to the scoreboard, so every setter reports its own changeboth
Scorefour mutable fields: the value, a lock bit, a display component and a number format. ReadOnlyScoreInfo, PlayerScoreEntry and ScoreAccess are the other three faces of itboth
ScoreHolderthe interface Entity implements, with ScoreHolder.forNameOnly minting anonymous ones — the most consequential class on the pageboth
PlayerTeamthe only subclass of Team: the mutable state, the setters, a precomputed display style, and friendly-fire plus see-invisibles packed into one wire byteboth
ServerScoreboardthree fields — the server, ServerScoreboard.trackedObjectives, and one dirty boolean — and thirteen overrides, the ten hooks and three methods that change team membership and display slots, which between them broadcast what changed and mark the save dirty. It lives in net/minecraft/server, not beside Scoreboardserver
NbtPathArgument874 lines, the largest argument type in the game, and a whole query language: six node kinds and a depth limit of 512both
DataCommands/data, over three DataAccessors — BlockDataAccessor, EntityDataAccessor and StorageDataAccessor. Beside it, ScoreboardCommand, TeamCommand and TriggerCommand write the scoreboard, as execute store doesserver

net/minecraft/world/scores is the whole model, and every class in it ships in both jars. Beside it: CommandStorage, a lazy façade over one SavedData per namespace, so minecraft:foo and mypack:foo are different files; and net/minecraft/network/chat/numbers, holding NumberFormat with the three kinds NumberFormatTypes registers.

One command, two models, and a number that lands in a third place

The figure follows that command from the engine’s stage walk to the client’s sidebar; BuildContexts is the engine’s walker over a parsed chain (the execution engine).

sequenceDiagram
    box Server
    participant BC as BuildContexts
    participant ExecC as ExecuteCommand
    participant DataC as DataCommands
    participant SS as ServerScoreboard
    participant SA as ScoreAccess
    end
    box Client
    participant CPL as ClientPacketListener
    end

    BC->>BC: as @a — one source becomes one per player
    BC->>ExecC: store result score — the modifier, once per source
    ExecC->>ExecC: storeValue — @s and the objective already resolved
    BC->>DataC: getData, the leaf, queued once per source
    DataC->>DataC: build the entity's whole save tag
    DataC->>DataC: follow the path, collapse the tag to one int
    DataC-->>ExecC: the result, to the source's callback
    ExecC->>SS: getOrCreatePlayerScore
    ExecC->>SA: set
    SA->>SS: onScoreChanged — only if changed or new
    SS->>CPL: ClientboundSetScorePacket — only for a displayed objective
    CPL->>CPL: handleSetScore — the owner is only a name

One execute store result score for one player — the store is fixed on the source before the inner command runs, the number reaches it by callback, and two separate gates decide whether the write is a packet.

The paragraphs below take its arrows.

The store target is resolved before the inner command runs, not after. ExecuteCommand.wrapStores builds the store node as a redirect with a modifier, not as a step after the leaf. The modifier resolves the score holder and the objective against this source, then decorates the source with a callback chained onto whatever was already there. So the store is a property of the source, which is why several stores compose, and why each forked player writes their own row without the leaf command knowing a scoreboard exists.

The result travels by callback, not by return value — the source’s own callback, which is precisely why execute store works on a command typed in chat whose frame callback is empty (the execution engine).

A failing command under store result writes 0, whichever kind it is. On the custom-executor path that is CustomCommandExecutor.WithErrorHandling doing what it does for every executor built on it (the execution engine), so a failing /function writes 0. For an ordinary leaf the result consumer is driven by Brigadier, and ContextChain.runExecutable catches the CommandSyntaxException and calls the consumer with success false, result 0 before rethrowing. The game hands that consumer straight through from the source’s own callback, so the two paths agree: a store target written by a command that threw holds zero, not its previous value.

/data get builds the whole entity to read one field. EntityDataAccessor.getData produces the entity’s entire save tag, freshly, and the path then walks it. That is the real cost of /data get, and it is why a per-tick /data get on a busy entity is a measurable expense.

Four rules collapse a tag to one integer. A numeric tag floors its double value; a collection and a compound both yield their size; a string yields its length. Nothing yields the value you might have meant: data get entity @s Inventory returns the number of stacks you are carrying, not 36 — Inventory.save writes only the non-empty slots, so the collection whose size you get is as short as your inventory is empty. A path matching more than one tag is an error; a path matching none is a different error.

The write can be a silent no-op, and may never reach the wire at all. ScoreAccess.set writes nothing and sends nothing when the value is unchanged and the score is not new — unless the objective has auto-update on, in which case it refreshes the display name first and a changed display counts as a change on its own. And ServerScoreboard.onScoreChanged is gated on the objective being in ServerScoreboard.trackedObjectives, whose only entrance is occupying a display slot.

Why a write is a handle

Scoreboard.getOrCreatePlayerScore does not return a Score. It returns a ScoreAccess — an anonymous object closing over the score, the objective and the holder, plus two decisions computed once, at handle-creation time: may this be modified (did the caller ask for a force-writable handle, or is the criteria not read-only), and was this score newly created by the lookup that produced this handle.

That is the whole answer to “why not a setter”. A setter would have to re-derive the first fact on every call, and the second is unrecoverable after the fact — once the Score exists, nothing can tell whether this call created it. Newness is what decides whether an unchanged value still needs a packet, so it has to survive from the lookup to the write. The handle is also the one place that knows when to fire the change hook, which is why a write goes through ScoreAccess.set, ScoreAccess.add, ScoreAccess.increment, ScoreAccess.reset, ScoreAccess.lock, ScoreAccess.unlock, ScoreAccess.display or ScoreAccess.numberFormatOverride; only removal — Scoreboard.resetSinglePlayerScore, Scoreboard.resetAllPlayerScores and Scoreboard.removeObjective — and loading from the save go round it.

Nothing here is ticked. MinecraftServer mentions the scoreboard five times in total — the field, the constructor, the load, the getter, and one call to ServerScoreboard.storeToSaveDataIfDirty inside MinecraftServer.saveAllChunks. There is no periodic sweep and no dirty-queue drain: every mutation that reaches the wire sends its packet synchronously, inside the call that made it, and there is one scoreboard per server, not per level, so scores and teams are global across dimensions.

Names that belong to nobody, and the # that hides them

The opening’s fake players are not one mechanism but two, and the # in front of #total does a different job in each.

In ScoreHolderArgument a leading # skips entity resolution entirely, so the token is taken as a literal name and no lookup happens. Its literal side has four branches in order — the wildcard *, a # name, a UUID searched across every level, and an online player — and the UUID and player branches fall back to a bare name when they find nothing. The wildcard does not: with no tracked holders at all it throws.

In the sidebar the same character means something unrelated: PlayerScoreEntry.isHidden filters the row out of the display. One character, two mechanisms, and together they are the whole hidden-fake-player idiom — a row that can be written by name, read by name, and never drawn.

Nothing about that requires an entity to exist. ScoreHolder.forNameOnly mints a holder from a string — it is all ClientPacketListener.handleSetScore ever builds from a score packet — and the scoreboard’s one flat map does not care where the string came from.

What a criterion can be, which is nearly anything

ObjectiveCriteria looks like an enum of eleven values and is not. Forty-three constants exist, thirty-two of them generated — sixteen team-kill and sixteen killed-by-team criteria, one per team colour. Six are read-only: health, food, air, armour, experience and level.

And then the tail. Stat extends ObjectiveCriteria, so every statistic in the game is a criterion, and ObjectiveCriteria.byName parses a colon-separated name by looking the left half up as a stat type and the right half in that stat type’s own registry. minecraft.mined:minecraft.stone is not a special case; it is the statistics registry addressed through a string. Nine stat types over the block, item, entity-type and custom-stat registries make thousands of valid criteria names, which is why /scoreboard objectives add accepts far more than the forty-three bare criteria names — its suggestions offer those and every stat name in every stat type’s registry.

The identity-keyed reverse index is what makes that cheap. Scoreboard.objectivesByCriteria is an identity map from criteria to the objectives watching it, ServerPlayer.awardStat hands the Stat object itself to Scoreboard.forAllObjectives, and object identity finds the watchers — sound only because each stat object is interned, one per value, in its StatType.

Criteria-driven scores are the one part of this page with a schedule, and it is narrower than it sounds. Scoreboard.forAllObjectives has seven call sites and every one is in ServerPlayer: one inside the helper each of the six read-only criteria is updated through, one for the death count, two for the kill counts, one that fires both team-kill criteria, and two for statistics — one awarding, one resetting. No criterion is driven from Entity, LivingEntity or Mob, so a skeleton killing a zombie increments nobody’s kill count — though Entity.load does write to it, calling Scoreboard.addPlayerToTeam when the tag it loads names a Team — a field the game reads but never saves, so it arrives only in a tag a command or an item supplies. The six read-only criteria are change-detection diffs — six consecutive comparisons against remembered fields — living in ServerPlayer.doTick, which runs in the connection phase, after the levels have ticked (the level tick). So damage taken during the level tick reaches the scoreboard, and the wire, later in the same tick rather than during it.

The path language, and the accessor that is coarser than it looks

Six node kinds — a named child, a match on an object, a match on the root object, a match on a list element, all elements, and an index — with a depth limit of 512.

The elegant part is creation. The parent-creating walk goes through the nodes and, for each one, asks the next node what shape its parent has to be: a named child wants a compound, an index wants a list. So a set through a.b.c materialises two compounds without any node knowing more than its own type — though an index never creates an element, so through a.b[0].c the walk makes a and an empty list b, finds nothing at [0], and fails. Removal is the same walk with a plain lookup instead, so it never creates.

The three accessors, by contrast, are coarse. DataAccessor has two methods that matter — read the whole tag, write the whole tag — and no path-aware write anywhere. Every /data modify is read everything, mutate in memory, write everything back, which is why a block-entity write reloads the block entity and marks the chunk dirty, and why an entity write round-trips the entity through its own load path and then restores the UUID by hand, because loading would have overwritten it. What each accessor does contribute is its own grammar subtree, which is how DataCommands builds the target half and the source half of every subcommand from one list of three providers applied twice.

Teams, which systems far from scores read

Team declares everything a reader asks for and PlayerTeam is its only subclass. What makes teams worth their own paragraph is who else consults them: collision through EntitySelector.pushableBy; nametag visibility through LivingEntityRenderer.shouldShowName; invisibility through Entity.isInvisibleTo; friendly fire through Entity.doTeamsAllowDamage; and death-message routing through ServerPlayer.die. Only ServerPlayer.die and LivingEntityRenderer.shouldShowName are reached from anywhere near one place; EntitySelector.pushableBy has six call sites, Entity.doTeamsAllowDamage seven, among them Player.canHarmPlayer and the attacks of a few mobs, and Entity.isInvisibleTo two. A sixth system is joined to teams both ways. Every team join, leave and modification calls through to ServerWaypointManager to remake the locator bar’s connections, and remaking one reads the team’s colour to re-colour its icon.

Two team behaviours are worth pinning. Team.isAlliedTo is reference equality, so two teams with byte-identical settings are never allied; every “same team?” test in the game is really “same object?”, safe only because Scoreboard.teamsByName is the single owner of every instance. And a team has two visibility settings of which only one ships: the wire parameters carry nametag visibility, while death-message visibility has a single reader in play, ServerPlayer.die, and the client never learns the rule because it does not need to. Nametag visibility also widens: when an entity has a team, LivingEntityRenderer.shouldShowName returns from the team switch directly and never reaches the checks that hide a name behind F1, for the camera entity, or for a vehicle — so a mob on a team set to always keeps its name through the HUD toggle.

What the client is ever told

Five packets, and one field that gates four of them

Five packets, all server → client, and no serverbound counterpart exists: ClientboundSetObjectivePacket, ClientboundSetDisplayObjectivePacket, ClientboundSetScorePacket, ClientboundResetScorePacket and ClientboundSetPlayerTeamPacket. All five reach every player, through PlayerList.broadcastAll or a loop over the player list — no distance filter, no dimension filter (what the client is told).

An objective in no display slot does not exist on the network. ServerScoreboard.onObjectiveAdded sends nothing at all; the only path into the tracked set is ServerScoreboard.setDisplayObjective. A scoreboard with two hundred objectives and an empty sidebar costs zero bandwidth — and putting one into a slot then ships every score it holds, to every player, at once. The join burst is the same shape: PlayerList.updateEntireScoreboard sends every team with its full member list, then walks all nineteen display slots and ships each distinct occupying objective with all of its scores.

What it is not told, and how it draws anyway

What the client is told is also less than it looks. ClientPacketListener constructs every objective it receives with ObjectiveCriteria.DUMMY, so a client cannot tell a health objective from a dummy one — it only knows to draw hearts — and the score packet carries no lock bit, which is why /trigger’s suggestions have to be computed on the server.

How a score is drawn is settled by a third object again, and it has its own fall-back order. A NumberFormat may ignore the number entirely — FixedFormat renders a constant component whatever the score is — and resolution runs per-score override, then per-objective, then a per-site default: red in the sidebar, yellow in the tab list, unstyled below the name. Below-name numbers are the odd one out: they are computed in Entity rather than in a renderer, and their range is an attribute, Attributes.BELOW_NAME_DISTANCE, syncable, default ten, maximum 512, so a server can change how far away a player’s below-name score is legible, per entity (attributes).

There is a third route by which a score reaches a client, and it carries no score packet at all: a {"score":…} or {"nbt":…} in a text component. ScoreContents and NbtContents resolve on the server, against the authoritative scoreboard, and put the result on the wire — never the reference (text components). A /tellraw is a photograph, not a subscription.

What survives a restart, and what does not

Saving is one boolean for the entire scoreboard, cleared by re-packing the whole thing, and it happens only when the world is saved — the autosave, /save-all, or shutdown. A crash thrown by the tick itself is a shutdown: it falls into the same finally, which calls MinecraftServer.saveAllChunks, whose very first statement is ServerScoreboard.storeToSaveDataIfDirty. What loses a score is an ending that never reaches that method — a crash relayed from a worker thread, which the shutdown’s own drain throws again before the save, a watchdog kill, a kill -9, a power cut — and then every score since the last autosave goes with it (how a server dies). ScoreboardSaveData sits under minecraft:scoreboard beside the world, with one command-storage file per namespace, both through the data fixer (level data and rules). The NBT field names are the archaeology — Objectives, PlayerScores, DisplaySlots, Teams, and inside them Name, CriteriaName, RenderType, Locked — capitalised, and preserved by codec.

The third sink is a boss bar, and it is this page’s shape again

execute store result bossbar <id> value writes into a CustomBossEvent, and a reader who has the scoreboard already knows the model. A boss bar is a named server-side thing holding a number, saved with the world, broadcast to the players attached to it, and writable only by a command.

BossEvent is the shape everything shares: an id, a name, a progress float from zero to one, a BossEvent.BossBarColor, a BossEvent.BossBarOverlay, and three booleans that ask the client to darken the screen, play the boss music and draw the world fog. ServerBossEvent adds the live membership — a Set<ServerPlayer> — and turns every setter into a broadcast, which is the whole of the sending side the book had not named: one ClientboundBossEventPacket with six operations, add and remove and one update apiece for progress, name, style and properties. There is no serverbound counterpart, exactly as there is none for a score. Four things in the game own one: WitherBoss, Raid, EnderDragonFight — and CustomBossEvent, which is /bossbar.

What CustomBossEvent adds is the part that rhymes. It keeps an integer CustomBossEvent.value and an integer CustomBossEvent.max, derives the float progress from them on every write, and holds its membership as a Set<UUID> beside the superclass’s set of live players. The UUID set is what persists and what /bossbar set … players edits; the live set is maintained by CustomBossEvent.onPlayerConnect, which re-attaches a player whose UUID is on the list, and by CustomBossEvent.onPlayerDisconnect, which calls the superclass’s removal, so that the UUID stays. A bar’s membership outlives the session the same way a score outlives the entity.

CustomBossEvents is the SavedData that holds them, one file at data/minecraft/custom_boss_events.dat (level data and rules), and its NBT is the same archaeology as the scoreboard’s — Name, Visible, Value, Max, Players, capitalised, preserved by codec. BossBarCommands is the write surface, at gamemaster, and an overload of ExecuteCommand.storeValue, the score sink’s own name, is the store sink: a sibling of the score sink rather than the same code, chaining the same kind of callback onto the source and writing the value or the maximum instead of a ScoreAccess. What the client does with the packet — the interpolation, the sky it darkens — is the HUD.

Questions players ask

Why is my sidebar not DisplaySlot.SIDEBAR? If the local player is on a team with a colour, Hud uses that colour’s own display slot and falls back to the plain sidebar otherwise. The colour-to-slot mapping lives on TeamColor, not on DisplaySlot, and it is what the sixteen team sidebars are for. The sidebar shows fifteen rows and hides before it cuts: hidden rows are filtered out, then the rest sorted by value descending and name case-insensitively, and then truncated to fifteen.

Why did /trigger say the objective is not enabled? Score’s lock bit starts locked and its codec defaults unlocked, so a score created by /scoreboard players set is locked while a score loaded from a file that omits the field is not. /trigger is the only command an unprivileged player can use to write a score, and its gate is three-part — the criteria must be the trigger criteria, the score must already exist, and it must be unlocked — and the command re-locks it immediately, so each enable buys exactly one use. It is also, with /teammsg (and its /tm alias), one of the two commands in this area registered with no permission requirement at all.

Why did my execute store throw an internal error? Because /scoreboard refuses a read-only objective and execute store does not check. The command resolves its write targets through ObjectiveArgument.getWritableObjective; ExecuteCommand.wrapStores uses the plain lookup and never asks for a force-writable handle. So execute store result score @s <a health objective> reaches ScoreAccess.set with modification disallowed and raises a raw runtime exception from inside a result callback rather than a command error. The same hole is reachable through /scoreboard players operation with ><, the one operator that writes both sides.

Why did my execute store into a data target do nothing at all? Its read-mutate-write is wrapped in a catch with an empty body: a player target, an uncreatable path, a path too deep — no message, no failure, no write. The score sink has no such catch.

Why can I read a player’s NBT but not write it? The entity accessor’s write path rejects any Player before doing anything else, and the read path has no such check. That one asymmetry is why every player-NBT technique is read-only. Relatedly, a no-op is a hard failure across the area — /data merge, /data modify, /data remove and /scoreboard players enable all throw when they changed nothing, as do most of /team’s setters and every one of /bossbar’s, which makes them usable as conditionals in a function, the same choice /advancement grant makes.

Do a mob’s scores survive it despawning? They survive unloading and die with the mob: Scoreboard.entityRemoved runs from the level’s destruction callback and is gated on the entity being both non-player and not alive.

Where to look

Scoreboard for the six maps, ScoreAccess for why a write is a handle, and ServerScoreboard.trackedObjectives for the one field that decides which objectives and scores a client’s scoreboard ever holds. ObjectiveCriteria.byName for the statistics bridge, NbtPathArgument.NbtPath for the tidiest piece of the area, and ExecuteCommand.wrapStores for the one that makes execute store stop feeling like magic.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Dialogs

Verified against Minecraft 26.3 · Part XIII · You click a server in the multiplayer list and, before the world has loaded — before you are in a world at all — a form appears with text boxes on it, and a vanilla server sends one only from a command it registers behind debug flags.

A dialog is a data pack’s form: a title, some body text, some inputs and some buttons, decoded from JSON and put on your screen. Nothing about that is surprising until you notice which protocol phase it works in. ClientboundShowDialogPacket is registered in both the play and the configuration protocols, with a different codec in each, so a server can interrupt the join handshake to ask you something — configuration being the phase a joining client is parked in, and the one a play session can be sent back to (protocol phases). Vanilla only ever does it from a dev-flag-gated command — but the machinery is there, complete, in the shipped jar.

And the reason it works there is not a special case bolted on; it is a second codec, and it explains itself. The configuration buffer is a plain byte buffer with no registry access (packets and stream codecs), so the packet cannot carry a holder id. Dialog.CONTEXT_FREE_STREAM_CODEC therefore sends the whole dialog inline. The dialog is encoded with no registry access at all, so a dialog it refers to goes inline too, and an element that can only be named by id — an enchantment on an item body’s item, say — cannot be sent at all.

A dialog is also one of this part’s two clearest instances of a pattern found across the game — game tests is the other — a thing made a registry element loaded from a data pack rather than a Java class. That argument is made once, for all its instances, in the data-driven type pattern; this page assumes it. Four of the pattern’s registries are dialog registries: BuiltInRegistries.DIALOG_TYPE for the dialog itself, and BuiltInRegistries.DIALOG_BODY_TYPE, BuiltInRegistries.INPUT_CONTROL_TYPE and BuiltInRegistries.DIALOG_ACTION_TYPE for the three kinds of part inside it.

The cast

classwhat it decidesside
Dialogthe registry element. Dialog.DIRECT_CODEC dispatches on BuiltInRegistries.DIALOG_TYPE; there are two stream codecs, and which one is used decides the whole pageboth
CommonDialogDatawhat every dialog embeds: titles, whether escape closes it, whether it pauses the game, the after-action, the body elements and the inputs. Its MapCodec is where the pause validation livesboth
DialogActionclose, none, or wait-for-response — and DialogAction.willUnpause is what that validation testsboth
InputControlTextInput, SingleOptionInput, BooleanInput, NumberRangeInput. An Input is a key plus a control, and the key must be a valid macro variable nameboth
Actionproduces an optional ClickEvent from the live input values, through Action.ValueGetterclient
ClickEventextended with ClickEvent.ShowDialog, which is how a chat message, a book, a dialog or a sign can open a dialog, and ClickEvent.Custom, which carries a custom actionboth
DialogScreensthe codec-to-screen-factory map, with DialogScreen as the base and DialogControlSet owning the live gettersclient
DialogConnectionAccessthe phase-specific way back to the server — and the configuration-phase one refuses to run commandsclient

net/minecraft/server/dialog spans four packages, all in the server jar; the screens that render them are client-only in net/minecraft/client/gui/screens/dialog — SimpleDialogScreen for both simple kinds, and MultiButtonDialogScreen, DialogListDialogScreen and ServerLinksDialogScreen over an abstract ButtonListDialogScreen — which is the same one-more-screen pattern GUI and screens covers everywhere else. The five kinds DialogTypes.bootstrap registers are NoticeDialog and ConfirmationDialog (both SimpleDialog) and MultiActionDialog, DialogListDialog and ServerLinksDialog (all ButtonListDialog) — and both of those supertypes are interfaces, not classes.

From a JSON file to a click the server reads

sequenceDiagram
    box Server
    participant DlgC as DialogCommand
    participant SP as ServerPlayer
    participant MS as MinecraftServer
    end
    box Client
    participant CComPL as ClientCommon<br/>PacketListenerImpl
    participant DlgS as DialogScreen
    participant DCS as DialogControlSet
    end

    DlgC->>SP: openDialog
    SP->>CComPL: ClientboundShowDialogPacket — a holder id, or inline
    CComPL->>CComPL: handleShowDialog — a screen for the dialog's type
    CComPL->>DlgS: the new screen
    DlgS->>DCS: addInput, per input — a live getter each
    DCS->>DCS: the click — the action reads the getters now
    DCS->>DlgS: runAction, with the click event it made
    DlgS->>CComPL: DialogConnectionAccess.<br/>sendCustomAction
    CComPL->>MS: ServerboundCustom<br/>ClickActionPacket
    MS->>MS: handleCustom<br/>ClickAction

One /dialog show and one click on a custom button — the input values are read at the click, by the control set that has held a live getter for each input since the screen was built, and the packet that carries them back reaches a handler that, in vanilla, writes a debug log line and nothing else.

The trace turns on one decision: when are the input values read? Not at packet time and not at screen construction. DialogControlSet.addInput registers a live Action.ValueGetter for each input as the screen is built, and Action.createAction calls them at the moment of the click — which is why the same Action object produces a different command each time, and why CommandTemplate can be a template rather than a string. The button hands the click event it made to DialogScreen.runAction, and a custom one leaves through DialogConnectionAccess.sendCustomAction, the phase’s way back to the server. ActionTypes registers nine kinds. Seven are generated in a loop over the click-event kinds a server is allowed to send, each keeping that kind’s own name, so a click-event kind is automatically a dialog action — and the one kind that is not allowed, opening a local file, can never be one. The other two are registered by hand under names of their own: CommandTemplate as dynamic/run_command, and CustomAll as dynamic/custom, which packs every input value into an NBT compound.

The packet on the way out is the play-phase form, which names a registered dialog by its holder id and sends an unregistered one inline; in the configuration phase the same packet is always inline, for the reason at the top of this page.

Nothing on the server side of this ever ticks. A dialog is a packet send from whatever ran the command or handled the click; the reply hops off the Netty thread onto the server’s PacketProcessor before MinecraftServer.handleCustomClickAction sees it, and on the client ClientCommonPacketListenerImpl.handleShowDialog hops to the client’s processor before touching the screen stack. Exactly one thing in this system ticks: WaitingForResponseScreen, counting ticks to reveal and then enable its Back button.

Four ways a dialog opens, and two of them never ask the server

Four things open a dialog in play; the configuration-phase sender, DebugConfigCommand, sends its packet itself (below). Two of the four go through ServerPlayer.openDialog, the server-side entry point, and the other two open it on the client with no packet at all.

/dialog show is the obvious one. A click event dispatched on the client is the interesting one, because “a component with a click event” is not the same as “a component whose click events are dispatched”: there are exactly three places on the client where a show dialog event actually is — chat, a book, and DialogScreen itself, which dispatches its own buttons and body text. An item’s name or lore is tooltip text and dispatches nothing.

A sign is clicked, but its click event is never dispatched on the client. SignBlockEntity reads the event server-side and calls ServerPlayer.openDialog directly — the same way it runs a run command event, and the reason a sign is the one clickable thing the client never gets to vet (permissions).

A tag is the fourth, and it needs no server at all in the moment. DialogTags.PAUSE_SCREEN_ADDITIONS and DialogTags.QUICK_ACTIONS let a data pack put dialogs behind a pause-menu button and behind a hotkey (with the shipped tags empty, that button opens the server’s links, when it sent any), and Dialogs holds the three the jar ships. Closing one from the server is ClientboundClearDialogPacket, and it is registered in both phases, as the two packets already on this page are — ClientboundShowDialogPacket going out and ServerboundCustomClickActionPacket coming back. Three packets, both protocols, one system.

What a dialog is made of

Inside a dialog, the parts dispatch on registries of their own the same way the dialog does: DialogBody over BuiltInRegistries.DIALOG_BODY_TYPE (PlainMessage and ItemBody), InputControl over BuiltInRegistries.INPUT_CONTROL_TYPE, and ActionButton carrying a CommonButtonData of label, tooltip and width. DialogBodyTypes, InputControlTypes and ActionTypes are the three bootstraps that fill them in code, StaticAction is the plain “do this fixed click event” action, and DialogCommand is /dialog itself. DialogBodyHandlers and InputControlHandlers are the client-side factory maps that mirror them.

One of those parts reaches outside the system. An input’s key is validated by ParsedTemplate against StringTemplate.isValidVariableName — the same rule a macro function’s parameters obey — which is why every input can be named in a CommandTemplate, and the seam into functions and macros.

What a data pack cannot do

Three defences are built into the model rather than into any particular dialog, and each one exists because the feature would otherwise be a way to trap a player.

The exit is not optional. DialogScreen’s initialisation is final: it always adds a warning button that opens a nested confirm screen offering to disconnect, and repositions it if a layout would push it off-screen. A dialog whose after-action waits for a response swaps in WaitingForResponseScreen, which reveals a Back button after a second and enables it after five.

Pausing is validated by the codec, wherever it decodes. A dialog that pauses the game with an after-action that never unpauses is rejected, because it would strand the player in a paused world. The check sits on CommonDialogData’s codec rather than on the loader, and MapCodec.validate is applied to both directions of the codec — so it runs on the server as it encodes and on the client as it decodes, which is what covers a dialog sent inline in the configuration phase.

A button that runs a command is not simply a chat command. It goes through ClientPacketListener.sendUnattendedCommand, which parses it up to twice and, on three of its four outcomes, shows you a confirmation screen before sending anything (permissions owns the four outcomes). What is this system’s own is the phase: the configuration-phase DialogConnectionAccess refuses to run a command at all, logging a warning instead, so a button on a dialog shown before you are in a world can do everything except that.

The extension point vanilla does not use

MinecraftServer.handleCustomClickAction is one line, logging at debug. The entire custom-action mechanism — an arbitrary id plus an arbitrary NBT payload, sent from a dialog, a chat message or a book (a sign’s is handed to it on the server) — exists for data packs and server software to build on. The game itself only defines the transport, and defends it with a 32 KB NBT accounter and a 64 KB cap on the payload’s own length prefix.

The same is true one level up: the only vanilla sender of a configuration-phase dialog is DebugConfigCommand, which the game registers only in a dedicated server’s command set, and only when started with its debug system properties (Brigadier and commands). A server really can put a form in front of you before you are in the world. Vanilla does it only there.

Where to look

Dialog and CommonDialogData for the model, then Action — the value-getter indirection is the only subtle thing in the whole system. DialogScreens for how a codec becomes a screen, and MinecraftServer.handleCustomClickAction for the one line that is the extension point.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Game tests

Verified against Minecraft 26.3 · Part XIII · Run /test run * on a vanilla server and one test runs, and passes. The suite is not in the game — a test is a data-pack file, the Java body is a value the JSON points at, and the shipped jar declares exactly one of each.

Game tests are how Mojang checks that a piston still pushes and a hopper still pulls: a small structure is pasted into a spare corner of a world, a test body runs against it for a bounded number of ticks, and a block beside it turns green or red. That much is the familiar part. The unusual part is where a test lives.

There is no GameTest annotation and no test registry class. A test is a registry element loaded from data/<ns>/test_instance/, and the Java body — when there is one — is a value in a second registry, Registries.TEST_FUNCTION, that the JSON points at. The Java half is a payload, not the declaration. Which means the shipped jar contains GameTestInstances’ single always-pass instance, BuiltinTestFunctions’ body for it, and GameTestEnvironments’ default environment — an empty TestEnvironmentDefinition.AllOf — and the real suite lives in Mojang’s test sources, not in the game you downloaded.

This is the data-driven type pattern again, and game tests are its most complete instance. Five registries hold the system: Registries.TEST_INSTANCE and Registries.TEST_ENVIRONMENT are loaded from a data pack, BuiltInRegistries.TEST_INSTANCE_TYPE and BuiltInRegistries.TEST_ENVIRONMENT_DEFINITION_TYPE are the built-in type registries the JSON in those two dispatches on, and Registries.TEST_FUNCTION holds the Java bodies.

The cast

classwhat it decides
GameTestInstancethe registry element. GameTestInstance.run takes a GameTestHelper and is the body — BlockBasedTestInstance needs no Java at all, FunctionGameTestInstance invokes a Registries.TEST_FUNCTION entry
TestDatathe declaration record every instance delegates to: environment, dimension, structure, tick budgets, required, rotation, manual-only, the two retry counts, sky access, padding
TestEnvironmentDefinitionthe seven ways to bend the world for a test, shaped as an undo log
GameTestBatcha group of tests keyed by their environment holder and their dimension. A batch is an environment, in one dimension
GameTestRunnerowns the batches and the two structure spawners, and runs a test again when ReportGameListener, reading the run’s retry options or the test’s own two retry counts, asks it to
GameTestInfoone run of one test: its position, its timeout, its sequences, its outcome
GameTestHelperthe entire surface a test body sees — coordinate translation, world edits, spawning, assertions, outcomes
TestInstanceBlockEntitythe block entity that owns a test’s bounding box, status and beacon beam, and does the real work of placing, saving and encasing the structure

Every class in net/minecraft/gametest/framework is server-side, and net/minecraft/gametest/Main is the headless entry point beside it. The screens that author a test are not in it — they are client-only and live with the rest of the GUI, and they are below.

The objects, and how they nest

What a test is and what running it builds are two sets of objects, and the figure shows what each one holds.

classDiagram
    class GameTestInstance {
        <<abstract>>
        TestData info
        run(GameTestHelper)
    }
    class TestData {
        Identifier structure
        int maxTicks
        int setupTicks
        int maxAttempts
    }
    class TestEnvironmentDefinition {
        <<interface>>
        setup(ServerLevel)
        teardown(ServerLevel, saved)
    }
        class GameTestBatch {
        Collection gameTestInfos
        ResourceKey dimension
    }
    class GameTestInfo {
        int tickCount
    }
    class GameTestHelper {
        GameTestInfo testInfo
    }
    class TestInstanceBlockEntity

    GameTestInstance --> TestData : info
    TestData --> TestEnvironmentDefinition : environment
    GameTestBatch --> TestEnvironmentDefinition : keyed by
    GameTestBatch --> GameTestInfo : fifty by default
    GameTestInfo --> GameTestInstance : test
    GameTestInfo --> TestInstanceBlockEntity : the block in the world
    GameTestInfo ..> GameTestHelper : a new one at tick zero

A run at the top — a batch is one environment’s worth of test runs in one dimension — and the declaration below it, an instance, its record and the environment the record names; a solid arrow is a field, labelled with its name or its meaning, and the dotted one is the helper each run makes for its body.

A batch is not a name: it is an environment, in a dimension. GameTestBatchFactory groups tests by their TestEnvironmentDefinition holder and their dimension, because that pair is what GameTestBatch is keyed by, and each group is split into runs of fifty (a default, not a cap — /test verify builds batches of a hundred). One environment is active at a time on the runner, and moving between batches tears the old one down and stands the new one up.

The environment interface is an undo log. TestEnvironmentDefinition.setup returns a value that TestEnvironmentDefinition.teardown is handed back, and the seven kinds divide three ways. Five bend one thing about the world and hand back what it was, so teardown puts it back (the weather to its kind, not to its old countdowns): TestEnvironmentDefinition.ClockTime, TestEnvironmentDefinition.SetDifficulty, TestEnvironmentDefinition.SetGameRules, TestEnvironmentDefinition.Timelines and TestEnvironmentDefinition.Weather. TestEnvironmentDefinition.Functions is the exception with no state to return — it runs one data-pack function on the way in and another on the way out, either one optional. And TestEnvironmentDefinition.AllOf is the composite: it returns its children’s activations and unwinds them in reverse.

One test, from a command to a green block

sequenceDiagram
    participant TC as TestCommand
    participant GTR as GameTestRunner
    participant TIB as TestInstance<br/>BlockEntity
    participant GTT as GameTestTicker
    participant GI as GameTestInfo
    participant RGL as ReportGameListener

    TC->>GTR: the batches, by environment and dimension
    GTR->>GI: prepareTestStructure, for each run
    GI->>TIB: placeStructure, then encaseStructure
    GTR->>GTR: activate the environment — keep what setup returns
    GTR->>GTT: add, for every run whose test block was made
    rect rgba(0, 0, 0, 0.04)
    Note over GTT,GI: every server tick, from a negative count
    GTT->>GI: tick
    GI->>GI: startTest at zero — the body runs with a new helper
    GI->>GI: succeed, or fail with a GameTestException
    GI->>RGL: testPassed or testFailed
    RGL->>TIB: setSuccess or setErrorMessage
    RGL->>RGL: say, then GlobalTestReporter
    end

One test from /test run to its beam — the structure is placed before the environment is set up, the body runs once when a negative tick count reaches zero, and the listener writes the outcome to the block, whose beam is green, red, or orange for an optional test that failed.

Game tests tick on the server thread, from GameTestTicker in one of the last zones MinecraftServer.tickChildren opens, and only while the tick-rate manager reports the game running normally — so /tick freeze suspends a running test where it stands (the server tick for the order, and its questions for what a freeze does and does not stop).

The structure comes first, then the environment. For every run in a batch GameTestRunner has its spawner call GameTestInfo.prepareTestStructure, which has the run’s block entity paste the structure (TestInstanceBlockEntity.placeStructure) and wall it in with barriers (TestInstanceBlockEntity.encaseStructure); only then is the batch’s environment activated, and only the runs whose test block was made are handed to the ticker — a run whose paste failed goes too, already carrying its error.

Setup ticks run before tick zero. GameTestInfo.startExecution starts its counter negative — by the declared setup ticks, plus the spawner’s own tick delay, plus one — so the body runs when the count reaches zero, in GameTestInfo.startTest, which hands it a new GameTestHelper.

A sequence catches one kind of exception and only one. GameTestSequence is the “do this, wait, then assert that” chain — GameTestSequence.thenExecuteAfter, GameTestSequence.thenWaitUntil, GameTestSequence.thenSucceed — and it uses an exception as ordinary control flow, at most one thrown and swallowed per sequence per tick. What it catches is a GameTestAssertException: an assertion that has not come true yet is not a failure, so the sequence swallows it and tries again next tick. A timeout is a different subclass — GameTestTimeoutException, which GameTestInfo builds and records when the tick count passes the budget, never throwing it — so nothing can swallow it: to GameTestInfo, which records whatever GameTestException ends the run, a timeout is one more failure.

Reporting is a listener chain, and it writes to up to four places. Chat, the block, the progress bar and the report. A finished run calls GameTestListener.testPassed or GameTestListener.testFailed on each of its listeners, and ReportGameListener answers with TestInstanceBlockEntity.setSuccess or TestInstanceBlockEntity.setErrorMessage, which is what colours the beam. ReportGameListener is also what says something in chat, to every player; MultipleTestTracker is the progress bar, which only the headless server prints; and GlobalTestReporter hands the outcome to one of two reporters, LogTestReporter, which logs the failures, or JUnitLikeTestReporter, which writes every result to an XML file.

What a test leaves behind is not undone. The environment has its undo log; the world does not. A passing test has its barrier shell removed and the non-player entities in and around it discarded, and nothing else: the pasted blocks stay where they were put and the test instance block stays with them — except under /test verify, which clears each finished batch’s space and breaks its test blocks before the next. When its batch finishes, or under /test verify when a test fails, the runner unforces every force-loaded chunk in the level, the test’s and any other’s. A failing test does not even lose its shell, which is the point — the failure is left standing so it can be walked into and looked at. Clearing any of it is a command: /test clearall walks the test instance blocks in range, clears the space around each, takes the barriers off and breaks the block, and a test instance block’s own reset re-pastes the structure over whatever the last run left.

A test with no Java in it

BlockBasedTestInstance runs a test built entirely from TestBlocks inside the structure. TestBlockMode has four values — start, log, fail and accept — and the rules are as simple as they sound: exactly one start block emits redstone to begin, an accept block being triggered is a pass, and a fail block being triggered is a failure carrying its stored message. That is a unit test authored in-game with a redstone circuit and shipped as a structure plus a JSON.

The client half the framework’s package list hides is what makes that practical: TestInstanceBlockEditScreen and TestBlockEditScreen are how a test is authored in game, TestInstanceRenderer draws the bounding box, and GameTestBlockHighlightRenderer is the sole consumer of ClientboundGameTestHighlightPosPacket. Both serverbound test packets are sent by the client, from those screens: one sets a test block’s fields, and the other sets a test instance block’s test, size and rotation and queries, runs, resets or saves it, from inside the game. TestInstanceBlock is the block itself; TestInstanceBlockEntity, in the cast above, is where the work is.

Three things a running server should know

/test exists on every server, not only in a development environment. Only TestCommand’s export subcommands are gated on running from an IDE; the rest sit at Commands.LEVEL_GAMEMASTERS (permissions). How it addresses a test is the data-driven move again: TestFinder hands a subcommand the tests it names, as Registries.TEST_INSTANCE holders where it takes an id selector or asks for the last failures, and as test blocks found by position otherwise, and the glob in /test run * is ResourceSelectorArgument, the one argument type in the game that takes one.

Test instance blocks are points of interest. Locating every test within a 250-block radius is a POI query, not a block scan (points of interest), which is what makes /test’s radius subcommands cheap — and what makes a world full of saved tests carry them in its POI storage.

Underneath both sits a layer with no command of its own. StructureUtils and StructureGridSpawner clear the space, lay tests out in a grid, transform the far corner and find every test block by position (jigsaw and templates owns the template machinery they call). GameTestServer is the third MinecraftServer subclass — beside the integrated and the dedicated ones (anatomy) — driven by GameTestMainUtil, and one thing it changes that matters here is GameTestServer.waitUntilNextTick: it drains tasks instead of sleeping, so a headless test run goes flat out rather than at twenty ticks a second. It also installs a no-op gizmo collector (debugging the running game).

Where to look

GameTestInstance and TestData for what a test is, then GameTestInfo for what happens on a tick, then TestInstanceBlockEntity for what a test costs the world, and GameTestHelper when you want to write one.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Reference

Verified against Minecraft 26.3 · Reference · The shelf behind the lectures: everything a viewer would pause the video to read, kept where no lecture has to stop for it.

A lecture explains one thing at a time, and it cannot stop to list the 44 entity-data serializers or the ten bits of a flag word without losing the room. The rule for this tier is one question: would a viewer pause the video to read this? If yes, it lives here and the page links to it — so a shelf page is reached from the page that needs it, and the table below is grouped by the one question a reader of a catalogue should ask of it rather than for browsing.

That question is not what does it list but how is it kept, because a catalogue is only as good as the version it was read from. Thirteen of the twenty-three are rewritten by a tool on every deploy, so their rows are re-derived each time, though the sentences each tool types around its tables, and the odd cell it types into a row, are not. The other ten were written by a session reading the classes one at a time, and go stale exactly the way this page’s own last column did.

A shelf is looked up, not watched, so a Reference page is exempt from the rule that every page carries a figure: it draws one only where its subject is a shape — the conversions between coordinate spaces, the ways work crosses a thread boundary — and everywhere else, this page included, the table or the list is the figure.

How each page is kept, and who leans on it

Generated · decompile is read off the source, generated · corpus off the book itself — its pages, and the lane key in its page template — and both are rewritten on every deploy; hand-kept is a session’s reading. The last column’s numerals are the parts in sidebar order, I to XIII, as the lecture map lists them.

pagewhat it listskept bythe parts whose landing pages point at it
Packetsevery packet, by protocol group and directiongenerated · decompileIII, V, VI, VII, VIII, IX, X, XIII
Registriesevery registry key but the root’s: built-in, data-pack, syncedgenerated · decompileII, IV, V, VI, VII, IX, XII, XIII
Data componentsevery DataComponentType in DataComponents, persistent or not, and how it crosses the wiregenerated · decompileII, V, VII, VIII, IX
Game rulesevery rule, type, category, defaultgenerated · decompileIII, IV, V, VI, VIII
Attributesevery attribute: default, range, sentiment, syncablegenerated · decompileVI, VIII
Entity data serializersall 44, in registration order, which is the wire idgenerated · decompileVI
Enchantment hooksevery public EnchantmentHelper entry point and its callersgenerated · decompileVII
Loot context parameter setsall thirty-one, with required and optional keysgenerated · decompileVII, XIII
Entity spawn reasonsall nineteen, and which classes change behaviour for eachgenerated · decompileVI
Structure spawn overridesthe six structures that replace a biome’s spawn list, and with whatgenerated · decompileVI, XII
The weapon helpersthe seven Item.Properties helpers and every item built by onegenerated · decompileVII
Block update flagsthe ten bits of Level.setBlock’s flag wordhand-keptIV, V
Damage outside LivingEntitywhat each of the twenty-two non-living classes does when hithand-keptVI, VIII
What the HUD draws, and whenevery HUD element and the condition, if any, it is behindhand-keptX
Submit phases and feature renderersthe fifteen phases and the twelve renderers, in the order the code lists themhand-keptXI
Density-function nodesthe forty-four node types, what the compiler turns each into, what range each reports, and which ids the shipped data writeshand-keptXII
Threadsevery thread, who makes it, what may run on ithand-keptI, III, IV, VI, IX, X, XI
Math and primitivesthe coordinate spaces, packings, shapes and random sourceshand-keptII, IV, V, VI, XII
Level data and ruleswho owns the seed, spawn, rules and border, and which file each is inhand-keptIII, IV, VIII, IX, XII
Naming driftthe 1.21-era names a reader will reach for, and what 26.3 calls themhand-keptI, II, VII, X, XI, XII
Glossarya short entry per term, and the page that owns ithand-keptII, V, VI, X, XI, XII, XIII
Diagram lanesevery lane abbreviation and the class it means, and the ten that mean a thread, the process, the wire, the disk, a service or the game’s own code insteadgenerated · corpusevery part
Class indexevery class a written page backticks or a figure names, and the pages that name itgenerated · corpus—

The two generated kinds differ in what they are read from. python tools/gen_reference.py all rewrites eleven of them from the decompile — its declarations, its call sites and its data files — so a version bump re-derives their rows rather than re-reading them; the other two are read off this book instead, by python tools/verify_names.py --index (the class index, from every written page’s backticked names and every class a figure names) and python tools/check_lanes.py --index (the lanes, from the key in the book’s page template). Hand-kept means a part session read the classes and wrote the rows, tools/verify_names.py checks the backticked names on the page, and a fact-check re-reads the rows — the code’s own orders drift on a version bump, and three of these pages (the HUD, the submit phases, the density-function nodes) are laid out in one.

The last column is one a tool can settle, and it is now settled by one: each of the thirteen parts’ landing pages names the shelf pages it uses, and tools/check_deps.py re-derives this column from those thirteen sections and refuses to publish when the two disagree. It disagreed in six rows on the day the check was written, which is what a hand-kept column does.

For agents: the whole site is also served as one file at /llms-full.txt.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Packets

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every packet the game defines, by the PacketTypes class that declares it. common, cookie and ping packets are shared by more than one protocol phase; the exact phase→packet bindings are in the five *Protocols classes (GameProtocols, ConfigurationProtocols, LoginProtocols, StatusProtocols, HandshakeProtocols), which bind the shared groups too. See Packets and stream codecs.

groupclientboundserverbound
common (CommonPacketTypes)146
configuration (ConfigurationPacketTypes)63
cookie (CookiePacketTypes)11
game (GamePacketTypes)12961
handshake (HandshakePacketTypes)01
login (LoginPacketTypes)54
ping (PingPacketTypes)11
status (StatusPacketTypes)11
total15778

common — CommonPacketTypes — shared across phases

iddirectionclass
clear_dialogclientboundClientboundClearDialogPacket
custom_payloadclientboundClientboundCustomPayloadPacket
custom_report_detailsclientboundClientboundCustomReportDetailsPacket
disconnectclientboundClientboundDisconnectPacket
keep_aliveclientboundClientboundKeepAlivePacket
pingclientboundClientboundPingPacket
post_effectsclientboundClientboundPostEffectsPacket
resource_pack_popclientboundClientboundResourcePackPopPacket
resource_pack_pushclientboundClientboundResourcePackPushPacket
server_linksclientboundClientboundServerLinksPacket
show_dialogclientboundClientboundShowDialogPacket
store_cookieclientboundClientboundStoreCookiePacket
transferclientboundClientboundTransferPacket
update_tagsclientboundClientboundUpdateTagsPacket
client_informationserverboundServerboundClientInformationPacket
custom_click_actionserverboundServerboundCustomClickActionPacket
custom_payloadserverboundServerboundCustomPayloadPacket
keep_aliveserverboundServerboundKeepAlivePacket
pongserverboundServerboundPongPacket
resource_packserverboundServerboundResourcePackPacket

configuration — ConfigurationPacketTypes

iddirectionclass
code_of_conductclientboundClientboundCodeOfConductPacket
finish_configurationclientboundClientboundFinishConfigurationPacket
registry_dataclientboundClientboundRegistryDataPacket
reset_chatclientboundClientboundResetChatPacket
select_known_packsclientboundClientboundSelectKnownPacks
update_enabled_featuresclientboundClientboundUpdateEnabledFeaturesPacket
accept_code_of_conductserverboundServerboundAcceptCodeOfConductPacket
finish_configurationserverboundServerboundFinishConfigurationPacket
select_known_packsserverboundServerboundSelectKnownPacks
iddirectionclass
cookie_requestclientboundClientboundCookieRequestPacket
cookie_responseserverboundServerboundCookieResponsePacket

game — GamePacketTypes

iddirectionclass
add_entityclientboundClientboundAddEntityPacket
add_transient_blockclientboundClientboundAddTransientBlockPacket
animateclientboundClientboundAnimatePacket
award_statsclientboundClientboundAwardStatsPacket
block_changed_ackclientboundClientboundBlockChangedAckPacket
block_destructionclientboundClientboundBlockDestructionPacket
block_entity_dataclientboundClientboundBlockEntityDataPacket
block_eventclientboundClientboundBlockEventPacket
block_updateclientboundClientboundBlockUpdatePacket
boss_eventclientboundClientboundBossEventPacket
bundleclientboundClientboundBundlePacket
bundle_delimiterclientboundClientboundBundleDelimiterPacket
change_difficultyclientboundClientboundChangeDifficultyPacket
chunk_batch_finishedclientboundClientboundChunkBatchFinishedPacket
chunk_batch_startclientboundClientboundChunkBatchStartPacket
chunks_biomesclientboundClientboundChunksBiomesPacket
clear_titlesclientboundClientboundClearTitlesPacket
command_suggestionsclientboundClientboundCommandSuggestionsPacket
commandsclientboundClientboundCommandsPacket
container_closeclientboundClientboundContainerClosePacket
container_set_contentclientboundClientboundContainerSetContentPacket
container_set_dataclientboundClientboundContainerSetDataPacket
container_set_slotclientboundClientboundContainerSetSlotPacket
cooldownclientboundClientboundCooldownPacket
custom_chat_completionsclientboundClientboundCustomChatCompletionsPacket
damage_eventclientboundClientboundDamageEventPacket
debug/block_valueclientboundClientboundDebugBlockValuePacket
debug/chunk_valueclientboundClientboundDebugChunkValuePacket
debug/entity_valueclientboundClientboundDebugEntityValuePacket
debug/eventclientboundClientboundDebugEventPacket
debug_sampleclientboundClientboundDebugSamplePacket
delete_chatclientboundClientboundDeleteChatPacket
disguised_chatclientboundClientboundDisguisedChatPacket
entity_eventclientboundClientboundEntityEventPacket
entity_position_syncclientboundClientboundEntityPositionSyncPacket
explodeclientboundClientboundExplodePacket
forget_level_chunkclientboundClientboundForgetLevelChunkPacket
game_eventclientboundClientboundGameEventPacket
game_rule_valuesclientboundClientboundGameRuleValuesPacket
game_test_highlight_posclientboundClientboundGameTestHighlightPosPacket
hurt_animationclientboundClientboundHurtAnimationPacket
initialize_borderclientboundClientboundInitializeBorderPacket
level_chunk_with_lightclientboundClientboundLevelChunkWithLightPacket
level_eventclientboundClientboundLevelEventPacket
level_particlesclientboundClientboundLevelParticlesPacket
light_updateclientboundClientboundLightUpdatePacket
loginclientboundClientboundLoginPacket
low_disk_space_warningclientboundClientboundLowDiskSpaceWarningPacket
map_item_dataclientboundClientboundMapItemDataPacket
merchant_offersclientboundClientboundMerchantOffersPacket
mount_screen_openclientboundClientboundMountScreenOpenPacket
move_entity_posclientboundClientboundMoveEntityPacket.Pos
move_entity_pos_rotclientboundClientboundMoveEntityPacket.PosRot
move_entity_rotclientboundClientboundMoveEntityPacket.Rot
move_minecart_along_trackclientboundClientboundMoveMinecartPacket
move_vehicleclientboundClientboundMoveVehiclePacket
open_bookclientboundClientboundOpenBookPacket
open_screenclientboundClientboundOpenScreenPacket
open_sign_editorclientboundClientboundOpenSignEditorPacket
place_ghost_recipeclientboundClientboundPlaceGhostRecipePacket
player_abilitiesclientboundClientboundPlayerAbilitiesPacket
player_chatclientboundClientboundPlayerChatPacket
player_combat_endclientboundClientboundPlayerCombatEndPacket
player_combat_enterclientboundClientboundPlayerCombatEnterPacket
player_combat_killclientboundClientboundPlayerCombatKillPacket
player_info_removeclientboundClientboundPlayerInfoRemovePacket
player_info_updateclientboundClientboundPlayerInfoUpdatePacket
player_look_atclientboundClientboundPlayerLookAtPacket
player_positionclientboundClientboundPlayerPositionPacket
player_rotationclientboundClientboundPlayerRotationPacket
projectile_powerclientboundClientboundProjectilePowerPacket
recipe_book_addclientboundClientboundRecipeBookAddPacket
recipe_book_removeclientboundClientboundRecipeBookRemovePacket
recipe_book_settingsclientboundClientboundRecipeBookSettingsPacket
remove_entitiesclientboundClientboundRemoveEntitiesPacket
remove_mob_effectclientboundClientboundRemoveMobEffectPacket
reset_scoreclientboundClientboundResetScorePacket
respawnclientboundClientboundRespawnPacket
rotate_headclientboundClientboundRotateHeadPacket
section_blocks_updateclientboundClientboundSectionBlocksUpdatePacket
select_advancements_tabclientboundClientboundSelectAdvancementsTabPacket
server_dataclientboundClientboundServerDataPacket
set_action_bar_textclientboundClientboundSetActionBarTextPacket
set_border_centerclientboundClientboundSetBorderCenterPacket
set_border_lerp_sizeclientboundClientboundSetBorderLerpSizePacket
set_border_sizeclientboundClientboundSetBorderSizePacket
set_border_warning_delayclientboundClientboundSetBorderWarningDelayPacket
set_border_warning_distanceclientboundClientboundSetBorderWarningDistancePacket
set_cameraclientboundClientboundSetCameraPacket
set_chunk_cache_centerclientboundClientboundSetChunkCacheCenterPacket
set_chunk_cache_radiusclientboundClientboundSetChunkCacheRadiusPacket
set_cursor_itemclientboundClientboundSetCursorItemPacket
set_default_spawn_positionclientboundClientboundSetDefaultSpawnPositionPacket
set_display_objectiveclientboundClientboundSetDisplayObjectivePacket
set_entity_dataclientboundClientboundSetEntityDataPacket
set_entity_linkclientboundClientboundSetEntityLinkPacket
set_entity_motionclientboundClientboundSetEntityMotionPacket
set_equipmentclientboundClientboundSetEquipmentPacket
set_experienceclientboundClientboundSetExperiencePacket
set_healthclientboundClientboundSetHealthPacket
set_held_slotclientboundClientboundSetHeldSlotPacket
set_objectiveclientboundClientboundSetObjectivePacket
set_passengersclientboundClientboundSetPassengersPacket
set_player_inventoryclientboundClientboundSetPlayerInventoryPacket
set_player_teamclientboundClientboundSetPlayerTeamPacket
set_scoreclientboundClientboundSetScorePacket
set_simulation_distanceclientboundClientboundSetSimulationDistancePacket
set_subtitle_textclientboundClientboundSetSubtitleTextPacket
set_timeclientboundClientboundSetTimePacket
set_title_textclientboundClientboundSetTitleTextPacket
set_titles_animationclientboundClientboundSetTitlesAnimationPacket
soundclientboundClientboundSoundPacket
sound_entityclientboundClientboundSoundEntityPacket
start_configurationclientboundClientboundStartConfigurationPacket
stop_soundclientboundClientboundStopSoundPacket
swing_animationclientboundClientboundSwingAnimationPacket
system_chatclientboundClientboundSystemChatPacket
tab_listclientboundClientboundTabListPacket
tag_queryclientboundClientboundTagQueryPacket
take_item_entityclientboundClientboundTakeItemEntityPacket
teleport_entityclientboundClientboundTeleportEntityPacket
test_instance_block_statusclientboundClientboundTestInstanceBlockStatus
ticking_stateclientboundClientboundTickingStatePacket
ticking_stepclientboundClientboundTickingStepPacket
update_advancementsclientboundClientboundUpdateAdvancementsPacket
update_attributesclientboundClientboundUpdateAttributesPacket
update_mob_effectclientboundClientboundUpdateMobEffectPacket
update_recipesclientboundClientboundUpdateRecipesPacket
waypointclientboundClientboundTrackedWaypointPacket
accept_teleportationserverboundServerboundAcceptTeleportationPacket
attackserverboundServerboundAttackPacket
block_entity_tag_queryserverboundServerboundBlockEntityTagQueryPacket
bundle_item_selectedserverboundServerboundSelectBundleItemPacket
change_difficultyserverboundServerboundChangeDifficultyPacket
change_game_modeserverboundServerboundChangeGameModePacket
chatserverboundServerboundChatPacket
chat_ackserverboundServerboundChatAckPacket
chat_commandserverboundServerboundChatCommandPacket
chat_command_signedserverboundServerboundChatCommandSignedPacket
chat_session_updateserverboundServerboundChatSessionUpdatePacket
chunk_batch_receivedserverboundServerboundChunkBatchReceivedPacket
client_commandserverboundServerboundClientCommandPacket
client_tick_endserverboundServerboundClientTickEndPacket
command_suggestionserverboundServerboundCommandSuggestionPacket
configuration_acknowledgedserverboundServerboundConfigurationAcknowledgedPacket
container_button_clickserverboundServerboundContainerButtonClickPacket
container_clickserverboundServerboundContainerClickPacket
container_closeserverboundServerboundContainerClosePacket
container_slot_state_changedserverboundServerboundContainerSlotStateChangedPacket
debug_subscription_requestserverboundServerboundDebugSubscriptionRequestPacket
edit_bookserverboundServerboundEditBookPacket
entity_tag_queryserverboundServerboundEntityTagQueryPacket
interactserverboundServerboundInteractPacket
jigsaw_generateserverboundServerboundJigsawGeneratePacket
lock_difficultyserverboundServerboundLockDifficultyPacket
move_player_posserverboundServerboundMovePlayerPacket.Pos
move_player_pos_rotserverboundServerboundMovePlayerPacket.PosRot
move_player_rotserverboundServerboundMovePlayerPacket.Rot
move_player_status_onlyserverboundServerboundMovePlayerPacket.StatusOnly
move_vehicleserverboundServerboundMoveVehiclePacket
paddle_boatserverboundServerboundPaddleBoatPacket
pick_item_from_blockserverboundServerboundPickItemFromBlockPacket
pick_item_from_entityserverboundServerboundPickItemFromEntityPacket
place_recipeserverboundServerboundPlaceRecipePacket
player_abilitiesserverboundServerboundPlayerAbilitiesPacket
player_actionserverboundServerboundPlayerActionPacket
player_commandserverboundServerboundPlayerCommandPacket
player_inputserverboundServerboundPlayerInputPacket
player_loadedserverboundServerboundPlayerLoadedPacket
punchserverboundServerboundPunchPacket
recipe_book_change_settingsserverboundServerboundRecipeBookChangeSettingsPacket
recipe_book_seen_recipeserverboundServerboundRecipeBookSeenRecipePacket
rename_itemserverboundServerboundRenameItemPacket
seen_advancementsserverboundServerboundSeenAdvancementsPacket
select_tradeserverboundServerboundSelectTradePacket
set_beaconserverboundServerboundSetBeaconPacket
set_carried_itemserverboundServerboundSetCarriedItemPacket
set_command_blockserverboundServerboundSetCommandBlockPacket
set_command_minecartserverboundServerboundSetCommandMinecartPacket
set_creative_mode_slotserverboundServerboundSetCreativeModeSlotPacket
set_game_ruleserverboundServerboundSetGameRulePacket
set_jigsaw_blockserverboundServerboundSetJigsawBlockPacket
set_structure_blockserverboundServerboundSetStructureBlockPacket
set_test_blockserverboundServerboundSetTestBlockPacket
sign_updateserverboundServerboundSignUpdatePacket
spectator_actionserverboundServerboundSpectatorActionPacket
teleport_to_entityserverboundServerboundTeleportToEntityPacket
test_instance_block_actionserverboundServerboundTestInstanceBlockActionPacket
use_itemserverboundServerboundUseItemPacket
use_item_onserverboundServerboundUseItemOnPacket

handshake — HandshakePacketTypes

iddirectionclass
intentionserverboundClientIntentionPacket

login — LoginPacketTypes

iddirectionclass
custom_queryclientboundClientboundCustomQueryPacket
helloclientboundClientboundHelloPacket
login_compressionclientboundClientboundLoginCompressionPacket
login_disconnectclientboundClientboundLoginDisconnectPacket
login_finishedclientboundClientboundLoginFinishedPacket
custom_query_answerserverboundServerboundCustomQueryAnswerPacket
helloserverboundServerboundHelloPacket
keyserverboundServerboundKeyPacket
login_acknowledgedserverboundServerboundLoginAcknowledgedPacket

ping — PingPacketTypes — shared across phases

iddirectionclass
pong_responseclientboundClientboundPongResponsePacket
ping_requestserverboundServerboundPingRequestPacket

status — StatusPacketTypes

iddirectionclass
status_responseclientboundClientboundStatusResponsePacket
status_requestserverboundServerboundStatusRequestPacket

Registries

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every registry key in the game but the root registry’s own. 156 of them are declared in Registries; five more are declared by the class that owns them (ClockTimeMarkers, EquipmentAssets, RecipePropertySet, ServerFunctionLibrary, WaypointStyleAssets) with the public ResourceKey.createRegistryKey rather than Registries’ private helper, which is why this total is larger than the 156 identifiers and registries counts. Built-in registries are created empty when BuiltInRegistries loads, filled during Bootstrap.bootStrap — most by BuiltInRegistries.bootStrap through their owners’ own bootstrap methods, the block, item and entity-type registries earlier, by their owner classes’ static initialisers, which Bootstrap.bootStrap reaches first — and frozen; data-pack registries are loaded per world by RegistryDataLoader from JSON (WORLD_REGISTRIES, or DIMENSION_REGISTRIES for level stems), and the reloadable ones by the same loader on every data-pack reload (RELOADABLE_REGISTRIES, read by ReloadableServerRegistries); synced ones are sent to the client in the configuration phase (SYNCHRONIZED_REGISTRIES). A key that is none of these is a registry type the game reasons about without a global instance (e.g. per-world or client-side). See Identifiers and registries.

161 keys · 95 built-in · 60 data-pack, 8 of them reloadable · 32 synced

keyelement typekindsynced
activity (Registries.ACTIVITY)Activitybuilt-in
advancement (Registries.ADVANCEMENT)Advancementdata-pack (reloadable)
attribute (Registries.ATTRIBUTE)Attributebuilt-in
attribute_type (Registries.ATTRIBUTE_TYPE)AttributeType<…>built-in
banner_pattern (Registries.BANNER_PATTERN)BannerPatterndata-packyes
block (Registries.BLOCK)Blockbuilt-in
block_entity_type (Registries.BLOCK_ENTITY_TYPE)BlockEntityType<…>built-in
block_predicate_type (Registries.BLOCK_PREDICATE_TYPE)BlockPredicateType<…>built-in
block_transformer (Registries.BLOCK_TRANSFORMER)BlockTransformerdata-packyes
cat_sound_variant (Registries.CAT_SOUND_VARIANT)CatSoundVariantdata-packyes
cat_variant (Registries.CAT_VARIANT)CatVariantdata-packyes
chat_type (Registries.CHAT_TYPE)ChatTypedata-packyes
chicken_sound_variant (Registries.CHICKEN_SOUND_VARIANT)ChickenSoundVariantdata-packyes
chicken_variant (Registries.CHICKEN_VARIANT)ChickenVariantdata-packyes
chunk_status (Registries.CHUNK_STATUS)ChunkStatusbuilt-in
clock_time_marker (ClockTimeMarkers.ROOT_ID)ClockTimeMarker—
command_argument_type (Registries.COMMAND_ARGUMENT_TYPE)ArgumentTypeInfo<…>built-in
consume_effect_type (Registries.CONSUME_EFFECT_TYPE)ConsumeEffect.Type<…>built-in
context_float_provider (Registries.CONTEXT_FLOAT_PROVIDER)ContextFloatProviderdata-pack (reloadable)
context_float_provider_type (Registries.CONTEXT_FLOAT_PROVIDER_TYPE)MapCodec<…>built-in
context_int_provider (Registries.CONTEXT_INT_PROVIDER)ContextIntProviderdata-pack (reloadable)
context_int_provider_type (Registries.CONTEXT_INT_PROVIDER_TYPE)MapCodec<…>built-in
context_key_set (Registries.CONTEXT_KEY_SET)ContextKeySetbuilt-in
cow_sound_variant (Registries.COW_SOUND_VARIANT)CowSoundVariantdata-packyes
cow_variant (Registries.COW_VARIANT)CowVariantdata-packyes
creative_mode_tab (Registries.CREATIVE_MODE_TAB)CreativeModeTabbuilt-in
custom_stat (Registries.CUSTOM_STAT)Identifierbuilt-in
damage_type (Registries.DAMAGE_TYPE)DamageTypedata-packyes
data_component_predicate_type (Registries.DATA_COMPONENT_PREDICATE_TYPE)DataComponentPredicate.Type<…>built-in
data_component_type (Registries.DATA_COMPONENT_TYPE)DataComponentType<…>built-in
debug_subscription (Registries.DEBUG_SUBSCRIPTION)DebugSubscription<…>built-in
decorated_pot_pattern (Registries.DECORATED_POT_PATTERN)DecoratedPotPatterndata-packyes
dialog (Registries.DIALOG)Dialogdata-packyes
dialog_action_type (Registries.DIALOG_ACTION_TYPE)MapCodec<…>built-in
dialog_body_type (Registries.DIALOG_BODY_TYPE)MapCodec<…>built-in
dialog_type (Registries.DIALOG_TYPE)MapCodec<…>built-in
dimension (Registries.DIMENSION)Level—
dimension (Registries.LEVEL_STEM)LevelStemdata-pack (dimension)
dimension_type (Registries.DIMENSION_TYPE)DimensionTypedata-packyes
enchantment (Registries.ENCHANTMENT)Enchantmentdata-packyes
enchantment_effect_component_type (Registries.ENCHANTMENT_EFFECT_COMPONENT_TYPE)DataComponentType<…>built-in
enchantment_entity_effect_type (Registries.ENCHANTMENT_ENTITY_EFFECT_TYPE)MapCodec<…>built-in
enchantment_level_based_value_type (Registries.ENCHANTMENT_LEVEL_BASED_VALUE_TYPE)MapCodec<…>built-in
enchantment_location_based_effect_type (Registries.ENCHANTMENT_LOCATION_BASED_EFFECT_TYPE)MapCodec<…>built-in
enchantment_provider (Registries.ENCHANTMENT_PROVIDER)EnchantmentProviderdata-pack
enchantment_provider_type (Registries.ENCHANTMENT_PROVIDER_TYPE)MapCodec<…>built-in
enchantment_value_effect_type (Registries.ENCHANTMENT_VALUE_EFFECT_TYPE)MapCodec<…>built-in
entity_sub_predicate_type (Registries.ENTITY_SUB_PREDICATE_TYPE)Codec<…>built-in
entity_type (Registries.ENTITY_TYPE)EntityType<…>built-in
environment_attribute (Registries.ENVIRONMENT_ATTRIBUTE)EnvironmentAttribute<…>built-in
equipment_asset (EquipmentAssets.ROOT_ID)EquipmentAsset—
float_provider_type (Registries.FLOAT_PROVIDER_TYPE)MapCodec<…>built-in
fluid (Registries.FLUID)Fluidbuilt-in
frog_variant (Registries.FROG_VARIANT)FrogVariantdata-packyes
function (ServerFunctionLibrary.TYPE_KEY)CommandFunction<…>—
game_event (Registries.GAME_EVENT)GameEventbuilt-in
game_rule (Registries.GAME_RULE)GameRule<…>built-in
height_provider_type (Registries.HEIGHT_PROVIDER_TYPE)HeightProviderType<…>built-in
incoming_rpc_methods (Registries.INCOMING_RPC_METHOD)IncomingRpcMethod<…>built-in
input_control_type (Registries.INPUT_CONTROL_TYPE)MapCodec<…>built-in
instrument (Registries.INSTRUMENT)Instrumentdata-packyes
int_provider_type (Registries.INT_PROVIDER_TYPE)MapCodec<…>built-in
item (Registries.ITEM)Itembuilt-in
item_modifier (Registries.ITEM_MODIFIER)LootItemFunctiondata-pack (reloadable)
jukebox_song (Registries.JUKEBOX_SONG)JukeboxSongdata-packyes
loot_condition_type (Registries.LOOT_CONDITION_TYPE)MapCodec<…>built-in
loot_function_type (Registries.LOOT_FUNCTION_TYPE)MapCodec<…>built-in
loot_nbt_provider_type (Registries.LOOT_NBT_PROVIDER_TYPE)MapCodec<…>built-in
loot_pool_entry_type (Registries.LOOT_POOL_ENTRY_TYPE)MapCodec<…>built-in
loot_score_provider_type (Registries.LOOT_SCORE_PROVIDER_TYPE)MapCodec<…>built-in
loot_table (Registries.LOOT_TABLE)LootTabledata-pack (reloadable)
map_decoration_type (Registries.MAP_DECORATION_TYPE)MapDecorationTypebuilt-in
memory_module_type (Registries.MEMORY_MODULE_TYPE)MemoryModuleType<…>built-in
menu (Registries.MENU)MenuType<…>built-in
mob_effect (Registries.MOB_EFFECT)MobEffectbuilt-in
number_format_type (Registries.NUMBER_FORMAT_TYPE)NumberFormatType<…>built-in
outgoing_rpc_methods (Registries.OUTGOING_RPC_METHOD)OutgoingRpcMethod<…>built-in
painting_variant (Registries.PAINTING_VARIANT)PaintingVariantdata-packyes
particle_type (Registries.PARTICLE_TYPE)ParticleType<…>built-in
permission_check_type (Registries.PERMISSION_CHECK_TYPE)MapCodec<…>built-in
permission_type (Registries.PERMISSION_TYPE)MapCodec<…>built-in
pig_sound_variant (Registries.PIG_SOUND_VARIANT)PigSoundVariantdata-packyes
pig_variant (Registries.PIG_VARIANT)PigVariantdata-packyes
point_of_interest_type (Registries.POINT_OF_INTEREST_TYPE)PoiTypebuilt-in
pos_rule_test (Registries.POS_RULE_TEST)PosRuleTestType<…>built-in
position_source_type (Registries.POSITION_SOURCE_TYPE)PositionSourceType<…>built-in
potion (Registries.POTION)Potionbuilt-in
predicate (Registries.PREDICATE)LootItemConditiondata-pack (reloadable)
recipe (Registries.RECIPE)Recipe<…>data-pack (reloadable)
recipe_book_category (Registries.RECIPE_BOOK_CATEGORY)RecipeBookCategorybuilt-in
recipe_display (Registries.RECIPE_DISPLAY)RecipeDisplay.Type<…>built-in
recipe_property_set (RecipePropertySet.TYPE_KEY)RecipePropertySet—
recipe_serializer (Registries.RECIPE_SERIALIZER)RecipeSerializer<…>built-in
recipe_type (Registries.RECIPE_TYPE)RecipeType<…>built-in
rule_block_entity_modifier (Registries.RULE_BLOCK_ENTITY_MODIFIER)RuleBlockEntityModifierType<…>built-in
rule_test (Registries.RULE_TEST)RuleTestType<…>built-in
sensor_type (Registries.SENSOR_TYPE)SensorType<…>built-in
slot_display (Registries.SLOT_DISPLAY)SlotDisplay.Type<…>built-in
slot_source (Registries.SLOT_SOURCE)SlotSourcedata-pack (reloadable)
slot_source_type (Registries.SLOT_SOURCE_TYPE)MapCodec<…>built-in
sound_event (Registries.SOUND_EVENT)SoundEventbuilt-in
spawn_condition_type (Registries.SPAWN_CONDITION_TYPE)MapCodec<…>built-in
stat_type (Registries.STAT_TYPE)StatType<…>built-in
sulfur_cube_archetype (Registries.SULFUR_CUBE_ARCHETYPE)SulfurCubeArchetypedata-packyes
test_environment (Registries.TEST_ENVIRONMENT)TestEnvironmentDefinition<…>data-packyes
test_environment_definition_type (Registries.TEST_ENVIRONMENT_DEFINITION_TYPE)MapCodec<…>built-in
test_function (Registries.TEST_FUNCTION)Consumer<…>built-in
test_instance (Registries.TEST_INSTANCE)GameTestInstancedata-packyes
test_instance_type (Registries.TEST_INSTANCE_TYPE)MapCodec<…>built-in
ticket_type (Registries.TICKET_TYPE)TicketTypebuilt-in
timeline (Registries.TIMELINE)Timelinedata-packyes
trade_set (Registries.TRADE_SET)TradeSetdata-pack
trial_spawner (Registries.TRIAL_SPAWNER_CONFIG)TrialSpawnerConfigdata-pack
trigger_type (Registries.TRIGGER_TYPE)CriterionTrigger<…>built-in
trim_material (Registries.TRIM_MATERIAL)TrimMaterialdata-packyes
trim_pattern (Registries.TRIM_PATTERN)TrimPatterndata-packyes
villager_profession (Registries.VILLAGER_PROFESSION)VillagerProfessionbuilt-in
villager_trade (Registries.VILLAGER_TRADE)VillagerTradedata-pack
villager_type (Registries.VILLAGER_TYPE)VillagerTypebuilt-in
waypoint_style_asset (WaypointStyleAssets.ROOT_ID)WaypointStyleAsset—
wolf_sound_variant (Registries.WOLF_SOUND_VARIANT)WolfSoundVariantdata-packyes
wolf_variant (Registries.WOLF_VARIANT)WolfVariantdata-packyes
world_clock (Registries.WORLD_CLOCK)WorldClockdata-packyes
worldgen/biome (Registries.BIOME)Biomedata-packyes
worldgen/biome_source (Registries.BIOME_SOURCE)MapCodec<…>built-in
worldgen/block_state_provider (Registries.BLOCK_STATE_PROVIDER)BlockStateProviderdata-packyes
worldgen/block_state_provider_type (Registries.BLOCK_STATE_PROVIDER_TYPE)MapCodec<…>built-in
worldgen/carver (Registries.CARVER)WorldCarverdata-pack
worldgen/carver_type (Registries.CARVER_TYPE)MapCodec<…>built-in
worldgen/chunk_generator (Registries.CHUNK_GENERATOR)MapCodec<…>built-in
worldgen/density_function (Registries.DENSITY_FUNCTION)DensityFunctiondata-pack
worldgen/density_function_type (Registries.DENSITY_FUNCTION_TYPE)MapCodec<…>built-in
worldgen/feature (Registries.FEATURE)Featuredata-pack
worldgen/feature_size_type (Registries.FEATURE_SIZE_TYPE)FeatureSizeType<…>built-in
worldgen/feature_type (Registries.FEATURE_TYPE)MapCodec<…>built-in
worldgen/flat_level_generator_preset (Registries.FLAT_LEVEL_GENERATOR_PRESET)FlatLevelGeneratorPresetdata-pack
worldgen/foliage_placer_type (Registries.FOLIAGE_PLACER_TYPE)FoliagePlacerType<…>built-in
worldgen/material_condition (Registries.MATERIAL_CONDITION)MaterialConditiondata-pack
worldgen/material_condition_type (Registries.MATERIAL_CONDITION_TYPE)MapCodec<…>built-in
worldgen/material_rule (Registries.MATERIAL_RULE)MaterialRuledata-pack
worldgen/material_rule_type (Registries.MATERIAL_RULE_TYPE)MapCodec<…>built-in
worldgen/multi_noise_biome_source_parameter_list (Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST)MultiNoiseBiomeSourceParameterListdata-pack
worldgen/noise (Registries.NOISE)NormalNoisedata-pack
worldgen/noise_settings (Registries.NOISE_SETTINGS)NoiseGeneratorSettingsdata-pack
worldgen/placed_feature (Registries.PLACED_FEATURE)PlacedFeaturedata-pack
worldgen/placement_modifier_type (Registries.PLACEMENT_MODIFIER_TYPE)MapCodec<…>built-in
worldgen/pool_alias_binding (Registries.POOL_ALIAS_BINDING)MapCodec<…>built-in
worldgen/processor_list (Registries.PROCESSOR_LIST)StructureProcessorListdata-pack
worldgen/root_placer_type (Registries.ROOT_PLACER_TYPE)RootPlacerType<…>built-in
worldgen/structure (Registries.STRUCTURE)Structuredata-pack
worldgen/structure_piece (Registries.STRUCTURE_PIECE)StructurePieceTypebuilt-in
worldgen/structure_placement (Registries.STRUCTURE_PLACEMENT)MapCodec<…>built-in
worldgen/structure_pool_element (Registries.STRUCTURE_POOL_ELEMENT)StructurePoolElementType<…>built-in
worldgen/structure_processor (Registries.STRUCTURE_PROCESSOR)MapCodec<…>built-in
worldgen/structure_set (Registries.STRUCTURE_SET)StructureSetdata-pack
worldgen/structure_type (Registries.STRUCTURE_TYPE)StructureType<…>built-in
worldgen/template_pool (Registries.TEMPLATE_POOL)StructureTemplatePooldata-pack
worldgen/tree_decorator_type (Registries.TREE_DECORATOR_TYPE)TreeDecoratorType<…>built-in
worldgen/trunk_placer_type (Registries.TRUNK_PLACER_TYPE)TrunkPlacerType<…>built-in
worldgen/world_preset (Registries.WORLD_PRESET)WorldPresetdata-pack
zombie_nautilus_variant (Registries.ZOMBIE_NAUTILUS_VARIANT)ZombieNautilusVariantdata-packyes

Data components

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every DataComponentType registered in DataComponents. Persistent types have a Codec and are written to disk; a type without one is transient (DataComponentType.isTransient), never saved but sent like the rest. Every type crosses the wire: the on the wire column says whether it declares its own StreamCodec or is sent as NBT through the one DataComponentType.Builder.build derives from its Codec, and a type with neither cannot be built. Cached persistent codecs share one EncoderCache. See Data components.

122 components

idvalue typepersistenton the wire
custom_data (DataComponents.CUSTOM_DATA)CustomDatayesNBT, through its Codec
max_stack_size (DataComponents.MAX_STACK_SIZE)Integeryesits own StreamCodec
max_damage (DataComponents.MAX_DAMAGE)Integeryesits own StreamCodec
damage (DataComponents.DAMAGE)Integeryesits own StreamCodec
unbreakable (DataComponents.UNBREAKABLE)Unityesits own StreamCodec
use_effects (DataComponents.USE_EFFECTS)UseEffectsyesits own StreamCodec
custom_name (DataComponents.CUSTOM_NAME)Componentyes (cached)its own StreamCodec
minimum_attack_charge (DataComponents.MINIMUM_ATTACK_CHARGE)Floatyesits own StreamCodec
damage_type (DataComponents.DAMAGE_TYPE)Holder<…>yesits own StreamCodec
item_name (DataComponents.ITEM_NAME)Componentyes (cached)its own StreamCodec
item_model (DataComponents.ITEM_MODEL)Identifieryes (cached)its own StreamCodec
lore (DataComponents.LORE)ItemLoreyes (cached)its own StreamCodec
rarity (DataComponents.RARITY)Rarityyesits own StreamCodec
enchantments (DataComponents.ENCHANTMENTS)ItemEnchantmentsyes (cached)its own StreamCodec
can_place_on (DataComponents.CAN_PLACE_ON)AdventureModePredicateyes (cached)its own StreamCodec
can_break (DataComponents.CAN_BREAK)AdventureModePredicateyes (cached)its own StreamCodec
attribute_modifiers (DataComponents.ATTRIBUTE_MODIFIERS)ItemAttributeModifiersyes (cached)its own StreamCodec
custom_model_data (DataComponents.CUSTOM_MODEL_DATA)CustomModelDatayesits own StreamCodec
tooltip_display (DataComponents.TOOLTIP_DISPLAY)TooltipDisplayyes (cached)its own StreamCodec
repair_cost (DataComponents.REPAIR_COST)Integeryesits own StreamCodec
creative_slot_lock (DataComponents.CREATIVE_SLOT_LOCK)Unitits own StreamCodec
enchantment_glint_override (DataComponents.ENCHANTMENT_GLINT_OVERRIDE)Booleanyesits own StreamCodec
intangible_projectile (DataComponents.INTANGIBLE_PROJECTILE)UnityesNBT, through its Codec
food (DataComponents.FOOD)FoodPropertiesyes (cached)its own StreamCodec
consumable (DataComponents.CONSUMABLE)Consumableyes (cached)its own StreamCodec
use_remainder (DataComponents.USE_REMAINDER)UseRemainderyes (cached)its own StreamCodec
use_cooldown (DataComponents.USE_COOLDOWN)UseCooldownyes (cached)its own StreamCodec
damage_resistant (DataComponents.DAMAGE_RESISTANT)DamageResistantyes (cached)its own StreamCodec
tool (DataComponents.TOOL)Toolyes (cached)its own StreamCodec
weapon (DataComponents.WEAPON)Weaponyes (cached)its own StreamCodec
attack_range (DataComponents.ATTACK_RANGE)AttackRangeyes (cached)its own StreamCodec
enchantable (DataComponents.ENCHANTABLE)Enchantableyes (cached)its own StreamCodec
equippable (DataComponents.EQUIPPABLE)Equippableyes (cached)its own StreamCodec
repairable (DataComponents.REPAIRABLE)Repairableyes (cached)its own StreamCodec
glider (DataComponents.GLIDER)Unityesits own StreamCodec
tooltip_style (DataComponents.TOOLTIP_STYLE)Identifieryes (cached)its own StreamCodec
death_protection (DataComponents.DEATH_PROTECTION)DeathProtectionyes (cached)its own StreamCodec
blocks_attacks (DataComponents.BLOCKS_ATTACKS)BlocksAttacksyes (cached)its own StreamCodec
piercing_weapon (DataComponents.PIERCING_WEAPON)PiercingWeaponyes (cached)its own StreamCodec
kinetic_weapon (DataComponents.KINETIC_WEAPON)KineticWeaponyes (cached)its own StreamCodec
attack_animation (DataComponents.ATTACK_ANIMATION)SwingAnimationyesits own StreamCodec
interact_animation (DataComponents.INTERACT_ANIMATION)SwingAnimationyesits own StreamCodec
additional_trade_cost (DataComponents.ADDITIONAL_TRADE_COST)Integerits own StreamCodec
block_transformer (DataComponents.BLOCK_TRANSFORMER)Holder<…>yes (cached)its own StreamCodec
villager_food (DataComponents.VILLAGER_FOOD)VillagerFoodyesits own StreamCodec
stored_enchantments (DataComponents.STORED_ENCHANTMENTS)ItemEnchantmentsyes (cached)its own StreamCodec
dye (DataComponents.DYE)DyeColoryesits own StreamCodec
dyed_color (DataComponents.DYED_COLOR)DyedItemColoryesits own StreamCodec
map_id (DataComponents.MAP_ID)MapIdyesits own StreamCodec
map_decorations (DataComponents.MAP_DECORATIONS)MapDecorationsyes (cached)NBT, through its Codec
map_post_processing (DataComponents.MAP_POST_PROCESSING)MapPostProcessingits own StreamCodec
charged_projectiles (DataComponents.CHARGED_PROJECTILES)ChargedProjectilesyes (cached)its own StreamCodec
bundle_contents (DataComponents.BUNDLE_CONTENTS)BundleContentsyes (cached)its own StreamCodec
potion_contents (DataComponents.POTION_CONTENTS)PotionContentsyes (cached)its own StreamCodec
potion_duration_scale (DataComponents.POTION_DURATION_SCALE)Floatyes (cached)its own StreamCodec
suspicious_stew_effects (DataComponents.SUSPICIOUS_STEW_EFFECTS)SuspiciousStewEffectsyes (cached)its own StreamCodec
writable_book_content (DataComponents.WRITABLE_BOOK_CONTENT)WritableBookContentyes (cached)its own StreamCodec
written_book_content (DataComponents.WRITTEN_BOOK_CONTENT)WrittenBookContentyes (cached)its own StreamCodec
trim (DataComponents.TRIM)ArmorTrimyes (cached)its own StreamCodec
debug_stick_state (DataComponents.DEBUG_STICK_STATE)DebugStickStateyes (cached)NBT, through its Codec
entity_data (DataComponents.ENTITY_DATA)TypedEntityData<…>yesits own StreamCodec
bucket_entity_data (DataComponents.BUCKET_ENTITY_DATA)CustomDatayesits own StreamCodec
block_entity_data (DataComponents.BLOCK_ENTITY_DATA)TypedEntityData<…>yesits own StreamCodec
instrument (DataComponents.INSTRUMENT)InstrumentComponentyes (cached)its own StreamCodec
provides_trim_material (DataComponents.PROVIDES_TRIM_MATERIAL)Holder<…>yes (cached)its own StreamCodec
ominous_bottle_amplifier (DataComponents.OMINOUS_BOTTLE_AMPLIFIER)OminousBottleAmplifieryesits own StreamCodec
jukebox_playable (DataComponents.JUKEBOX_PLAYABLE)JukeboxPlayableyesits own StreamCodec
provides_banner_patterns (DataComponents.PROVIDES_BANNER_PATTERNS)HolderSet<…>yes (cached)its own StreamCodec
recipes (DataComponents.RECIPES)List<…>yes (cached)NBT, through its Codec
lodestone_tracker (DataComponents.LODESTONE_TRACKER)LodestoneTrackeryes (cached)its own StreamCodec
firework_explosion (DataComponents.FIREWORK_EXPLOSION)FireworkExplosionyes (cached)its own StreamCodec
fireworks (DataComponents.FIREWORKS)Fireworksyes (cached)its own StreamCodec
profile (DataComponents.PROFILE)ResolvableProfileyes (cached)its own StreamCodec
note_block_sound (DataComponents.NOTE_BLOCK_SOUND)Identifieryesits own StreamCodec
banner_patterns (DataComponents.BANNER_PATTERNS)BannerPatternLayersyes (cached)its own StreamCodec
base_color (DataComponents.BASE_COLOR)DyeColoryesits own StreamCodec
pot_decorations (DataComponents.POT_DECORATIONS)PotDecorationsyes (cached)its own StreamCodec
container (DataComponents.CONTAINER)ItemContainerContentsyes (cached)its own StreamCodec
block_state (DataComponents.BLOCK_STATE)BlockItemStatePropertiesyes (cached)its own StreamCodec
bees (DataComponents.BEES)Beesyes (cached)its own StreamCodec
sulfur_cube_content (DataComponents.SULFUR_CUBE_CONTENT)SulfurCubeContentyes (cached)its own StreamCodec
lock (DataComponents.LOCK)LockCodeyesNBT, through its Codec
container_loot (DataComponents.CONTAINER_LOOT)SeededContainerLootyesNBT, through its Codec
break_sound (DataComponents.BREAK_SOUND)Holder<…>yes (cached)its own StreamCodec
compostable (DataComponents.COMPOSTABLE)Compostableyesits own StreamCodec
cooking_fuel (DataComponents.COOKING_FUEL)CookingFuelyesits own StreamCodec
brewing_fuel (DataComponents.BREWING_FUEL)BrewingFuelyesits own StreamCodec
mob_visibility (DataComponents.MOB_VISIBILITY)MobVisibilityyesits own StreamCodec
villager/variant (DataComponents.VILLAGER_VARIANT)Holder<…>yesits own StreamCodec
wolf/variant (DataComponents.WOLF_VARIANT)Holder<…>yesits own StreamCodec
wolf/sound_variant (DataComponents.WOLF_SOUND_VARIANT)Holder<…>yesits own StreamCodec
wolf/collar (DataComponents.WOLF_COLLAR)DyeColoryesits own StreamCodec
fox/variant (DataComponents.FOX_VARIANT)Fox.Variantyesits own StreamCodec
salmon/size (DataComponents.SALMON_SIZE)Salmon.Variantyesits own StreamCodec
parrot/variant (DataComponents.PARROT_VARIANT)Parrot.Variantyesits own StreamCodec
tropical_fish/pattern (DataComponents.TROPICAL_FISH_PATTERN)TropicalFish.Patternyesits own StreamCodec
tropical_fish/base_color (DataComponents.TROPICAL_FISH_BASE_COLOR)DyeColoryesits own StreamCodec
tropical_fish/pattern_color (DataComponents.TROPICAL_FISH_PATTERN_COLOR)DyeColoryesits own StreamCodec
mooshroom/variant (DataComponents.MOOSHROOM_VARIANT)MushroomCow.Variantyesits own StreamCodec
rabbit/variant (DataComponents.RABBIT_VARIANT)Rabbit.Variantyesits own StreamCodec
pig/variant (DataComponents.PIG_VARIANT)Holder<…>yesits own StreamCodec
pig/sound_variant (DataComponents.PIG_SOUND_VARIANT)Holder<…>yesits own StreamCodec
cow/variant (DataComponents.COW_VARIANT)Holder<…>yesits own StreamCodec
cow/sound_variant (DataComponents.COW_SOUND_VARIANT)Holder<…>yesits own StreamCodec
chicken/variant (DataComponents.CHICKEN_VARIANT)Holder<…>yesits own StreamCodec
chicken/sound_variant (DataComponents.CHICKEN_SOUND_VARIANT)Holder<…>yesits own StreamCodec
zombie_nautilus/variant (DataComponents.ZOMBIE_NAUTILUS_VARIANT)Holder<…>yesits own StreamCodec
frog/variant (DataComponents.FROG_VARIANT)Holder<…>yesits own StreamCodec
horse/variant (DataComponents.HORSE_VARIANT)Variantyesits own StreamCodec
painting/variant (DataComponents.PAINTING_VARIANT)Holder<…>yesits own StreamCodec
llama/variant (DataComponents.LLAMA_VARIANT)Llama.Variantyesits own StreamCodec
axolotl/variant (DataComponents.AXOLOTL_VARIANT)Axolotl.Variantyesits own StreamCodec
cat/variant (DataComponents.CAT_VARIANT)Holder<…>yesits own StreamCodec
cat/sound_variant (DataComponents.CAT_SOUND_VARIANT)Holder<…>yesits own StreamCodec
cat/collar (DataComponents.CAT_COLLAR)DyeColoryesits own StreamCodec
sheep/color (DataComponents.SHEEP_COLOR)DyeColoryesits own StreamCodec
shulker/color (DataComponents.SHULKER_COLOR)DyeColoryesits own StreamCodec
provides_pottery_pattern (DataComponents.PROVIDES_POTTERY_PATTERN)Holder<…>yesits own StreamCodec
sign_text_front (DataComponents.SIGN_TEXT_FRONT)SignTextyes (cached)its own StreamCodec
sign_text_back (DataComponents.SIGN_TEXT_BACK)SignTextyes (cached)its own StreamCodec
waxed (DataComponents.WAXED)Unityesits own StreamCodec
cushion/color (DataComponents.CUSHION_COLOR)DyeColoryesits own StreamCodec

Game rules

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every rule declared in GameRules, with its category (GameRuleCategory) and default. Integer rules list the bounds they declare after the default — a minimum always, a maximum only where one is not Integer.MAX_VALUE — and any feature gate. Values live in a GameRuleMap — a SavedData at data/minecraft/game_rules.dat, one set for the whole server rather than one per level. See Level data and rules.

59 rules

ruletypecategorydefault
command_block_output (GameRules.COMMAND_BLOCK_OUTPUT)Booleanchattrue
log_admin_commands (GameRules.LOG_ADMIN_COMMANDS)Booleanchattrue
send_command_feedback (GameRules.SEND_COMMAND_FEEDBACK)Booleanchattrue
show_advancement_messages (GameRules.SHOW_ADVANCEMENT_MESSAGES)Booleanchattrue
show_death_messages (GameRules.SHOW_DEATH_MESSAGES)Booleanchattrue
block_drops (GameRules.BLOCK_DROPS)Booleandropstrue
block_explosion_drop_decay (GameRules.BLOCK_EXPLOSION_DROP_DECAY)Booleandropstrue
entity_drops (GameRules.ENTITY_DROPS)Booleandropstrue
mob_drops (GameRules.MOB_DROPS)Booleandropstrue
mob_explosion_drop_decay (GameRules.MOB_EXPLOSION_DROP_DECAY)Booleandropstrue
projectiles_can_break_blocks (GameRules.PROJECTILES_CAN_BREAK_BLOCKS)Booleandropstrue
tnt_explosion_drop_decay (GameRules.TNT_EXPLOSION_DROP_DECAY)Booleandropsfalse
allow_entering_nether_using_portals (GameRules.ALLOW_ENTERING_NETHER_USING_PORTALS)Booleanmisctrue
command_blocks_work (GameRules.COMMAND_BLOCKS_WORK)Booleanmisctrue
global_sound_events (GameRules.GLOBAL_SOUND_EVENTS)Booleanmisctrue
max_block_modifications (GameRules.MAX_BLOCK_MODIFICATIONS)Integermisc32768 (min 1)
max_command_forks (GameRules.MAX_COMMAND_FORKS)Integermisc65536 (min 0)
max_command_sequence_length (GameRules.MAX_COMMAND_SEQUENCE_LENGTH)Integermisc65536 (min 0)
max_minecart_speed (GameRules.MAX_MINECART_SPEED)Integermisc8 (min 1, max 1000, requires FeatureFlags.MINECART_IMPROVEMENTS)
reduced_debug_info (GameRules.REDUCED_DEBUG_INFO)Booleanmiscfalse
spawner_blocks_work (GameRules.SPAWNER_BLOCKS_WORK)Booleanmisctrue
tnt_explodes (GameRules.TNT_EXPLODES)Booleanmisctrue
forgive_dead_players (GameRules.FORGIVE_DEAD_PLAYERS)Booleanmobstrue
max_entity_cramming (GameRules.MAX_ENTITY_CRAMMING)Integermobs24 (min 0)
mob_griefing (GameRules.MOB_GRIEFING)Booleanmobstrue
raids (GameRules.RAIDS)Booleanmobstrue
universal_anger (GameRules.UNIVERSAL_ANGER)Booleanmobsfalse
drowning_damage (GameRules.DROWNING_DAMAGE)Booleanplayertrue
elytra_movement_check (GameRules.ELYTRA_MOVEMENT_CHECK)Booleanplayertrue
ender_pearls_vanish_on_death (GameRules.ENDER_PEARLS_VANISH_ON_DEATH)Booleanplayertrue
fall_damage (GameRules.FALL_DAMAGE)Booleanplayertrue
fire_damage (GameRules.FIRE_DAMAGE)Booleanplayertrue
freeze_damage (GameRules.FREEZE_DAMAGE)Booleanplayertrue
immediate_respawn (GameRules.IMMEDIATE_RESPAWN)Booleanplayerfalse
keep_inventory (GameRules.KEEP_INVENTORY)Booleanplayerfalse
limited_crafting (GameRules.LIMITED_CRAFTING)Booleanplayerfalse
locator_bar (GameRules.LOCATOR_BAR)Booleanplayertrue
natural_health_regeneration (GameRules.NATURAL_HEALTH_REGENERATION)Booleanplayertrue
player_movement_check (GameRules.PLAYER_MOVEMENT_CHECK)Booleanplayertrue
players_nether_portal_creative_delay (GameRules.PLAYERS_NETHER_PORTAL_CREATIVE_DELAY)Integerplayer0 (min 0)
players_nether_portal_default_delay (GameRules.PLAYERS_NETHER_PORTAL_DEFAULT_DELAY)Integerplayer80 (min 0)
players_sleeping_percentage (GameRules.PLAYERS_SLEEPING_PERCENTAGE)Integerplayer100 (min 0)
pvp (GameRules.PVP)Booleanplayertrue
respawn_radius (GameRules.RESPAWN_RADIUS)Integerplayer10 (min 0)
spectators_generate_chunks (GameRules.SPECTATORS_GENERATE_CHUNKS)Booleanplayertrue
spawn_mobs (GameRules.SPAWN_MOBS)Booleanspawningtrue
spawn_monsters (GameRules.SPAWN_MONSTERS)Booleanspawningtrue
spawn_patrols (GameRules.SPAWN_PATROLS)Booleanspawningtrue
spawn_phantoms (GameRules.SPAWN_PHANTOMS)Booleanspawningtrue
spawn_wandering_traders (GameRules.SPAWN_WANDERING_TRADERS)Booleanspawningtrue
spawn_wardens (GameRules.SPAWN_WARDENS)Booleanspawningtrue
advance_time (GameRules.ADVANCE_TIME)Booleanupdatestrue
advance_weather (GameRules.ADVANCE_WEATHER)Booleanupdatestrue
fire_spread_radius_around_player (GameRules.FIRE_SPREAD_RADIUS_AROUND_PLAYER)Integerupdates128 (min -1)
lava_source_conversion (GameRules.LAVA_SOURCE_CONVERSION)Booleanupdatesfalse
max_snow_accumulation_height (GameRules.MAX_SNOW_ACCUMULATION_HEIGHT)Integerupdates1 (min 0, max 8)
random_tick_speed (GameRules.RANDOM_TICK_SPEED)Integerupdates3 (min 0)
spread_vines (GameRules.SPREAD_VINES)Booleanupdatestrue
water_source_conversion (GameRules.WATER_SOURCE_CONVERSION)Booleanupdatestrue

Attributes

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every attribute registered in Attributes. All of them are RangedAttributes, so every one clamps to its range once, at the end of AttributeInstance.calculateValue. Syncable attributes are the only ones ClientboundUpdateAttributesPacket ever carries: a mutation to one of the others changes the server’s number and never reaches the client at all. The sentiment decides tooltip colour and nothing else. Defaults here are the registry’s, and most entity types override them in their own AttributeSupplier. See Attributes.

40 attributes, 32 syncable and 8 not

idconstantdefaultminmaxsyncablesentiment
air_drag_modifierAttributes.AIR_DRAG_MODIFIER102048yespositive
armorAttributes.ARMOR0030yespositive
armor_toughnessAttributes.ARMOR_TOUGHNESS0020yespositive
attack_damageAttributes.ATTACK_DAMAGE202048positive
attack_knockbackAttributes.ATTACK_KNOCKBACK005positive
attack_speedAttributes.ATTACK_SPEED401024yespositive
below_name_distanceAttributes.BELOW_NAME_DISTANCE100512yespositive
block_break_speedAttributes.BLOCK_BREAK_SPEED101024yespositive
block_interaction_rangeAttributes.BLOCK_INTERACTION_RANGE4.5064yespositive
bouncinessAttributes.BOUNCINESS001yespositive
burning_timeAttributes.BURNING_TIME101024yesnegative
camera_distanceAttributes.CAMERA_DISTANCE4032yespositive
entity_interaction_rangeAttributes.ENTITY_INTERACTION_RANGE3064yespositive
explosion_knockback_resistanceAttributes.EXPLOSION_KNOCKBACK_RESISTANCE001yespositive
fall_damage_multiplierAttributes.FALL_DAMAGE_MULTIPLIER10100yesnegative
flying_speedAttributes.FLYING_SPEED0.401024yespositive
follow_rangeAttributes.FOLLOW_RANGE3202048positive
friction_modifierAttributes.FRICTION_MODIFIER102048yespositive
gravityAttributes.GRAVITY0.08-11yesneutral
jump_strengthAttributes.JUMP_STRENGTH0.42032yespositive
knockback_resistanceAttributes.KNOCKBACK_RESISTANCE0-21positive
luckAttributes.LUCK0-10241024yespositive
max_absorptionAttributes.MAX_ABSORPTION002048yespositive
max_healthAttributes.MAX_HEALTH2011024yespositive
mining_efficiencyAttributes.MINING_EFFICIENCY001024yespositive
movement_efficiencyAttributes.MOVEMENT_EFFICIENCY001yespositive
movement_speedAttributes.MOVEMENT_SPEED0.701024yespositive
name_tag_distanceAttributes.NAME_TAG_DISTANCE640512yespositive
oxygen_bonusAttributes.OXYGEN_BONUS001024yespositive
safe_fall_distanceAttributes.SAFE_FALL_DISTANCE3-10241024yespositive
scaleAttributes.SCALE10.062516yesneutral
sneaking_speedAttributes.SNEAKING_SPEED0.301yespositive
spawn_reinforcementsAttributes.SPAWN_REINFORCEMENTS_CHANCE001positive
step_heightAttributes.STEP_HEIGHT0.6010yespositive
submerged_mining_speedAttributes.SUBMERGED_MINING_SPEED0.2020yespositive
sweeping_damage_ratioAttributes.SWEEPING_DAMAGE_RATIO001yespositive
tempt_rangeAttributes.TEMPT_RANGE1002048positive
water_movement_efficiencyAttributes.WATER_MOVEMENT_EFFICIENCY001yespositive
waypoint_receive_rangeAttributes.WAYPOINT_RECEIVE_RANGE0060000000neutral
waypoint_transmit_rangeAttributes.WAYPOINT_TRANSMIT_RANGE0060000000neutral

Entity data serializers

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every EntityDataSerializer in EntityDataSerializers, in registration order, which is the wire id — EntityDataSerializers.registerSerializer pushes each one into a CrudeIncrementalIntIdentityHashBiMap that hands out the next int. A SynchedEntityData.DataValue on the wire is an unsigned byte accessor id, this var-int, and the encoded value. For value type marks the ones built by EntityDataSerializer.forValueType, the immutable case where EntityDataSerializer.copy is identity. See Synched entity data.

44 serializers, wire ids 0 to 43

idconstantvalue typebuilt by
0EntityDataSerializers.BYTEBytefor value type
1EntityDataSerializers.INTIntegerfor value type
2EntityDataSerializers.LONGLongfor value type
3EntityDataSerializers.FLOATFloatfor value type
4EntityDataSerializers.STRINGStringfor value type
5EntityDataSerializers.COMPONENTComponentfor value type
6EntityDataSerializers.OPTIONAL_COMPONENTOptional<Component>for value type
7EntityDataSerializers.ITEM_STACKItemStackan anonymous subclass
8EntityDataSerializers.BOOLEANBooleanfor value type
9EntityDataSerializers.ROTATIONSRotationsfor value type
10EntityDataSerializers.BLOCK_POSBlockPosfor value type
11EntityDataSerializers.OPTIONAL_BLOCK_POSOptional<BlockPos>for value type
12EntityDataSerializers.DIRECTIONDirectionfor value type
13EntityDataSerializers.OPTIONAL_LIVING_ENTITY_REFERENCEOptional<EntityReference<LivingEntity>>for value type
14EntityDataSerializers.BLOCK_STATEBlockStatefor value type
15EntityDataSerializers.OPTIONAL_BLOCK_STATEOptional<BlockState>for value type
16EntityDataSerializers.PARTICLEParticleOptionsfor value type
17EntityDataSerializers.PARTICLESList<ParticleOptions>for value type
18EntityDataSerializers.VILLAGER_DATAVillagerDatafor value type
19EntityDataSerializers.OPTIONAL_UNSIGNED_INTOptionalIntfor value type
20EntityDataSerializers.POSEPosefor value type
21EntityDataSerializers.CAT_VARIANTHolder<CatVariant>for value type
22EntityDataSerializers.CAT_SOUND_VARIANTHolder<CatSoundVariant>for value type
23EntityDataSerializers.COW_VARIANTHolder<CowVariant>for value type
24EntityDataSerializers.COW_SOUND_VARIANTHolder<CowSoundVariant>for value type
25EntityDataSerializers.WOLF_VARIANTHolder<WolfVariant>for value type
26EntityDataSerializers.WOLF_SOUND_VARIANTHolder<WolfSoundVariant>for value type
27EntityDataSerializers.FROG_VARIANTHolder<FrogVariant>for value type
28EntityDataSerializers.PIG_VARIANTHolder<PigVariant>for value type
29EntityDataSerializers.PIG_SOUND_VARIANTHolder<PigSoundVariant>for value type
30EntityDataSerializers.CHICKEN_VARIANTHolder<ChickenVariant>for value type
31EntityDataSerializers.CHICKEN_SOUND_VARIANTHolder<ChickenSoundVariant>for value type
32EntityDataSerializers.ZOMBIE_NAUTILUS_VARIANTHolder<ZombieNautilusVariant>for value type
33EntityDataSerializers.OPTIONAL_GLOBAL_POSOptional<GlobalPos>for value type
34EntityDataSerializers.PAINTING_VARIANTHolder<PaintingVariant>for value type
35EntityDataSerializers.SNIFFER_STATESniffer.Statefor value type
36EntityDataSerializers.ARMADILLO_STATEArmadillo.ArmadilloStatefor value type
37EntityDataSerializers.COPPER_GOLEM_STATECopperGolemStatefor value type
38EntityDataSerializers.WEATHERING_COPPER_STATEWeatheringCopper.WeatherStatefor value type
39EntityDataSerializers.VECTOR3Vector3fcfor value type
40EntityDataSerializers.QUATERNIONQuaternionfcfor value type
41EntityDataSerializers.RESOLVABLE_PROFILEResolvableProfilefor value type
42EntityDataSerializers.HUMANOID_ARMHumanoidArmfor value type
43EntityDataSerializers.DYE_COLORDyeColorfor value type

Enchantment hooks

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every public entry point of EnchantmentHelper, with the classes that call it. 60 classes call it, so this table is where most of the rest of the game meets the enchantment system: most rows are a moment at which some other system asks whether an enchantment wants to change what happens, and the rest store, look up or choose enchantments. Callers are the declaring files, one per class, excluding EnchantmentHelper itself. See Enchantments for what an enchantment is and Enchanting for the selection half.

50 entry points, 47 of them called from outside the class

entry pointoverloadscalled from
EnchantmentHelper.canStoreEnchantments1AnvilMenu
EnchantmentHelper.createBook1CreativeModeTabs
EnchantmentHelper.doPostAttackEffects1AbstractCubeMob, AbstractWindCharge, Bee, ChargeAttack, EnderDragon, EvokerFangs, HoglinBase, IronGolem, LargeFireball, LivingEntity, LlamaSpit, Mob, Player, RamTarget, ShulkerBullet, SmallFireball, WitherSkull
EnchantmentHelper.doPostAttackEffectsWithItemSource1AbstractArrow, Player
EnchantmentHelper.doPostAttackEffectsWithItemSourceOnBreak1ThrownTrident
EnchantmentHelper.doPostPiercingAttackEffects1LivingEntity
EnchantmentHelper.enchantItem2EnchantWithLevelsFunction
EnchantmentHelper.enchantItemFromProvider1Enderman, Mob, Pillager, SkeletonTrapGoal, Vindicator
EnchantmentHelper.filterCompatibleEnchantments1nothing outside the class
EnchantmentHelper.forEachModifier2ItemStack
EnchantmentHelper.getAvailableEnchantmentResults1nothing outside the class
EnchantmentHelper.getDamageProtection1LivingEntity
EnchantmentHelper.getEnchantmentCost1EnchantmentMenu
EnchantmentHelper.getEnchantmentLevel1EnchantedCountIncreaseFunction, LootItemRandomChanceWithEnchantedBonusCondition
EnchantmentHelper.getEnchantmentsForCrafting1AnvilMenu, EnchantCommand, GrindstoneMenu, RepairItemRecipe
EnchantmentHelper.getFishingLuckBonus1FishingRodItem
EnchantmentHelper.getFishingTimeReduction1FishingRodItem
EnchantmentHelper.getHighestLevel1nothing outside the class
EnchantmentHelper.getItemEnchantmentLevel1ApplyBonusCount, BonusLevelTableCondition
EnchantmentHelper.getPiercingCount1AbstractArrow
EnchantmentHelper.getRandomItemWith1ExperienceOrb
EnchantmentHelper.getTridentReturnToOwnerAcceleration1ThrownTrident
EnchantmentHelper.getTridentSpinAttackStrength1TridentItem
EnchantmentHelper.has1AbstractHorse, Allay, ArmorSlot, ArmorStand, Equippable, Mob, Piglin, Player, ZombieVillager
EnchantmentHelper.hasAnyEnchantments1GrindstoneMenu
EnchantmentHelper.hasTag1BeehiveBlock, DecoratedPotBlock, IceBlock, InfestedBlock
EnchantmentHelper.isEnchantmentCompatible1EnchantCommand
EnchantmentHelper.isImmuneToDamage1LivingEntity
EnchantmentHelper.modifyArmorEffectiveness1CombatRules
EnchantmentHelper.modifyCrossbowChargingTime1CrossbowItem
EnchantmentHelper.modifyDamage1AbstractArrow, LivingEntity, Mob, ServerPlayer, ThrownTrident
EnchantmentHelper.modifyDurabilityToRepairFromXp1ExperienceOrb
EnchantmentHelper.modifyFallBasedDamage1MaceItem
EnchantmentHelper.modifyKnockback1AbstractArrow, LivingEntity
EnchantmentHelper.onHitBlock1AbstractArrow, ServerPlayerGameMode, ThrownTrident
EnchantmentHelper.onProjectileSpawned1Projectile
EnchantmentHelper.pickHighestLevel1CrossbowItem, TridentItem
EnchantmentHelper.processAmmoUse1ProjectileWeaponItem
EnchantmentHelper.processBlockExperience1Block
EnchantmentHelper.processDurabilityChange1ItemStack
EnchantmentHelper.processEquipmentDropChance1Mob
EnchantmentHelper.processMobExperience1LivingEntity
EnchantmentHelper.processProjectileCount1ProjectileWeaponItem
EnchantmentHelper.processProjectileSpread1ProjectileWeaponItem
EnchantmentHelper.runLocationChangedEffects2LivingEntity, ServerPlayer
EnchantmentHelper.selectEnchantment1EnchantmentMenu, EnchantmentsByCost, EnchantmentsByCostWithDifficulty
EnchantmentHelper.setEnchantments1AnvilMenu
EnchantmentHelper.stopLocationBasedEffects2LivingEntity, ServerPlayer
EnchantmentHelper.tickEffects1LivingEntity
EnchantmentHelper.updateEnchantments1GrindstoneMenu, ItemStack, RepairItemRecipe, SetEnchantmentsFunction

Loot context parameter sets

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every ContextKeySet registered in LootContextParamSets, with the keys its ContextKeySet.Builder declared. The set a roll is checked against belongs to the caller: ContextMap.Builder.buildAndValidate throws both on a required key that is absent and on a key the set does not declare at all, so this table is the contract each call site has to satisfy. A loot table names a set of its own, its type, but that one is checked only when the table loads. A loot element reads a key with LootContext.getOptional, which answers null for a key that is not there, or asks only whether it is there, with LootContext.hasParameter. Seventeen of these thirty-one sets never roll a LootTable at all — the engine is wider than the loot package. See Contexts and predicates.

31 parameter sets

setidrequiredoptional
LootContextParamSets.ADVANCEMENT_ENTITYadvancement_entityLootContextParams.THIS_ENTITY, LootContextParams.ORIGIN—
LootContextParamSets.ADVANCEMENT_LOCATIONadvancement_locationLootContextParams.THIS_ENTITY, LootContextParams.ORIGIN, LootContextParams.TOOL, LootContextParams.BLOCK_STATELootContextParams.BLOCK_ENTITY
LootContextParamSets.ADVANCEMENT_REWARDadvancement_rewardLootContextParams.THIS_ENTITY, LootContextParams.ORIGIN—
LootContextParamSets.ARCHAEOLOGYarchaeologyLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY, LootContextParams.TOOL—
LootContextParamSets.PIGLIN_BARTERbarterLootContextParams.THIS_ENTITY—
LootContextParamSets.BLOCKblockLootContextParams.BLOCK_STATE, LootContextParams.ORIGIN, LootContextParams.TOOLLootContextParams.THIS_ENTITY, LootContextParams.BLOCK_ENTITY, LootContextParams.EXPLOSION_RADIUS
LootContextParamSets.BLOCK_INTERACTblock_interactLootContextParams.BLOCK_STATE, LootContextParams.ORIGINLootContextParams.BLOCK_ENTITY, LootContextParams.INTERACTING_ENTITY, LootContextParams.TOOL
LootContextParamSets.BLOCK_USEblock_useLootContextParams.THIS_ENTITY, LootContextParams.ORIGIN, LootContextParams.BLOCK_STATE—
LootContextParamSets.CHESTchestLootContextParams.ORIGINLootContextParams.THIS_ENTITY
LootContextParamSets.COMMANDcommandLootContextParams.ORIGINLootContextParams.THIS_ENTITY
LootContextParamSets.COMMAND_COMPUTE_DEFAULTcommand_compute_defaultLootContextParams.ORIGINLootContextParams.THIS_ENTITY
LootContextParamSets.COMMAND_COMPUTE_ENTITYcommand_compute_entityLootContextParams.ORIGIN, LootContextParams.TARGET_ENTITYLootContextParams.THIS_ENTITY
LootContextParamSets.COMMAND_COMPUTE_POSITIONcommand_compute_positionLootContextParams.ORIGIN, LootContextParams.BLOCK_STATELootContextParams.THIS_ENTITY, LootContextParams.BLOCK_ENTITY
LootContextParamSets.COMMAND_SLOT_SOURCEcommand_slot_sourceLootContextParams.ORIGIN, LootContextParams.CONTAINERLootContextParams.THIS_ENTITY
LootContextParamSets.CONTAINER_PROCESScontainer_processLootContextParams.BLOCK_ENTITY, LootContextParams.BLOCK_STATE, LootContextParams.CONTAINER, LootContextParams.ORIGIN—
LootContextParamSets.EMPTYempty——
LootContextParamSets.ENCHANTED_DAMAGEenchanted_damageLootContextParams.THIS_ENTITY, LootContextParams.ENCHANTMENT_LEVEL, LootContextParams.ORIGIN, LootContextParams.DAMAGE_SOURCELootContextParams.DIRECT_ATTACKING_ENTITY, LootContextParams.ATTACKING_ENTITY
LootContextParamSets.ENCHANTED_ENTITYenchanted_entityLootContextParams.THIS_ENTITY, LootContextParams.ENCHANTMENT_LEVEL, LootContextParams.ORIGIN—
LootContextParamSets.ENCHANTED_ITEMenchanted_itemLootContextParams.TOOL, LootContextParams.ENCHANTMENT_LEVEL—
LootContextParamSets.ENCHANTED_LOCATIONenchanted_locationLootContextParams.THIS_ENTITY, LootContextParams.ENCHANTMENT_LEVEL, LootContextParams.ORIGIN, LootContextParams.ENCHANTMENT_ACTIVE—
LootContextParamSets.ENTITYentityLootContextParams.THIS_ENTITY, LootContextParams.ORIGIN, LootContextParams.DAMAGE_SOURCELootContextParams.ATTACKING_ENTITY, LootContextParams.DIRECT_ATTACKING_ENTITY, LootContextParams.LAST_DAMAGE_PLAYER
LootContextParamSets.ENTITY_INTERACTentity_interactLootContextParams.TARGET_ENTITY, LootContextParams.TOOLLootContextParams.INTERACTING_ENTITY
LootContextParamSets.EQUIPMENTequipmentLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY—
LootContextParamSets.FISHINGfishingLootContextParams.ORIGIN, LootContextParams.TOOLLootContextParams.THIS_ENTITY
LootContextParamSets.ALL_PARAMSgenericLootContextParams.THIS_ENTITY, LootContextParams.LAST_DAMAGE_PLAYER, LootContextParams.DAMAGE_SOURCE, LootContextParams.ATTACKING_ENTITY, LootContextParams.DIRECT_ATTACKING_ENTITY, LootContextParams.ORIGIN, LootContextParams.BLOCK_STATE, LootContextParams.BLOCK_ENTITY, LootContextParams.TOOL, LootContextParams.EXPLOSION_RADIUS, LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED, LootContextParams.CONTAINER, LootContextParams.INTERACTING_ENTITY, LootContextParams.TARGET_ENTITY, LootContextParams.ENCHANTMENT_ACTIVE, LootContextParams.ENCHANTMENT_LEVEL—
LootContextParamSets.GIFTgiftLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY—
LootContextParamSets.HIT_BLOCKhit_blockLootContextParams.THIS_ENTITY, LootContextParams.ENCHANTMENT_LEVEL, LootContextParams.ORIGIN, LootContextParams.BLOCK_STATE—
LootContextParamSets.SELECTORselectorLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY—
LootContextParamSets.SHEARINGshearingLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY, LootContextParams.TOOL—
LootContextParamSets.VAULTvaultLootContextParams.ORIGINLootContextParams.THIS_ENTITY, LootContextParams.TOOL
LootContextParamSets.VILLAGER_TRADEvillager_tradeLootContextParams.ORIGIN, LootContextParams.THIS_ENTITY, LootContextParams.ADDITIONAL_COST_COMPONENT_ALLOWED—

Entity spawn reasons

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Every EntitySpawnReason constant, in declaration order, with what each one gates — the classes that compare against it and so behave differently for that reason — and how many classes pass it. A reason with an empty gates column changes nothing by itself: it is a label the spawn path carries for other code to read. EntitySpawnReason.isSpawner folds SPAWNER and TRIAL_SPAWNER together and EntitySpawnReason.ignoresLightRequirements is true of TRIAL_SPAWNER alone. See entity lifecycle.

19 reasons · 13 of them tested somewhere · 41 test sites

#reasonwhat it gatesclasses that pass it
0EntitySpawnReason.NATURALZombieHorse.finalizeSpawn; Drowned.finalizeSpawn; Husk.finalizeSpawn; Raider.finalizeSpawn4
1EntitySpawnReason.CHUNK_GENERATIONnothing tests it2
2EntitySpawnReason.SPAWNERMob.checkMobSpawnRules (through EntitySpawnReason.isSpawner); SkeletonHorse.checkSkeletonHorseSpawnRules (through EntitySpawnReason.isSpawner); Endermite.checkEndermiteSpawnRules (through EntitySpawnReason.isSpawner); Guardian.checkGuardianSpawnRules (through EntitySpawnReason.isSpawner); Monster.checkSurfaceMonstersSpawnRules (through EntitySpawnReason.isSpawner); Silverfish.checkSilverfishSpawnRules (through EntitySpawnReason.isSpawner); Slime.checkSlimeSpawnRules (through EntitySpawnReason.isSpawner); Stray.checkStraySpawnRules (through EntitySpawnReason.isSpawner); Drowned.checkDrownedSpawnRules (through EntitySpawnReason.isSpawner)2
3EntitySpawnReason.STRUCTUREPatrollingMonster.finalizeSpawn; Piglin.finalizeSpawn; Drowned.finalizeSpawn9
4EntitySpawnReason.BREEDINGVillager.finalizeSpawn41
5EntitySpawnReason.MOB_SUMMONEDnothing tests it2
6EntitySpawnReason.JOCKEYnothing tests it5
7EntitySpawnReason.EVENTTraderLlama.finalizeSpawn; PatrollingMonster.finalizeSpawn5
8EntitySpawnReason.CONVERSIONHusk.finalizeSpawn; Zombie.finalizeSpawn; Zombie.handleAttributes5
9EntitySpawnReason.REINFORCEMENTDrowned.checkDrownedSpawnRules1
10EntitySpawnReason.TRIGGEREDWarden.finalizeSpawn12
11EntitySpawnReason.BUCKETAxolotl.finalizeSpawn1
12EntitySpawnReason.SPAWN_ITEM_USEnothing tests it4
13EntitySpawnReason.COMMANDnothing tests it2
14EntitySpawnReason.DISPENSERnothing tests it5
15EntitySpawnReason.PATROLPatrollingMonster.finalizeSpawn1
16EntitySpawnReason.TRIAL_SPAWNERMob.checkMobSpawnRules (through EntitySpawnReason.isSpawner); Animal.checkAnimalSpawnRules (through EntitySpawnReason.ignoresLightRequirements); SkeletonHorse.checkSkeletonHorseSpawnRules (through EntitySpawnReason.isSpawner); SkeletonHorse.checkSkeletonHorseSpawnRules (through EntitySpawnReason.ignoresLightRequirements); Endermite.checkEndermiteSpawnRules (through EntitySpawnReason.isSpawner); Guardian.checkGuardianSpawnRules (through EntitySpawnReason.isSpawner); Monster.checkMonsterSpawnRules (through EntitySpawnReason.ignoresLightRequirements); Monster.checkSurfaceMonstersSpawnRules (through EntitySpawnReason.isSpawner); Silverfish.checkSilverfishSpawnRules (through EntitySpawnReason.isSpawner); Slime.checkSlimeSpawnRules (through EntitySpawnReason.isSpawner); Stray.checkStraySpawnRules (through EntitySpawnReason.isSpawner); Drowned.checkDrownedSpawnRules (through EntitySpawnReason.isSpawner); Drowned.checkDrownedSpawnRules (through EntitySpawnReason.ignoresLightRequirements)2
17EntitySpawnReason.LOADZombie.handleAttributes6
18EntitySpawnReason.DIMENSION_TRAVELZombie.handleAttributes1

Structure spawn overrides

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

Which structures replace a biome’s spawn list, for which mob category, and with what. A structure JSON’s spawn_overrides map is read into Structure.spawnOverrides; when NaturalSpawner builds a spawn list, ChunkGenerator.getMobsAt walks the structures that chunk references, in no fixed order, and the first that declares an override for the category and whose box contains the position answers; the biome’s list is then not consulted. An override with an empty spawn list is therefore a ban, not a no-op. The one exception is the creatures a chunk gets while it generates: NaturalSpawner.spawnMobsForChunkGeneration draws the creature category from the biome’s list and reads no override. box is piece (only inside a piece’s own bounding box) or full (anywhere in the structure’s box, which for a structure that adapts the terrain around it is grown by twelve blocks on every side). The mechanism is on entity lifecycle; the nether fortress has a second, hard-coded list in front of this one.

52 structures carry the field · 6 declare an override · 23 overrides in all

structurecategoryboxwhat spawns instead (weight, min–max)
ancient_cityambientfullnothing — the category is suppressed inside the box
ancient_cityaxolotlsfullnothing — the category is suppressed inside the box
ancient_citycreaturefullnothing — the category is suppressed inside the box
ancient_citymiscfullnothing — the category is suppressed inside the box
ancient_citymonsterfullnothing — the category is suppressed inside the box
ancient_cityunderground_water_creaturefullnothing — the category is suppressed inside the box
ancient_citywater_ambientfullnothing — the category is suppressed inside the box
ancient_citywater_creaturefullnothing — the category is suppressed inside the box
fortressmonsterpieceblaze (10, 2–3), zombified_piglin (5, 4–4), wither_skeleton (8, 5–5), skeleton (2, 5–5), magma_cube (3, 4–4)
monumentaxolotlsfullnothing — the category is suppressed inside the box
monumentmonsterfullguardian (1, 2–4)
monumentunderground_water_creaturefullnothing — the category is suppressed inside the box
pillager_outpostmonsterfullpillager (1, 1–1)
swamp_hutcreaturepiececat (1, 1–1)
swamp_hutmonsterpiecewitch (1, 1–1)
trial_chambersambientpiecenothing — the category is suppressed inside the box
trial_chambersaxolotlspiecenothing — the category is suppressed inside the box
trial_chamberscreaturepiecenothing — the category is suppressed inside the box
trial_chambersmiscpiecenothing — the category is suppressed inside the box
trial_chambersmonsterpiecenothing — the category is suppressed inside the box
trial_chambersunderground_water_creaturepiecenothing — the category is suppressed inside the box
trial_chamberswater_ambientpiecenothing — the category is suppressed inside the box
trial_chamberswater_creaturepiecenothing — the category is suppressed inside the box

The other 46 structures carry the field empty — the field is required — and the biome’s list stands.

The weapon helpers on Item.Properties

Generated from the 26.3 decompile by tools/gen_reference.py. Do not edit by hand.

The seven Item.Properties methods that turn a bare item into something you can hit with, what each one installs, and every item built by one. The components column names what a helper sets by name; through Item.Properties.durability, Item.Properties.repairable, Item.Properties.enchantable and Item.Properties.attributes every one also sets the durability components and a stack size of one, and the repair, enchantability and attribute-modifier components. Item.Properties.tool is the shared body: pickaxe, axe, hoe and shovel are it with a mining tag and a shield-disable time filled in; sword and spear go their own way. None of the six needs a class of its own: axe, hoe and shovel add DataComponents.BLOCK_TRANSFORMER, which the base Item.useOn runs on a right-click, so every item a helper builds is a plain Item. See items and stacks and data components.

helperdelegates tocomponents it setsattributes
Item.Properties.toolapplyToolPropertiesDataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.pickaxetoolDataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.axetoolDataComponents.BLOCK_TRANSFORMER, DataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.hoetoolDataComponents.BLOCK_TRANSFORMER, DataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.shoveltoolDataComponents.BLOCK_TRANSFORMER, DataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.swordapplySwordPropertiesDataComponents.TOOL, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED (through ToolMaterial)
Item.Properties.spear—DataComponents.DAMAGE_TYPE, DataComponents.KINETIC_WEAPON, DataComponents.PIERCING_WEAPON, DataComponents.ATTACK_RANGE, DataComponents.MINIMUM_ATTACK_CHARGE, DataComponents.ATTACK_ANIMATION, DataComponents.USE_EFFECTS, DataComponents.WEAPONAttributes.ATTACK_DAMAGE, Attributes.ATTACK_SPEED

The 42 items built by one

damage and speed are the two baselines a tool or sword call passes; the material adds its own attack-damage bonus on top. A spear’s call passes an attack duration and a damage multiplier instead, so its row leaves both cells empty.

itemhelpermaterialdamage baselinespeed baseline
Items.WOODEN_SWORDItem.Properties.swordToolMaterial.WOOD3.0-2.4
Items.WOODEN_SHOVELItem.Properties.shovelToolMaterial.WOOD1.5-3.0
Items.WOODEN_PICKAXEItem.Properties.pickaxeToolMaterial.WOOD1.0-2.8
Items.WOODEN_AXEItem.Properties.axeToolMaterial.WOOD6.0-3.2
Items.WOODEN_HOEItem.Properties.hoeToolMaterial.WOOD0.0-3.0
Items.COPPER_SWORDItem.Properties.swordToolMaterial.COPPER3.0-2.4
Items.COPPER_SHOVELItem.Properties.shovelToolMaterial.COPPER1.5-3.0
Items.COPPER_PICKAXEItem.Properties.pickaxeToolMaterial.COPPER1.0-2.8
Items.COPPER_AXEItem.Properties.axeToolMaterial.COPPER7.0-3.2
Items.COPPER_HOEItem.Properties.hoeToolMaterial.COPPER-1.0-2.0
Items.STONE_SWORDItem.Properties.swordToolMaterial.STONE3.0-2.4
Items.STONE_SHOVELItem.Properties.shovelToolMaterial.STONE1.5-3.0
Items.STONE_PICKAXEItem.Properties.pickaxeToolMaterial.STONE1.0-2.8
Items.STONE_AXEItem.Properties.axeToolMaterial.STONE7.0-3.2
Items.STONE_HOEItem.Properties.hoeToolMaterial.STONE-1.0-2.0
Items.GOLDEN_SWORDItem.Properties.swordToolMaterial.GOLD3.0-2.4
Items.GOLDEN_SHOVELItem.Properties.shovelToolMaterial.GOLD1.5-3.0
Items.GOLDEN_PICKAXEItem.Properties.pickaxeToolMaterial.GOLD1.0-2.8
Items.GOLDEN_AXEItem.Properties.axeToolMaterial.GOLD6.0-3.0
Items.GOLDEN_HOEItem.Properties.hoeToolMaterial.GOLD0.0-3.0
Items.IRON_SWORDItem.Properties.swordToolMaterial.IRON3.0-2.4
Items.IRON_SHOVELItem.Properties.shovelToolMaterial.IRON1.5-3.0
Items.IRON_PICKAXEItem.Properties.pickaxeToolMaterial.IRON1.0-2.8
Items.IRON_AXEItem.Properties.axeToolMaterial.IRON6.0-3.1
Items.IRON_HOEItem.Properties.hoeToolMaterial.IRON-2.0-1.0
Items.DIAMOND_SWORDItem.Properties.swordToolMaterial.DIAMOND3.0-2.4
Items.DIAMOND_SHOVELItem.Properties.shovelToolMaterial.DIAMOND1.5-3.0
Items.DIAMOND_PICKAXEItem.Properties.pickaxeToolMaterial.DIAMOND1.0-2.8
Items.DIAMOND_AXEItem.Properties.axeToolMaterial.DIAMOND5.0-3.0
Items.DIAMOND_HOEItem.Properties.hoeToolMaterial.DIAMOND-3.00.0
Items.NETHERITE_SWORDItem.Properties.swordToolMaterial.NETHERITE3.0-2.4
Items.NETHERITE_SHOVELItem.Properties.shovelToolMaterial.NETHERITE1.5-3.0
Items.NETHERITE_PICKAXEItem.Properties.pickaxeToolMaterial.NETHERITE1.0-2.8
Items.NETHERITE_AXEItem.Properties.axeToolMaterial.NETHERITE5.0-3.0
Items.NETHERITE_HOEItem.Properties.hoeToolMaterial.NETHERITE-4.00.0
Items.WOODEN_SPEARItem.Properties.spearToolMaterial.WOOD——
Items.STONE_SPEARItem.Properties.spearToolMaterial.STONE——
Items.COPPER_SPEARItem.Properties.spearToolMaterial.COPPER——
Items.IRON_SPEARItem.Properties.spearToolMaterial.IRON——
Items.GOLDEN_SPEARItem.Properties.spearToolMaterial.GOLD——
Items.DIAMOND_SPEARItem.Properties.spearToolMaterial.DIAMOND——
Items.NETHERITE_SPEARItem.Properties.spearToolMaterial.NETHERITE——

Block update flags

Verified against Minecraft 26.3 · Reference · Hand-kept from Block’s UPDATE_ constants: the ten bits of Level.setBlock’s flag word, what reads each, and the named combinations.

The flag word is Level.setBlock’s third argument — the last one on the three-argument overload, and followed by an update limit on the four-argument one. It is a bit set, tagged in signatures by Block.UpdateFlags, an annotation that carries no values of its own. Blocks and states draws Level.setBlock as two flowcharts and a table whose gates name these bits by number; this is the table behind the numbers, and every page of Parts IV and V that passes a flag word means the same bits by them. Two of the constants do not mean what they look like: Block.UPDATE_NONE is not zero — it is 260, two bits set — and 512 names two different things, a bit and a recursion budget that is not a bit at all.

bitconstantwhat reads it
1Block.UPDATE_NEIGHBORSthe neighbour fan-out in the tail, and the gate on BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval in the chunk write
2Block.UPDATE_CLIENTSLevel.sendBlockUpdated — on the server the broadcast, the path-type cache and, when the collision shape changed, the re-path check on every navigating mob; a re-mesh on the client, where it is LevelExtractor.blockChanged
4Block.UPDATE_INVISIBLEthe client half of that same gate, and nothing else: Level.setBlock tests it only on the client, so a server write carrying it with bit 2 still broadcasts
8Block.UPDATE_IMMEDIATEone place in the game: LevelExtractor.blockChanged, which marks the re-mesh as player-caused
16Block.UPDATE_KNOWN_SHAPEthree readers: Level.setBlock, where it suppresses all three shape passes; BlockInput.place, where it decides whether the state is fixed up against its neighbours before the write; and WorldGenRegion.setBlock, where it suppresses the post-processing mark
32Block.UPDATE_SUPPRESS_DROPSBlock.updateOrDestroy, whose destroy branch drops resources unless it is set. A one-level flag: it is masked out of the word as it propagates, both to the neighbours and into the recursive write — and no caller hands Block.updateOrDestroy a word with it set, so in this release it changes nothing
64Block.UPDATE_MOVE_BY_PISTONpassed on as movedByPiston, and lets BlockBehaviour.BlockStateBase.affectNeighborsAfterRemoval run without bit 1
128Block.UPDATE_SKIP_SHAPE_UPDATE_ON_WIRENeighborUpdater.executeShapeUpdate, which then skips any shape update whose target is dust, whatever the source. Only the experimental wire evaluator sets it (signal and dust)
256Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTSsuppresses BlockEntity.preRemoveSideEffects
512Block.UPDATE_SKIP_ON_PLACEsuppresses BlockBehaviour.BlockStateBase.onPlace

Four named combinations stand beside the bits, decomposed here rather than in the reader’s head, and only the first two are ever named by a page in this book.

constantvaluethe bitswhere the book meets it
Block.UPDATE_ALL31 + 2the common write — every page that says flags 3 means this
Block.UPDATE_ALL_IMMEDIATE111 + 2 + 8placement, your client’s predicted break, and an emptied bucket
Block.UPDATE_NONE2604 + 256nowhere in this book by name — and none is a misnomer: a write with it tells no client, sends no neighbour update and skips the block entity’s side effects, but still runs BlockBehaviour.BlockStateBase.onPlace and the three shape passes, which reach the neighbours all the same
Block.UPDATE_SKIP_ALL_SIDEEFFECTS81616 + 32 + 256 + 512nowhere in this book; it is the strict mode of /setblock, /fill, /clone and /place and of a structure block, and what /clone’s temporary barriers, a game test’s template and a tree clearing its saplings pass — each with bit 2 added, so the clients are still told; the one write that carries it without bit 2 is the barrier StructureTemplate.placeInWorld sets at 820 where a block with data is about to land, and overwrites at once

512 is two constants and means two unrelated things. As a bit it is Block.UPDATE_SKIP_ON_PLACE. As Block.UPDATE_LIMIT it is not a bit at all: it is the default value of Level.setBlock’s fourth argument, the recursion budget for the shape cascade, counting recursion depth rather than requests — which is what keeps it distinct from CollectingNeighborUpdater.maxChainedNeighborUpdates (block interaction), which counts requests.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Damage outside LivingEntity

Verified against Minecraft 26.3 · Reference · Hand-kept from net/minecraft/world/entity/**.

Entity.hurtServer is abstract, so every branch has to answer for itself. The twenty-two classes are every non-LivingEntity class that declares it, and none of them runs LivingEntity’s rules itself — no armour, no hurt cooldown, no CombatTracker, no death sequence. The lecture that frames them is damage and death; this is the per-class table.

The last column is the client half. Entity.hurtClient has a default — it returns false — and thirteen of the twenty-two inherit it unchanged; eight declare their own and MinecartTNT inherits VehicleEntity’s. It is never handed the damage amount — Entity.hurtOrSimulate drops it on the client — and it answers whether the client should play a hit’s effects itself, for its own swing and for a projectile hit it simulates.

A swing reaches about half of these. The client picks only an entity whose Entity.isPickable is true, which among the twenty-two is Interaction, PrimedTnt, FallingBlockEntity, the three projectiles a player can deflect, the attached blocks, EndCrystal, ShulkerBullet, EnderDragonPart and the vehicles. Two gates follow in Player.attack, which the client runs as it sends the attack and the server runs again — Player.cannotAttack and Player.deflectProjectile (the sword swing owns both). The first refuses a class whose Entity.isAttackable is false (EyeOfEnder, FallingBlockEntity, ExperienceOrb, ItemEntity, and in the projectile row a firework, every arrow and a thrown trident — though the server disconnects a player whose attack names an item, an orb or an arrow before Player.attack runs) or whose Entity.skipAttackInteraction answers true (Interaction while its response flag is off; BlockAttachedEntity answers true outright for a player who may not interact at its position, and otherwise by running its own hurt with no damage). The second deflects those three projectiles, except a player’s WindCharge in its first five ticks, which refuses. Three rows below name their gate in the second column; PrimedTnt is the one nothing row a swing always reaches, and Interaction with its response flag on and a wind charge in its first five ticks reach theirs too; each Entity.hurtServer then does nothing. Entity.hurtOrSimulate is what Player.attack calls after the gates, and it picks Entity.hurtServer or Entity.hurtClient off the level.

classwhat it checks firstwhat it doesreturnsEntity.hurtClient
AreaEffectCloud—nothingfalsefalse
Display—nothingfalsefalse
Interactionnot reached while its response flag is off — Entity.skipAttackInteraction answers the flag’s negation, after recording the attackernothingfalsefalse
LightningBolt—nothingfalsefalse
Marker—nothingfalsefalse
OminousItemSpawner—nothingfalsefalse
PrimedTnt—nothing: a lit TNT block cannot be shot out of the airfalsefalse
EvokerFangs—nothingfalsefalse
EyeOfEndernever reached — Entity.isAttackable returns falsenothingfalsefalse
AbstractHurtingProjectilenot reached but by a player’s wind charge in its first five ticks — a large fireball or a wind charge is otherwise deflected, and the rest are unhittable by Entity.isPickablenothingfalsefalse
ProjectileEntity.isInvulnerableToBaseEntity.markHurt only, so the client is sent the entity’s motion again and nothing changesfalsefalse
FallingBlockEntityEntity.isInvulnerableToBaseEntity.markHurt onlyfalsefalse
ExperienceOrbEntity.isInvulnerableToBasesubtracts the damage from an int of health, Entity.discard at zerotruenot invulnerable
ItemEntityEntity.isInvulnerableToBase, then a Mob source under GameRules.MOB_GRIEFING, then ItemStack.canBeHurtBysame int of health, plus GameEvent.ENTITY_DAMAGE, and ItemStack.onDestroyed before the discardtruenot invulnerable, and ItemStack.canBeHurtBy agrees
BlockAttachedEntityEntity.isInvulnerableToBase, then a Mob source under GameRules.MOB_GRIEFINGBlockAttachedEntity.kill, Entity.markHurt, and BlockAttachedEntity.dropItem — one hit, whatever the amount; a leash knot’s drop is only a soundtruenot invulnerable
Cushiona Player source that may not build, or may not interact at the cushion’s position, is refusedotherwise BlockAttachedEntity’s one hittrue, or false when refused or under BlockAttachedEntity’s own gatesfalse for a player who may not build, then BlockAttachedEntity’s
ItemFrameItemFrame.fixed gates everything: a fixed frame is hurt only by DamageTypeTags.BYPASSES_INVULNERABILITY or a creative playera non-explosion hit on a frame holding something pops the item and stops there; otherwise it falls through to BlockAttachedEntity and the frame breakstruethe fixed gate first, then not invulnerable
EndCrystalEntity.isInvulnerableToBase, then is the source an EnderDragonremoves itself with Entity.RemovalReason.KILLED and explodes with power 6 — unless the source was already an explosion — then EndCrystal.onDestroyedBytruenot invulnerable, and not the dragon
ShulkerBullet—plays SoundEvents.SHULKER_BULLET_HURT, spawns fifteen ParticleTypes.CRIT, destroys itselftruetrue, unconditionally
EnderDragonPartEntity.isInvulnerableToBaseforwards the whole call to EnderDragon.hurt with itself as the part that was hitthe parent’s answerfalse
VehicleEntityalready removed, then Entity.isInvulnerableToBaseVehicleEntity.setDamage adds damage × 10, after flipping the hurt direction and setting ten ticks of hurt time; past 40 it is destroyed. A creative player gets all of that too, and then the vehicle is discarded at once whatever the accumulator reads — Entity.discard, which drops nothingtruetrue, unconditionally
MinecartTNTa burning AbstractArrow as the direct entity explodes it, scaled by the arrow’s speedon that path, nothing else when GameRules.TNT_EXPLODES is on or the cart is primed: the explosion calls Entity.discard, so VehicleEntity.hurtServer returns at its already-removed test. Any other source runs VehicleEntity.hurtServer, except that a fire or explosion source or a burning projectile goes straight to MinecartTNT.destroy, which lights the fuse, even for a creative playertrueinherited: true

Six patterns account for all of it: nothing happens (ten classes), a motion resend and nothing else (two), an int of health with no armour and no hurt cooldown (two), one hit destroys (five, though a non-explosion hit on an unfixed frame holding something takes only the item), an accumulator (two), and forward it to something else (one, the dragon part). Only four classes read the damage amount at all: the pair with an int of health and the accumulator pair. EnderDragonPart passes the number on without looking at it, and everything else is a yes-or-no.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

What the HUD draws, and when

Verified against Minecraft 26.3 · Reference · Hand-kept from Hud.extractRenderState and Gui.extractRenderState.

The HUD is one ordered sweep down two methods — the rows numbered 1 to 27 in Hud.extractRenderState and then four more in Gui.extractRenderState, which is the split the second table below is about — and almost every element in it is behind a condition. Four places in it record one element or another: the spyglass or the equipment overlay (rows 2 and 3), the portal or the nausea overlay (5 and 6), the hotbar or a spectator’s (8), and the item name or a spectator’s action (17 and 17b); food and the mount’s health (11 and 13) exclude each other too, though each is decided in its own method. The contextual bar is the exception, recorded unconditionally and made to draw nothing by an empty state object instead. The lecture that frames this is the HUD; this is the table it is built on, in record order — which is also the order things appear in front of each other, since the GUI render tree infers layering from call order and bounding boxes.

Three gates sit above everything in the first table and none of them belongs to Hud — resources loaded, the frame advancing game time, a level existing; the HUD is where they are explained. Below them Hud.extractRenderState short-circuits entirely while a LevelLoadingScreen is up, publishing GuiRenderState.isHudHidden before it does.

Hidden by F1 means Hud.isHidden, which Options.keyToggleGui — F1 — flips. It is the fourth column in the table below and the last in the second one.

#elementrecorded byhidden by F1?its own condition
1vignetteHud.extractVignetteyesOptions.vignette is on
2spyglass overlayHud.extractSpyglassOverlayyesfirst-person camera and the player is scoping
3equipment camera overlayHud.extractTextureOverlayyesfirst-person, not scoping, and some equipped item’s Equippable declares a camera overlay for the slot it is in
4powder-snow outlineHud.extractTextureOverlayyesEntity.getTicksFrozen above zero
5portal overlayHud.extractPortalOverlayyesthe interpolated portal intensity is above zero
6nausea overlayHud.extractConfusionOverlayyeselse: no portal effect, a nausea blend above zero, and Options.screenEffectScale below one
7crosshairHud.extractCrosshairyesfirst-person; either not a spectator or a hit result a spectator may see; and the F3 three-dimensional-crosshair entry is off
—new stratumGuiGraphicsExtractor.nextStratum—a hard layering barrier — everything below is above everything above. Hud calls it ten times in all; this is the one that separates the overlays from the rest, and there is another inside the crosshair
8hotbarHud.extractItemHotbaryesa camera player exists — replaced in spectator mode by SpectatorGui.extractHotbar, which records only while a spectator menu is open
9armourHud.extractArmoryesinside the health block, and LivingEntity.getArmorValue above zero
10heartsHud.extractHeartsyesinside the health block
11foodHud.extractFoodyesinside the health block, and the vehicle contributes no hearts
12air bubblesHud.extractAirBubblesyesinside the health block, and the player’s eyes are in water or the air supply is below its maximum
13mount healthHud.extractVehicleHealthyesa ridden LivingEntity with a non-zero heart count — outside the health block, so creative shows it
14contextual bar, backgroundContextualBar.extractBackgroundyesalways recorded, but which of four states it is in is re-decided every frame by Hud.nextContextualInfoState
15experience levelContextualBar.extractExperienceLevelyesthe game mode has experience and the level is above zero — recorded between the bar’s two passes, so it survives whichever bar wins
16contextual bar, foregroundContextualBar.extractRenderStateyesalways recorded; empty in ContextualBar.EMPTY, ExperienceBar and JumpableVehicleBar, so LocatorBar is the only one of the four states that draws anything here
17selected item nameHud.extractSelectedItemNameyesnot a spectator, Hud.toolHighlightTimer above zero, and the stack is not empty
17bspectator action nameSpectatorGui.extractActionyeselse: the game mode is spectator and the player is one, and a spectator menu is open — the same slot as row 17, and the second of the two places SpectatorGui replaces a Hud element
18status effectsHud.extractEffectsyesthe player has effects, no screen is showing them itself, and the instance sets MobEffectInstance.showIcon
19boss barsBossHealthOverlayyesthe overlay has events
20sleep fadeHud.extractSleepOverlaynoPlayer.getSleepTimer above zero — the one element between the two hidden-gated blocks
21demo textHud.extractDemoOverlayyesMinecraft.isDemo
22scoreboard sidebarHud.displayScoreboardSidebaryesa display objective for the team’s colour slot, else for DisplaySlot.SIDEBAR
23action barHud.extractOverlayMessageyesHud.overlayMessageString is set and its timer has not run out
24title and subtitleHud.extractTitleyesHud.title is set and Hud.titleTime is above zero
25chatChatComponent.extractRenderStateyesa player exists and the chat screen is not focused
26tab listPlayerTabOverlay.extractRenderStateyesOptions.keyPlayerList is down, and either this is not a local server, or more than one player is listed, or a DisplaySlot.LIST objective exists
27subtitlesSubtitleOverlayyes, with one exceptionOptions.showSubtitles is on, and a subtitle-carrying sound has played within its own range — a distance test, not a volume one, so a category muted to silence still puts its subtitle on screen. Deferred when there is no screen or the screen declares itself in-game UI, and recorded even while hidden if such a screen is up — which is the exception

The four elements a reader expects at the end of that list are not on Hud.extractRenderState’s list at all: Gui.extractRenderState records them instead, in the order below, with the overlay or the screen going in between. Three of the four are still Hud methods called from the outer one — Hud.extractSavingIndicator, Hud.extractDebugOverlay and Hud.extractDeferredSubtitles — and only the toasts, which are ToastManager.extractRenderState’s, are outside Hud entirely. The row with no number is the overlay-or-screen slot rather than an element:

#elementits own conditionhidden by F1?
—the overlay, or else the screenGui.overlay if there is one — the loading overlay is the one you will have seen; otherwise, and only once resources are loaded, Gui.screen through Screen.extractRenderStateWithTooltipAndSubtitles. Gui never records both, though LoadingOverlay itself records the screen beneath its fill while it fades in and out; this is the whole of what a screen contributes to the record—
28saving indicatorOptions.showAutosaveIndicator is on, the frame is drawing a level, and a save is still animatingno
29toastsresources are loadedchecks the flag itself
30debug overlayresources are loaded, the current screen is not DebugOptionsScreen, and some debug entry is on — one set to show always, or, while F3 is open, one set to show in the overlaychecks the flag itself
31deferred subtitlesrow 27 deferred them — but only when neither a screen nor the loading overlay is up: a screen draws them itself from Screen.extractBackground, below its own widgets, LoadingOverlay draws them itself, and this call then finds nothing left—

Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Submit phases and feature renderers

Verified against Minecraft 26.3 · Reference · Hand-kept from SubmitNodeCollection and the FeatureRenderDispatcher constructor.

Everything an entity, block entity, particle or debug renderer draws in a level arrives as a submit node — the sky is drawn by its own renderer in its own frame-graph pass; the clouds, the weather and the world border by their own renderers inside the main pass; and terrain by its chunk sections. SubmitNodeCollection sorts those nodes into fifteen named phases as they come in, and twelve feature renderers turn them into vertices. The lecture that frames both is entity rendering, which names four of the phases and one of the renderers. Two more renderers are named elsewhere — TextFeatureRenderer on text and fonts, QuadParticleFeatureRenderer on particles — and everything else here is only here.

The fifteen phases

One SubmitNodeCollection holds fourteen of them, and a SubmitNodeStorage holds one collection per order bucket — the global draw order a submit chooses with SubmitNodeCollector.order — and the fifteenth, seeThrough, which every one of its collections shares. So “every order bucket” below means every one of those collections in turn.

Those fifteen are the set with improved transparency off, which is how every storage but LevelRenderer’s always runs; the level’s follows GameRenderer.useImprovedTransparency each frame, through SubmitNodeStorage.setUseImprovedTransparency. With it on, rows 2 to 10 and row 12 are ten names for one simple phase, SubmitNodeCollection.oitTranslucent, which FeatureRenderDispatcher.PreparedFrame.executeOit drains three times, once per OitStage. Row 11 is drained on its own, by FeatureRenderDispatcher.PreparedFrame.executeWaterMask — twice a frame when there is a water mask.

Rows 1 to 14 are in the order of SubmitNodeCollection.allPhases, and row 15 comes last because SubmitNodeStorage.drainPhases hands it over after every bucket’s. That is not the order they are drawn in, and neither is the last column: FeatureRenderDispatcher.PreparedFrame.executeTranslucent alone makes three separate sweeps over every order bucket, so the phases it drains are numbered by sweep — but within a sweep the order is the one the sweep’s own statements run in, not the order of the rows. Sweep 1 drains shadows, then translucent models, then name tags, then texts, then translucent custom geometry — so in each bucket translucent models, row 7, are drawn before the name tags and texts of rows 3 and 4.

Three of the fifteen are a TranslucentFeatureRenderPhase — rows 6, 7 and 15, translucentBlocksAndItems, translucentModels and seeThrough — and the other twelve a SimpleFeatureRenderPhase, which is not what two of the four translucent names suggest. What lets those three sort at all is on the node rather than the phase: TranslucentSubmit is the interface that carries it, through TranslucentSubmit.distanceToCameraSq, and five of the twelve renderers’ nested Submit records implement it — BlockModelFeatureRenderer, ItemFeatureRenderer, ModelFeatureRenderer, MovingBlockFeatureRenderer and TextFeatureRenderer. A phase can only order what its nodes can answer. What the two kinds do differently to a node’s chance of sharing a draw belongs to entity rendering.

#phasewhat lands in itdrained by
1SubmitNodeCollection.solidthe opaque default: models whose RenderType is not an outline and does not blend or forces the solid phase (RenderType.forceSolidModelPhase), block models whose render type neither blends nor outlines, moving blocks whose model does not declare the translucent material flag, an item’s quads whose render type does not blend, custom geometry that neither blends nor outlines, the opaque half of every quad-particle group, every flame and every leash — plus the opaque cases rows 3, 4, 5, 9, 10 and 12 send here.executeSolid
2SubmitNodeCollection.shadowsone node per SubmitNodeCollection.submitShadow, carrying the radius and the EntityRenderState.ShadowPiece list sampled at extract.executeTranslucent, sweep 1
3SubmitNodeCollection.nameTagsevery name tag’s node in Font.DisplayMode.NORMAL, see-through or not — a see-through one’s carries an emission bump on its light, opaque white and no background, and while the level renders goes to solid instead.executeTranslucent, sweep 1
4SubmitNodeCollection.textsworld-space text that is not a name tag, from SubmitNodeCollection.submitText, and a text display’s background, from SubmitNodeCollection.submitTextBackground, when not see-through — though while the level renders, text in opaque colour with no background goes to solid.executeTranslucent, sweep 1
5SubmitNodeCollection.shapeOutlinesVoxelShape edge outlines submitted without the after-terrain flag, unless their colour is fully opaque — those go to solid, flag or not.executeTranslucent, sweep 2
6SubmitNodeCollection.translucentBlocksAndItemsan item’s quads whose render type blends — an item with both kinds is two nodes, here and in solid — block models whose render type blends, and moving blocks whose model declares the translucent material flag.executeTranslucent, sweep 3
7SubmitNodeCollection.translucentModelsmodels whose RenderType blends, does not force the solid phase and is not the water mask.executeTranslucent, sweep 1
8SubmitNodeCollection.translucentCustomGeometrycustom geometry whose RenderType blends — a simple phase in spite of the name, so it is not distance-sorted.executeTranslucent, sweep 1
9SubmitNodeCollection.translucentGizmosthe translucent debug primitive group submitted without the on-top flag — its opaque twin goes to solid.executeTranslucent, sweep 2
10SubmitNodeCollection.breakingOverlaythe crumbling decal over geometry that blends: a model’s ModelFeatureRenderer.CrumblingOverlay, from SubmitNodeCollection.submitCrumblingOverlay on a render type that admits one, and a SubmitNodeCollection.submitBreakingBlockModel for a block whose model declares the translucent material flag — over anything opaque the decal goes to solid.executeTranslucent, sweep 3
11SubmitNodeCollection.waterMaskmodels submitted with the water-mask render type.executeTranslucent, sweep 3
12SubmitNodeCollection.afterTerrainoutlines flagged after-terrain, unless fully opaque, plus the translucent half of every quad-particle group.executeTranslucentAfterTerrain
13SubmitNodeCollection.alwaysOnTopGizmosgizmo groups flagged on top, opaque or not.executeAlwaysOnTop
14SubmitNodeCollection.outlinea second copy of a model, block model, moving block or item whose outline colour was non-zero — a model or block model only where its render type has an outline variant, and a moving block or item with its ordinary type, re-typed quad by quad inside the feature renderer to the outline variant every item and moving-block render type has — plus custom geometry, and a model or block model submitted with an outline render type (an invisible entity that glows), which are not a second copy at all: an outline render type routes the only copy here.executeOutline, which FeatureRenderDispatcher.renderAllFeatures never calls — LevelRenderer does, into its own target
15SubmitNodeCollection.seeThroughthe storage’s one phase, SubmitNodeStorage.seeThrough, not one per bucket: the second node a see-through name tag emits, in Font.DisplayMode.SEE_THROUGH with the background restored, and any text or text background submitted in that mode — sorted by distance across every bucket at onceFeatureRenderDispatcher.PreparedFrame.executeSeeThrough, once for the whole storage

Two things in the table are worth reading twice. A quad-particle group reaches the collector in one call and becomes two nodes, one in solid and one in afterTerrain, each wrapping the same render state with a flag that picks which of its layers it draws. And outline is not simply solid submitted twice: the glow is a second submission for a model, a block model, a moving block or an item, but flames, leashes and quad particles never reach it at all, a blending model pairs its outline copy with translucentModels rather than solid unless its render type forces the solid phase, and custom geometry, like a model submitted with an outline render type, goes to one phase or the other and never both.

The twelve feature renderers

In the order FeatureRenderDispatcher’s constructor registers them. All but one extend RenderTypeFeatureRenderer, which owns the shared StagedVertexBuffer draw and the merging of geometry that shares a render type.

feature rendererwhat it writes
ShadowFeatureRendererfour vertices per EntityRenderState.ShadowPiece onto one shared shadow render type, with the piece’s alpha as the colour and UVs derived from its bounds and the shadow radius
FlameFeatureRenderera stack of fire quads scaled to the entity’s bounding box, alternating two block-atlas sprites and flipping their U every other pair, at full block light
ModelFeatureRenderermodels — an entity’s, a block entity’s, a special item’s such as a shield or trident, and the elder guardian particle’s: Model.setupAnim and then Model.renderToBuffer over the ModelPart tree, into a buffer optionally wrapped for a sheeted decal or a UvMapping — an atlas sprite, or an armour trim’s paletted texture
TextFeatureRendererarbitrary world-space text and every name tag: glyph quads and background prepared through Font at the submitted pose and display mode, with an eight-direction outline pass and a polygon-offset second pass when an outline colour is set, and a text display’s background submitted on its own
LeashFeatureRenderera strip between the two ends of an EntityRenderState.LeashState, walked twice, as two ribbons that cross — twenty-five stations out and twenty-five back, a pair of vertices at each, a hundred in all — with its light interpolated between the endpoints and its colour alternating per station
ItemFeatureRendereritem quads, in one pass over the submits — a stack with a foil draws each quad on its material’s glint render type, which samples the enchantment glint beside the item texture, so geometry and foil go out together
CustomFeatureRenderernothing of its own: it hands the caller’s SubmitNodeCollector.CustomGeometryRenderer a vertex consumer for the requested render type
BlockModelFeatureRendererthe quads of a BlockStateModelPart list, in Direction order, at one fixed light and overlay coordinate for the whole submit
MovingBlockFeatureRenderera whole block state re-tesselated through ModelBlockRenderer — pistons, falling blocks and the transient blocks a server sends — honouring the ambient-occlusion and cutout-leaves options
QuadParticleFeatureRendererthe only one outside RenderTypeFeatureRenderer: it appends particle draws to the StagedVertexBuffer per SingleQuadParticle.Layer and issues them into the RenderPass it is handed, on each layer’s own pipeline
ShapeOutlineFeatureRenderertwo line vertices per edge of a VoxelShape, each carrying a per-vertex line width
GizmoFeatureRendererthe debug vocabulary: quads, triangle fans, lines, texts and points out of a DrawableGizmoPrimitives.Group, camera-relative

Those twelve are the answer to what can be drawn from a submit node — which, by the exclusions the opening makes, is everything in a level except the sky, the clouds, the weather, the world border and terrain. Anything a renderer wants that none of the other eleven draws goes through CustomFeatureRenderer.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Density-function nodes

Verified against Minecraft 26.3 · Reference · the forty-four node types a worldgen/density_function file may name: what each one takes, what the compiler turns it into, what range it reports, and which ids the shipped data actually writes.

Density functions is the lecture: one graph, rewritten twice and compiled once per dimension, and one kind of cache. This is the catalogue behind it — the four tables you would pause the video to read.

DensityFunctions.bootstrap registers every entry below into BuiltInRegistries.DENSITY_FUNCTION_TYPE, in this order, under the minecraft namespace. That registry is built-in and frozen at start-up, which is why adding a new kind of node takes code while adding a new graph takes a JSON file.

Forty-four node types are registered (DensityFunctions.bootstrap): one by name, then three context values, seven more by name, eleven unary transforms, four roundings, six arithmetic, and twelve last.

The table

children counts the density-function slots; each one accepts an id string, an inline object or a bare number, because every child slot is typed DensityFunction.CODEC.

idclasschildrenother fieldswhat it computes
constantConstantFunction0valuea fixed value
blend_alphaSimpleDensityFunction0—the chunk’s blend alpha from the sampling context, else 1.0
blend_offsetSimpleDensityFunction0—the chunk’s blend offset from the context, else 0.0
beardifierSimpleDensityFunction0—the chunk’s structure-terrain adjustment from the context, else 0.0
noiseNoiseFunction3, optionalnoise, xz_scale, y_scalesamples the noise a NormalNoise defines at position × scale, plus three offsets
end_outer_islandsEndIslandFunction0—the End’s simplex field of outer islands, as a density
distance_to_pointDistanceToPointFunction0point, metricthe distance from a fixed point, by one of four metrics
gradientGradientFunction0axis, tiling, from_coordinate, to_coordinate, from_value, to_valueone coordinate mapped onto a value range, clamped, repeated or mirrored past the ends
shift_aShiftNoiseFunction.ShiftA0noisedomain warp read at x, 0, z
shift_bShiftNoiseFunction.ShiftB0noisedomain warp read at z, x, 0
shiftShiftNoiseFunction.Shift0noisedomain warp read at x, y, z
absUnaryFunction1—absolute value
squareUnaryFunction1—the child squared
cubeUnaryFunction1—the child cubed
sqrtUnaryFunction1—the square root
half_negativeUnaryFunction1—identity above zero, halved below
quarter_negativeUnaryFunction1—identity above zero, quartered below
reciprocalUnaryFunction1—the reciprocal
negateUnaryFunction1—the child negated
squeezeUnaryFunction1—clamp to ±1, then a soft odd cubic
logUnaryFunction1—the natural logarithm
signUnaryFunction1—the child’s sign: −1, 0 or 1
floorRoundFunction2—rounded down to a multiple of multiple, 1 unless given
roundRoundFunction2—rounded to the nearest multiple
ceilRoundFunction2—rounded up to a multiple
truncateRoundFunction2—rounded toward zero to a multiple
addBinaryFunction2—the sum
subBinaryFunction2—the difference
mulBinaryFunction2—the product — with neither child a constant, a single sample short-circuits when the first is exactly zero
divBinaryFunction2—the quotient, with the same short-circuit
minBinaryFunction2—the minimum — with neither child a constant, a single sample skips the second when the first is already at or below the second’s lower bound
maxBinaryFunction2—the maximum, with the symmetric skip
powPowFunction2—base raised to exponent
splineSplineFunctioninside the splinesplinea CubicSpline whose coordinates are themselves density functions
lerpLerpFunction3—from first to second, by alpha
clampClampFunction1min, maxthe child, clamped
range_choiceRangeChoiceFunction3min_inclusive, max_exclusiveone of two branches, by whether the input is in range
interval_selectIntervalSelectFunction1 + a listthresholdsthe branch whose ascending threshold the input first falls below
cacheCacheFunction1—nothing of its own — a request the compiler replaces with a cache of its child; left in place it refuses to compile
blend_densityBlendDensityFunction1—the child, blended toward old-terrain density where the context holds a non-empty Blender
interpolatedInterpolatedFunction1cell_size_xz, cell_size_ythe child read at cell corners and interpolated between them
sliceSliceFunction1axis, coordinatethe child read with one coordinate pinned
find_top_surfaceFindTopSurfaceFunction2lower_bound, cell_heightsteps down in strides until the density goes positive, and returns that Y
old_blended_noiseBlendedNoise0xz_scale, y_scale, xz_factor, y_factor, smear_scale_multiplierthe pre-1.18 terrain noise, decoded unseeded

Where one class serves several ids. The three context values are the constants of one enum, SimpleDensityFunction; the eleven unary transforms are all UnaryFunction, a record of a UnaryFunction.Type and its input; the four roundings share RoundFunction, and the six arithmetic ids share BinaryFunction. In each case an enum constant carries its own codec, and the node’s codec() returns it — which is how a re-serialised graph comes back with the right id. Nothing is folded while parsing: the parse step builds each record as written, a constant operand is specialised only when the graph is compiled (into a sampler such as BinaryFunction.ConstAddSampler), and add in the JSON comes back as add.

One member of DensityFunctions is not in this table because it is not registered: DensityFunctions.HolderHolder, the in-memory stand-in for an id reference, which has no codec at all.

What the caches become

DensityFunctionCompiler.getSampler is the compile step: each dimension’s RandomState compiles a graph the first time it is asked for, and no chunk rewrites anything. A cache node is a request. Before compiling, the compiler’s rewrite inlines every DensityFunctions.HolderHolder and installs a DensityFunctionCompiler.PreparedCache in place of each cache node — one per distinct input, so two cache nodes around equal graphs share one cache. A second rewrite wraps each subgraph that ignores an axis its parent reads, constants and gradients aside, in a slice pinned at zero on that axis. A prepared cache refuses to encode, so only the parsed graph, which the rewrites copy rather than change, re-serialises.

nodecompiled tokeyed on
cachea CachingDensitySampler carrying the prepared cache’s number; its storage is a cell of the SamplerContext it runs inthe volume for a volume read — one buffer, refilled when a different DensityVolume asks — and the position for a single sample: the last block sampled, or its entry in the cached volume
interpolateda sampler that reads the child at the cell corners, a volume stepped by cell_size_xz and cell_size_y, and interpolates every block betweennothing — no value outlives the read
blend_densitya sampler that asks the context for this chunk’s Blender and blends only when there is a non-empty onenot cached

A context has cache cells only when it was built with SamplerContext.Builder.enableCaches: NoiseChunk builds one for each chunk and for each height query, and the biome step, the structure starts and checks, a caching biome resolver, the spawn search and the F3 readout build their own, and SamplerContext.EMPTY_UNCACHED passes every cached read straight through.

Three of the registered nodes above are the constants of one enum, SimpleDensityFunction, and the compiler replaces none of them: each compiles to a ContextBoundSampler that looks its key up in the sampling context on every read, which is how one compiled graph serves every chunk.

singletonreadswith nothing in the context
SimpleDensityFunction.BLEND_ALPHABlender.ALPHA_KEY, a sampler over the alphas NoiseChunk’s constructor computes at quart resolution when the blender is not emptythe constant 1.0
SimpleDensityFunction.BLEND_OFFSETBlender.OFFSET_KEY, likewisethe constant 0.0
SimpleDensityFunction.BEARDIFIERBeardifier.CONTEXT_KEY, this chunk’s Beardifierthe constant 0.0

Bounds

Every node answers DensityFunction.range, an Interval, without a position. Most take theirs from a child — cache, interpolated, slice and blend_density all do — and no registered node stores one: BinaryFunction, UnaryFunction and ClampFunction combine their children’s ranges through Interval on every call, and BlendedNoise derives its own from its parameters. The bounds of a parsed graph are the ones the compiler reads: a noise’s range comes from its definition, not its seed, and no rewrite changes a bound. What a bound is worth is the lecture’s argument; this table is which node departs from its child, and how.

idits range
add, sub, mul, div, min, maxinterval arithmetic on the two children’s rangesmul takes the least and greatest of the four products of the operands’ ends, a zero end giving zero even against infinity; div multiplies by the reciprocal’s range; min and max take the element-wise minimum and maximum. A min or max whose two ranges cannot overlap logs a warning when it is compiled, and only the side that wins whenever the inputs stay inside their declared ranges is sampled
abs, squarethe child’s ends mapped, with zero as the minimum when the child’s range contains zeroInterval.abs and Interval.square map both ends
reciprocal±infinity whenever the child’s range straddles zerothe reciprocal has no finite bound across zero
clampthe child’s range, clamped into min and maxa child already inside them keeps its own, narrower range
find_top_surfaceits lower_bound and its upper bound’s maximumY coordinates — this node’s range is on a different scale from every other row
noiseits NormalNoise’s, fixed when the noise definition is decodedthe three shifts are ignored; the sixty-four shipped noise definitions come out between ±0.87 and ±5.73

DensityFunctions.HolderHolder, off the table, asks its target, and throws while its holder is unbound; forward references parse anyway, because nothing asks a node for its bound until the graph is compiled.

What vanilla actually uses

Fifty-five JSON files ship under worldgen/density_function — four at the top level, the rest in a directory each for the End, the Nether, the overworld and the overworld’s two preset variants — and between them they use thirty-three of the forty-four ids. The seven Registries.NOISE_SETTINGS files, the recipes terrain reads — two of them, caves and floating_islands, named by no shipped world preset — write no id the density-function files lack. All seven final densities end by adding beardifier, three of them in a file and four inline.

That leaves eleven ids vanilla data never writes. constant is never written as a typed object, because a bare number is one. Nine are maths on a value: sqrt, reciprocal, log, sign, pow and the four roundings. And shift — the three-dimensional domain warp — is used by nothing: ShiftNoiseFunction.ShiftA and ShiftNoiseFunction.ShiftB cover the two two-dimensional warps vanilla wants.

For a 1.21-era reader. NoiseChunk.wrapNew is gone: DensityFunctionCompiler compiles each graph once per dimension, and each chunk supplies a SamplerContext. flat_cache, cache_2d, cache_once and cache_all_in_cell are gone, and cache does their work. shifted_noise is noise with shift fields, y_clamped_gradient is gradient on the Y axis, end_islands is end_outer_islands with the central island left to distance_to_point, and minValue and maxValue are DensityFunction.range.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Threads

Verified against Minecraft 26.3 · Reference · Hand-kept from net/minecraft/util/thread and every Thread the game starts, beside Anatomy’s four — looked up, not watched.

Every thread the game creates, who creates it, what runs on it, and what is allowed to run on it. The last column is the rule the rest of the documentation leans on: game state belongs to exactly one thread, and anything else hands its work to that thread rather than doing it itself, apart from the unlocked readers and the Netty-side handlers this page names.

Three ways work crosses a thread boundary

Two threads own game state — the Render thread owns the client’s, the Server thread owns the world’s — and everything else is a way of getting work to them or from them. Work crosses a thread boundary in exactly three ways, and the figure names which on every edge that is one of them: a posted task (a Runnable handed to the executor that will run it — the owner’s BlockableEventLoop, a worker pool, or the connection’s Netty event loop, which is where Connection.sendPacket puts a packet sent from any other thread; a console line, left in a list the tick drains, is the same shape), a completed future (a worker’s result, completed onto the owner’s executor), or a hopped handler (a packet decoded on Netty and re-posted to its owner by PacketUtils.ensureRunningOnSameThread).

flowchart TD
    RT["Render thread: Minecraft.runTick"]:::client
    NET["Netty IO: Connection"]:::netty
    ST["Server thread: MinecraftServer.runServer"]:::server
    WK["Worker-Main-n: Util.backgroundExecutor"]:::worker
    IO["IO-Worker-n: Util.ioPool"]:::worker
    SND["Sound engine: SoundEngineExecutor"]:::client
    LST["console, RCON, management: dedicated only"]:::server
    WD["Server Watchdog, query: dedicated only"]:::server
    RT -- "posted task" --> NET
    ST -- "posted task" --> NET
    NET -- "play packets: hopped" --> RT
    NET -- "play packets: hopped" --> ST
    RT -- "posted task" --> WK
    ST -- "posted task" --> WK
    WK -. "completed future" .-> RT
    WK -. "completed future" .-> ST
    ST -- "posted task" --> IO
    IO -. "completed future" .-> ST
    RT -- "posted task" --> SND
    LST -- "posted task" --> ST
    WD -. "reads, unsynchronised" .-> ST

The threads that carry work, coloured by the side each serves, and every edge between them labelled with how work crosses it: a posted task out, a completed future back, a play packet hopped onto its owner, and the one edge that is none of the three, the watchdog and the query listener reading the Server thread’s state without a lock.

The daemon column is the one that decides how the process ends, because a non-daemon thread holds the JVM open until something stops it. Six rows of the table are non-daemon; on a dedicated server, which has no Render thread, that is five, and only four of them can hold it open: ServerMain returns the moment it has registered the shutdown hook, long before anything needs stopping, which leaves the Server thread, Util.ioPool’s workers, and the RCON and query listeners. The first two are retired deliberately — the Server thread by returning, the IO pool by Util.shutdownExecutors — and the other two are GenericThreads, stopped by clearing GenericThread.running and joining: the query listener polls its socket every half second and notices the flag, and RconThread.stop closes its socket first, so the RCON accept ends at once. Everything else in the table is a daemon and simply stops existing; Server Connector, Swing’s event dispatch thread, Realms’s threads and the chat filter’s workers the Server thread starts, among the situational threads below, are not (a worker a sign or book edit starts from Netty inherits Netty’s daemon flag), and nor are the two java.util.Timer threads the client’s notifications and the watchdog’s exit start (how a server dies).

Every box in the figure has a row in the table — the listeners’ box has three, the watchdog’s two — and two rows have no box, because ServerMain has stopped existing before any of these edges are busy and the timer-hack thread does nothing at all. Netty is drawn once and shared because it is: in singleplayer the client’s Connection and the integrated server’s run on the same Netty Local IO threads, and the packets between them are real.

The threads a lecture leans on

threadmade bydaemonrunsmay touch
Render thread (client)the JVM main thread, renamed in client/main/Mainno — it is mainMinecraft.run → Minecraft.runTick once per frame; Minecraft.tick 0–10 times inside itEverything client-side: ClientLevel, LocalPlayer, the GPU (RenderSystem.assertOnRenderThread), screens, options. It is also the client’s event loop (Minecraft is a ReentrantBlockableEventLoop), so packet handlers run here after PacketUtils.ensureRunningOnSameThread.
ServerMain (dedicated)the jar’s bundler, which unpacks the libraries on the JVM’s main thread and starts this onenothe whole of boot: the properties, the EULA, the ManagementServer, session.lock, the WorldLoader, then MinecraftServer.spin, which constructs the DedicatedServer before it starts the Server thread, and finally registering the Server Shutdown Thread as a JVM shutdown hook — which is a separate thread and not this oneEverything, until the Server thread exists; after that, nothing (starting a server)
Server threadMinecraftServer.spinnoMinecraftServer.runServer → MinecraftServer.processPacketsAndTick every TickRateManager.nanosecondsPerTick (50 ms by default); MinecraftServer.waitUntilNextTick drains the task queue in the slackEvery ServerLevel, every chunk, entity and block entity, the PlayerList. Serverbound play packet handlers run here, all but the ten in the table below and the three they inherit that never hop. One per server; singleplayer has exactly one.
Netty IO (Netty NIO IO #n, Netty Epoll IO #n, Netty Kqueue IO #n, Netty Local IO #n)EventLoopGroupHolderyesthe Connection pipeline: split, decrypt, decompress, decode; encode, compress, encrypt — and the handshake and login handlersBytes and Packet objects — plus, in handshake and login, the handlers themselves: ServerHandshakePacketListenerImpl and ServerLoginPacketListenerImpl never hop. The login state machine is not all theirs, though: ServerLoginPacketListenerImpl is a TickablePacketListener, so the Server thread advances it once a tick (protocol phases walks that state machine) through MinecraftServer.tickConnection. A play handler that needs game state re-posts to the owning thread. Local is the in-process channel of singleplayer.
Worker-Main-nUtil.backgroundExecutor — a ForkJoinPool sized to the JDK’s available-processor count minus one, clamped by Util.maxAllowedExecutorThreads and capped by the max.bg.threads property (Util.getMaxThreads)yeschunk generation and lighting via ChunkTaskDispatcher; section meshing via SectionRenderDispatcher; resource-reload prepare phases; chunk serialisationIts own inputs, and during generation the chunks its step may write — the one being generated and, while features are placed, its eight neighbours — the generated chunk’s first mobs included, which the Server thread builds again from their saved tags when the chunk goes full. Results return to the owning thread as a CompletableFuture completed onto that thread’s executor, or, for a mesh with geometry, through the upload the Render thread runs — a mesh with nothing to draw the worker installs itself.
IO-Worker-nUtil.ioPoolnoregion file reads and writes through IOWorker, one PriorityConsecutiveExecutor per storage so writes to one file stay orderedFiles. Util.nonCriticalIoPool (Download-n) is a second pool of the same kind, for downloads, profile and account-service lookups, and sound decoding, and the one difference is this column: its threads are daemons, so nothing waits for them.
Sound engineSoundEngineExecutoryesa BlockableEventLoop that owns the per-source OpenAL callsThe SoundEngine’s channels; see the sound engine. Device open/close and the deletion of cached static buffers stay on the Render thread.
Server WatchdogDedicatedServer.initServer (dedicated only, positive limit only)yesServerWatchdogWrites nothing, but reads game state unsynchronised while the Server thread is mid-tick — the game rules and every level’s ServerLevel.getWatchdogStats, for the crash report. It kills the JVM past DedicatedServerProperties.maxTickTime, and with the tick wedged that kill does not save the world.
Server console handlerDedicatedServer.initServeryesreads stdinQueues each line to the server thread as a command; runs nothing itself.
RCON Listener #n / RCON ClientRconThread.create from DedicatedServer.initServer, when enable-rcon is on and RconThread.create gets past its own gates: it returns nothing if rcon.port is outside 1–65535, if rcon.password is empty, or if the socket cannot be openednoaccepts RCON sockets on a 500 ms timeout, though RconThread.stop closes the socket before it joins, so the accept ends at once; one RconClient thread per connection, which blocks with no timeoutSockets. Each command is queued to the server thread. Dedicated only. DedicatedServer.onServerExit must stop it or the JVM will not exit (how a server dies).
Query Listener #nQueryThreadGs4.create from DedicatedServer.initServer, when enable-querynothe GS4 query protocol, on a 500 ms socket timeout, so it notices GenericThread.running going false within half a secondIts full-status reply, rebuilt at most every five seconds from the player count and names it reads off the live PlayerList without a lock; the basic reply reads the count afresh on every request. Dedicated only, and stopped by the same call as RCON.
Management server IO #nManagementServer, built by JsonRpc in server/Mainyesa second, independent Netty event-loop group: the JSON-RPC/WebSocket management API and its heartbeatIts own pipeline; management calls reach the game through the server’s task queue. Dedicated only.
Timer hack threadUtil.startTimerHackThreadyessleeps foreverNothing. Keeps the JVM’s timer resolution high by existing.

The handlers that never hop

Connection.channelRead0 calls a packet’s handler on the Netty thread, and a handler’s first line is normally PacketUtils.ensureRunningOnSameThread, which re-posts it to the owning thread and aborts (the connection). The client’s play listener has nine handlers that omit it, two of them inherited; the server’s declares ten, and inherits three more that never hop (the keep-alive, the pong and the cookie response). They differ in kind: most of the client’s run to completion on Netty, and six of the server’s ten use the Netty thread only to hand the work somewhere else.

Nine on the client, then. Seven are declared in ClientPacketListener itself; the last two are inherited from ClientCommonPacketListenerImpl and are the two that matter most, because a keep-alive is answered, and a disconnect’s channel closed, from the Netty thread. Three rows reach the Render thread after all: the low-disk warning by a posted task, a keep-alive deferred to its next tick, and a disconnect, whose Connection.handleDisconnection that thread’s tick runs.

handlerwhat it does on the Netty thread
ClientPacketListener.handlePlayerCombatEnternothing — the body is empty
ClientPacketListener.handlePlayerCombatEndnothing — the body is empty
ClientPacketListener.handleChunkBatchStartstarts the ChunkBatchSizeCalculator’s clock
ClientPacketListener.handleChunkBatchFinishedstops it and sends ServerboundChunkBatchReceivedPacket with the chunks-per-tick it now wants — so the loop in what the client is told times the batch’s arrival and decode on Netty, not mesh building
ClientPacketListener.handleDebugSamplehands the sample to DebugScreenOverlay.logRemoteSample
ClientPacketListener.handlePongResponserecords the round trip in PingDebugMonitor
ClientPacketListener.handleLowDiskSpaceWarningcalls Minecraft.sendLowDiskSpaceWarning, which posts the toast to the Render thread itself — one of the three that cross after all, by Minecraft.execute rather than by the hop
ClientCommonPacketListenerImpl.handleKeepAlivereplies with ServerboundKeepAlivePacket through ClientCommonPacketListenerImpl.sendWhen, deferred while the window is frozen at RenderSystem.isFrozenAtPollEvents — so the answer goes at once from the Netty thread unless the Render thread is stuck in the poll, and then waits for its next tick
ClientCommonPacketListenerImpl.handleDisconnectcalls Connection.disconnect straight from the event loop, which closes the channel; the Render thread’s next tick finds it closed and runs Connection.handleDisconnection

Ten on the server, out of ServerGamePacketListenerImpl’s sixty-one game handlers — the other fifty-one open on the hop (the server tick has what that costs the tick). They fall into four kinds: two that touch nothing, four that post a task, two that wait on a future, and two that write listener state on Netty because nothing else can be asked to.

handlerwhat it does on the Netty thread
ServerGamePacketListenerImpl.handlePingRequestsends ClientboundPongResponsePacket back with the time it arrived, and nothing else
ServerGamePacketListenerImpl.handleCustomPayloadnothing — the body is empty
ServerGamePacketListenerImpl.handleChatadvances the acknowledgement window under the LastSeenMessagesValidator’s lock, then ServerGamePacketListenerImpl.tryHandleChat: illegal characters disconnect, a hidden chat visibility answers with a system message, and everything else resets the last-action time and posts the work through MinecraftServer.execute
ServerGamePacketListenerImpl.handleChatCommandthe same door without a signature — ServerGamePacketListenerImpl.tryHandleChat posts the parse and the run
ServerGamePacketListenerImpl.handleSignedChatCommandthe same, the signed arguments collected inside the posted task
ServerGamePacketListenerImpl.handleCustomCommandSuggestionshands the request to the player’s ServerCommandSuggestionsProvider, which keeps only the latest and answers at most one a tick, posting the work through MinecraftServer.execute or leaving it for the listener’s own tick
ServerGamePacketListenerImpl.handleSignUpdatestrips formatting and sends the four lines to the text filter; the result comes back on the server’s own executor
ServerGamePacketListenerImpl.handleEditBookthe same shape for a book’s title and pages, and the same hand-back
ServerGamePacketListenerImpl.handleChatAckapplies the offset to LastSeenMessagesValidator under a lock, and disconnects the player if it does not validate — real listener state, written off the main thread
ServerGamePacketListenerImpl.handleConfigurationAcknowledgedswaps the inbound protocol and installs a ServerConfigurationPacketListenerImpl, so the return to configuration begins on the network thread (protocol phases)

Situational threads

Real, but nothing in the corpus hangs on them: User Authenticator (one per login that asks the session server), Chat-Filter-Worker, Server Pinger and Server Connector (the multiplayer screen), Telemetry-Sender, LanServerPinger and its detector, World Upgrader, Datafixer Bootstrap (priority 1, so it yields to everything), the client and server shutdown hooks, ClientShutdownWatchdog once the client is closing, Swing’s event dispatch thread when a dedicated server is started without –nogui and runs its MinecraftServerGui and its Server log monitor, Latency Simulator #n behind a debug latency flag, the Friends List fetcher the client starts when its friend list is on, and ChaseServer’s two threads and ChaseClient’s one, which exist only behind SharedConstants.DEBUG_CHASE_COMMAND and the /chase command it registers. Realms starts eight more — seven in com/mojang/realmsclient and RealmsConnect’s connect task — and is out of scope with the rest of com/mojang/realmsclient.

The rule the last column is stating

Two threads own game state and everything else is a way of getting work to them: client state is the Render thread’s, server state is the Server thread’s, and in singleplayer they share a JVM and still talk about the world only through packets over the local channel (anatomy has the four to memorise, and the settings that do cross by direct call). Three consequences run through the table and each is owned by a lecture, not here: a play packet is decoded on Netty and handled on its owner’s thread (the connection); workers compute and owners commit, so a generated chunk is installed by the thread that owns it, never by the pool (the level tick); and an owning thread waiting on a future keeps draining its own queue through BlockableEventLoop.managedBlock, which is why a tick that waits for a chunk does not deadlock the chunk that needs the tick (the server tick).

Where to look

Util.backgroundExecutor · Util.ioPool · Util.startTimerHackThread · MinecraftServer.spin · EventLoopGroupHolder · GenericThread · SoundEngineExecutor · ServerWatchdog · PacketUtils.ensureRunningOnSameThread · BlockableEventLoop.managedBlock


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Math and primitives

Verified against Minecraft 26.3 · Reference · The coordinate spaces, geometry and randomness every system page assumes — looked up, not watched.

Every system in the game speaks in a handful of value types: an integer block position, a chunk column, a 16³ section, a double-precision world position, a direction, a box, a collision shape, a random source. They are the types the codebase reaches for most — BlockPos alone has 1,229 importers, more than any other Minecraft class — and most of what is confusing about “which coordinate is this” is answered by knowing which type a method takes.

The coordinate spaces

Six integer spaces and one double one, and every conversion is a named method. The figure is the spaces from the finest to the coarsest, and the one that adds a dimension, each arrow the arithmetic of one step; the table under it names the method for each arrow and, for all but GlobalPos, the way back, and the three that also pack to a long key are the next section’s.

flowchart TD
    V["Vec3: a double position"] -- "floor" --> B["BlockPos: one block"]
    B -- "shift 2" --> Q["QuartPos: a 4-block cell"]
    Q -- "shift 2" --> S["SectionPos: a 16-block cube"]
    B -- "shift 4" --> S
    S -- "drop y" --> C["ChunkPos: a 16-block column"]
    C -- "shift 5" --> R["region: 32 chunks a side, one file"]
    B -- "plus a dimension" --> G["GlobalPos"]

The coordinate spaces as a ladder of sizes: after the first step, which floors, every step down is a right shift by as many bits as the next space is coarser, except the one that drops the y axis and the one that adds a dimension; the table names the method for each.

spaceunittypeowner / notesconversions
block1 block, intBlockPos (extends Vec3i)immutable unless it is a BlockPos.MutableBlockPos, the loop subclassBlockPos.containing floors a double position; BlockPos.asLong packs to a long and BlockPos.of unpacks one
world position1 block, doubleVec3 (implements Position)entity positions, ray casts, velocitiesVec3.atCenterOf, Vec3.atLowerCornerOf, Vec3.atBottomCenterOf from a Vec3i; Vec3.directionFromRotation from pitch/yaw
chunk column16 blocksChunkPos — a record of x and zpacked to a long, the key of the chunk maps in ChunkMap and the ticket systemChunkPos.containing from a BlockPos; ChunkPos.pack / ChunkPos.unpack; ChunkPos.getMinBlockX, ChunkPos.getWorldPosition
section16³ cubeSectionPos (extends Vec3i)lighting, entity sections, render sectionsSectionPos.of from block/chunk/entity; SectionPos.blockToSectionCoord (shift 4), SectionPos.sectionToBlockCoord, SectionPos.sectionRelative (mask 15); SectionPos.chunk drops y; SectionPos.asLong
quart / biome4 blocksQuartPos (static only)biome samplingQuartPos.fromBlock (shift 2), QuartPos.toBlock, QuartPos.fromSection, QuartPos.toSection
region32 chunksnone — methods on ChunkPosthe .mca file gridChunkPos.getRegionX, ChunkPos.getRegionLocalX, ChunkPos.minFromRegion; ChunkPos.REGION_SIZE
dimension-qualified block—GlobalPos — record of a Level key and a BlockPoscompass targets, beds, a player’s last death locationGlobalPos.of
integer boxblocksBoundingBox for structures; BlockBox is a newer record that nothing yet usesstructure bounds, piece placementBoundingBox.intersects, BoundingBox.encapsulate; BlockBox.aabb, BlockBox.contains
double boxblocksAABB (a class of six public final doubles)entity bounding boxes, block shapes’ boundsAABB.move, AABB.inflate, AABB.intersects, AABB.clip
pitch / yawdegrees, floatVec2look direction, as (xRot, yRot) — except the Rotation NBT tag, which stores it the other way roundDirection.fromYRot, Direction.toYRot
pose rotationdegrees, float ×3RotationsArmorStand poses, and nothing else—
model / render spacefloatJOML Vector3f, Matrix4f, Quaternionf (external)everything under client/rendererVec3.toVector3f; Direction.step, Direction.getRotation; com/mojang/math Axis builds quaternions

Position is the three-double interface Vec3 implements. Vec3i is the int triple with the arithmetic — immutable to every caller that does not hold a BlockPos.MutableBlockPos, which the closer takes up — (Vec3i.offset, Vec3i.relative, Vec3i.distSqr, Vec3i.distManhattan); BlockPos adds the iteration helpers (BlockPos.betweenClosed, BlockPos.withinManhattan, BlockPos.spiralAround), each of which walks a single reused BlockPos.MutableBlockPos rather than allocating. BlockPos.breadthFirstTraversal is the exception in the same class: it is not an iterator at all, and allocates a queue of nodes and a set of visited longs. Cursor3D is a different allocation-free box cursor — the one that also classifies each position as inside, face, edge or corner (Cursor3D.TYPE_INSIDE, Cursor3D.TYPE_FACE, Cursor3D.TYPE_EDGE, Cursor3D.TYPE_CORNER) — and it is used by SectionPos, BlockCollisions and ClientLevel, not by BlockPos.

Colours are a primitive too, and they live in net/minecraft/util: ARGB is where every pack, unpack, lerp, multiply and alpha helper is, with CommonColors for the named constants, ColorRGBA for the codec-friendly value and Brightness for the packed block/sky light pair.

Three long keys

Three long-packings appear everywhere as map keys.

  • BlockPos.asLong — 26 bits X, 26 bits Z, 12 bits Y, high to low. BlockPos.PACKED_HORIZONTAL_LENGTH is derived from 30,000,000, the level’s hard horizontal bound (Level.MAX_LEVEL_SIZE), which is why it is 26 (BlockPos.MAX_HORIZONTAL_COORDINATE is 33,554,431); the remaining BlockPos.PACKED_Y_LENGTH is 12, giving −2048 to 2047. The usable range is narrower: DimensionType reserves a 32-block margin, so DimensionType.MIN_Y is −2032, DimensionType.MAX_Y is 2031 and DimensionType.Y_SIZE is 4064. BlockPos.getFlatIndex masks off the low four Y bits — it snaps a packed position to the bottom of its own 16-block section, which is the skylight walk-up idiom, not a per-column key (the top eight Y bits survive). BlockPos.STREAM_CODEC sends the packed long; Vec3i.STREAM_CODEC sends three varints.
  • SectionPos.asLong — 22 bits X, 22 bits Z, 20 bits Y. A section-relative position packs into a short (SectionPos.sectionRelativePos).
  • ChunkPos.pack — X in the low 32 bits, Z in the high 32. ChunkPos.INVALID_CHUNK_POS is a sentinel; ChunkPos.isValid is bounded by ChunkPyramid.MAX_CHUNK_COORDINATE_VALUE, which lives with the chunk status pyramid because the safety margin is derived from how many neighbours generation reads.

Directions and symmetry

Direction is the six-valued enum in 3D-data order DOWN, UP, NORTH, SOUTH, WEST, EAST (Direction.get3DDataValue); its horizontal subset has its own order starting at south (Direction.get2DDataValue). Direction.Axis (X, Y, Z), Direction.AxisDirection and Direction.Plane (horizontal, vertical) are the nested helpers; CompositeDirection.Direction8 the compass points; FrontAndTop the twelve jigsaw and crafter orientations; AxisCycle the axis permutation VoxelShape lookups use.

Block rotation is Rotation and Mirror (world/level/block), each of which maps onto OctahedralGroup (com/mojang/math) — the 48-element symmetry group of a cube, each element a permutation plus three inversion flags. That group is not decoration: Shapes.rotate, Shapes.rotateAll, Shapes.rotateHorizontal and Shapes.rotateAttachFace are how a block declares one shape and is handed the rest — three more from Shapes.rotateHorizontal, five from Shapes.rotateAll, and eleven from Shapes.rotateAttachFace, which is Shapes.rotateHorizontal run once per AttachFace. Transformation wraps a JOML matrix and lazily decomposes it into translation, left rotation, scale and right rotation for model JSON and a display entity’s transformation.

There are two things called Axis in scope: Direction.Axis and the quaternion factory in com/mojang/math. They are unrelated, and a grep finds a third in the save-migration tree, which this book does not cover.

Shapes and collision

A VoxelShape is a set of boxes on a per-axis coordinate grid, backed by a DiscreteVoxelShape bit grid (BitSetDiscreteVoxelShape). Shapes is the factory and algebra: Shapes.block, Shapes.empty, Shapes.box, Shapes.or, Shapes.join with a BooleanOp, Shapes.joinIsNotEmpty, Shapes.collide, Shapes.blockOccludes, with Shapes.EPSILON (1.0E-7) the tolerance the shape algebra’s own comparisons use and Shapes.BIG_EPSILON a coarser 1.0E-6; a collision context’s is above test uses a looser 1.0E-5. Implementations differ by how the grid is stored — CubeVoxelShape (even divisions), ArrayVoxelShape (explicit coordinate lists), SliceShape (a one-cell-thick view, used for face culling and occlusion) — and Shapes.joinUnoptimized picks an IndexMerger strategy per axis, building a CubeVoxelShape only when all three merge evenly; Shapes.join is that followed by VoxelShape.optimize.

Context-free collision and face-sturdiness queries are cheap because they are mostly not computed (the outline, interaction and support shapes are asked of the block every time): BlockBehaviour.BlockStateBase.initCache builds a BlockBehaviour.BlockStateBase.Cache per block state holding the collision shape, whether the collision shape reaches outside its cell, whether the collision shape is a full block, and a per-face sturdiness array — but only for a block whose shape is not dynamic, and the occlusion shape is not in it. That one is a field on BlockBehaviour.BlockStateBase itself, built whether the cache is or not. Without a cache a block answers those queries live, and says it has a large collision shape whatever it has; and the query an entity’s movement makes, with its CollisionContext, is never cached — it asks the block every time.

CollisionContext is what a shape query knows about who is asking: CollisionContext.of an entity (EntityCollisionContext — descending, bottom Y, held item, whether fluids collide), CollisionContext.empty, CollisionContext.placementContext — all three of which are EntityCollisionContexts, as is the MinecartCollisionContext CollisionContext.of returns for a minecart under the experimental movement flag. The one case that is neither is PositionCollisionContext, from CollisionContext.positionContext. Ray casts return a HitResult: BlockHitResult (position, face, inside, world-border) or EntityHitResult.

Two random families, a saved table, and a mixer

RandomSource (net/minecraft/util) is the interface. Most implementations live in world/level/levelgen, and the legacy family shares BitRandomSource, which defines RandomSource.nextInt and friends on top of a raw bit generator; XoroshiroRandomSource implements RandomSource directly, and RandomSequences keeps one more in world/. Two families coexist in one process:

  • Legacy LCG — LegacyRandomSource (the java.util.Random algorithm), SingleThreadedRandomSource (same, no atomics), ThreadSafeLegacyRandomSource. RandomSource.create returns a LegacyRandomSource with a uniquified seed; this is Level.random (inherited unchanged by ServerLevel and ClientLevel), Entity.random, ParticleEngine.random. RandomSource.createThreadLocalInstance returns a SingleThreadedRandomSource and is what ClientLevel.animateTick uses for block animation, and LevelRenderer for the block-destroy overlay.
  • Xoroshiro — XoroshiroRandomSource (128-bit state via RandomSupport.Seed128bit), the one a noise settings file gets unless its required legacy_random_source flag asks for legacy: NoiseGeneratorSettings.getRandomSource returns WorldgenRandom.Algorithm.XOROSHIRO unless the settings opt into legacy. Four of the seven shipped noise settings do opt in — nether, end, caves and floating_islands — and the overworld is not one of them, nor are amplified and large_biomes. So an ordinary world runs both families at once: its overworld’s noise is Xoroshiro and the noise of the two dimensions you walk into through a portal is legacy, while every dimension carves, and chooses where its structures go, with a legacy source, and decorates with a Xoroshiro one. RandomState forks the chosen algorithm positionally (PositionalRandomFactory.at, PositionalRandomFactory.fromHashOf) for the named noise consumers — the aquifer and the ore placer each get their own deterministic stream from the seed and position.

WorldgenRandom wraps any delegate and adds the seeding conventions — WorldgenRandom.setDecorationSeed, WorldgenRandom.setFeatureSeed, WorldgenRandom.setLargeFeatureWithSalt, WorldgenRandom.seedSlimeChunk — that make a structure land in the same place for the same seed. Features go through those, not through RandomState.

There is a third randomness path, on the Xoroshiro family: RandomSequence and RandomSequences, a saved, Identifier-keyed table of XoroshiroRandomSource streams derived from the world seed, which is what makes a loot table and /random reproducible across sessions. And two that are not a RandomSource at all: LinearCongruentialGenerator, the bare mixer BiomeManager uses for biome fuzzing, and Level.randValue, the integer Level.getBlockRandomPos steps for random-tick positions.

RandomSource.nextGaussian is produced by MarsagliaPolarGaussian, which caches a spare value — which is why reseeding a source resets it, in every implementation but the deprecated ThreadSafeLegacyRandomSource and WorldgenRandom, whose WorldgenRandom.setSeed reseeds only its delegate.

Mth and Util

Mth is the maths grab-bag (703 importers): Mth.floor, Mth.clamp, Mth.lerp, Mth.wrapDegrees, Mth.rotLerp, Mth.smallestEncompassingPowerOfTwo, Mth.log2, Mth.positiveModulo, Mth.hsvToRgb. Mth.sin and Mth.cos are lookups in a 65,536-entry table (Mth.cos is the same table with a quarter-turn phase shift), and the table is filled from the JDK’s ordinary sine rather than its strict one — so the platform-dependent step, if you are chasing animation determinism, is the table’s construction and not the lookup. Util (in net/minecraft/util, 457 importers) is where the executors live — Util.backgroundExecutor, Util.ioPool, Util.nonCriticalIoPool — along with time sources and collection helpers; Unit is the single-valued “void” type codecs and futures use. The other numeric odds and ends worth knowing by name are CubicSpline (terrain shaping), InclusiveRange, and BitStorage, the packed-integer array underneath every palette.

What trips people up

ChunkPos is a record with no asLong. The names are ChunkPos.pack and ChunkPos.unpack; construction from a block is ChunkPos.containing; the components are accessed as x() and z().

Vec3i.toMutable returns a JOML Vector3i, not a BlockPos.MutableBlockPos; the mutable block position comes from BlockPos.mutable or its constructor, and its BlockPos.MutableBlockPos.set / BlockPos.MutableBlockPos.move are the loop idiom.

A BlockPos may be mutable. Vec3i keeps protected setters that BlockPos.MutableBlockPos, a subclass of BlockPos, makes public; every other subclass treats them as final, so a BlockPos reference can hold a position that moves. BlockPos.immutable is the copy to call before storing a mutable one.

Level.random crashes when two threads draw at once. LegacyRandomSource holds an atomic seed not for safety but as a detector: a draw or a reseed whose compare-and-set loses to another thread’s write raises a ThreadingDetector exception, so concurrent use is caught when two uses collide and not otherwise. The genuinely safe variant, ThreadSafeLegacyRandomSource, and RandomSource.createThreadSafe are both deprecated. Touching a level’s random from a worker is loud only when it collides.

Tick randomness and worldgen randomness do not divide by family. The LCG drives every Level and Entity, the Nether’s and the End’s noise, and every dimension’s carvers and structure placement; the saved RandomSequences behind loot and /random are Xoroshiro, and so are the overworld’s noise and every dimension’s features. PositionalRandomFactory.parityConfigString is implemented by both families, so the parity dumps cover whichever one a dimension’s noise settings choose.

BlockBox is declared and unused. It is a tidy BlockPos-pair record in net/minecraft/core, and in 26.3 nothing calls it; structure bounds are still BoundingBox in world/level/levelgen/structure. Worth knowing before assuming a rename happened.

Direction.getRotation treats UP as identity, and Direction.step returns a fresh JOML vector while Direction.getUnitVec3f returns the read- only shared one.

SectionPos has SectionPos.x as both an instance and a static method, instance and static, same name.

Not everything with Block in the name is in net/minecraft/core. BlockBox, BlockMath and Cursor3D are; BlockUtil is in net/minecraft/util (naming drift). BlockMath is model-rotation plumbing, not coordinates.

Where to look

Vec3i · BlockPos · ChunkPos · SectionPos · QuartPos · GlobalPos · Cursor3D · Vec3 · AABB · BoundingBox · Direction · Rotation · OctahedralGroup · VoxelShape · Shapes · CollisionContext · HitResult · ARGB · Mth · RandomSource · BitRandomSource · WorldgenRandom · PositionalRandomFactory · RandomSequences · Util


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Level data and rules

Verified against Minecraft 26.3 · Reference · Who owns the seed, the spawn, the rules, the border and the dimensions, where each is saved, and what tells the client — looked up, not watched.

The facts about a world that are not blocks or entities: its seed and dimensions, its spawn, its game time, its difficulty, its rules, its border, its scoreboard and maps. In 26.3 about half of them have left level.dat — PrimaryLevelData is a stub and everything else is a SavedData file under a data/ folder, one server-global and one per dimension — so the question this page answers is always the same one: which file remembers this, and who is allowed to change it. The table under who owns what is the page. Six sections follow it, one for each datum whose answer is longer than a row — what is left in level.dat, with how a save happens inside it, the two saved-data storages, the rules, the border, the dimensions and the seed, and difficulty and weather — and a row with no section below it is a row the table has already answered in full.

Five parts point here — III for what a boot reads, IV for what the world is made of, VIII for the two game rules that decide whether the server checks your movement, IX for the rule values a server sends a gamemaster’s client on request, and XII for the seed — and the level tick is where most of them are read.

Who owns what

In the saved as column, dimensions/…/ stands for dimensions/<namespace>/<path>/, the folder a dimension’s own id names; everything else is relative to the world folder.

datumownersaved astold to the client by
seed, structures, bonus chest, dimension listWorldGenSettings (MinecraftServer.getWorldGenSettings)data/minecraft/world_gen_settings.datthe obfuscated seed in CommonPlayerSpawnInfo, the dimension list in ClientboundLoginPacket; structures and the bonus chest, nothing
world spawnPrimaryLevelData.respawnDatalevel.datClientboundSetDefaultSpawnPositionPacket — the effective spawn below on join, respawn and dimension change, the stored one when it is set
game timePrimaryLevelData.gameTime (shared by every level)level.datClientboundSetTimePacket
day timeServerClockManagerdata/minecraft/world_clocks.datClientboundSetTimePacket
difficulty, lock, hardcoreLevelSettings.DifficultySettingslevel.datClientboundChangeDifficultyPacket — hardcore alone rides in ClientboundLoginPacket
game type, allow-commands, name, data packsLevelSettingslevel.datthe game type only as a new player’s default, through CommonPlayerSpawnInfo; the enabled features at the configuration phase; the name and allow-commands, nothing
game rulesGameRuleMap via GameRulesdata/minecraft/game_rules.datfive rules only, plus ClientboundGameRuleValuesPacket on request
weatherWeatherDatadata/minecraft/weather.datClientboundGameEventPacket
world borderWorldBorder, one per ServerLeveldimensions/…/data/minecraft/world_border.datClientboundInitializeBorderPacket and the five ClientboundSetBorder… packets
scoreboardServerScoreboard (MinecraftServer.scoreboard), buffered by ScoreboardSaveData at save timedata/minecraft/scoreboard.datClientboundSetObjectivePacket, ClientboundSetScorePacket, ClientboundSetPlayerTeamPacket
mapsMapItemSavedData per MapId, MapIndex for the counterdata/minecraft/maps/<n>.dat, data/minecraft/maps/last_id.datClientboundMapItemDataPacket
raidsRaids per leveldimensions/…/data/minecraft/raids.datboss bars
chunk ticketsTicketStorage per level (tickets)dimensions/…/data/minecraft/chunk_tickets.dat—
dragon fightEnderDragonFight, where DimensionType.hasEnderDragonFightdimensions/…/data/minecraft/ender_dragon_fight.datboss bars
boss bars, scheduled functions, random sequences, stopwatches, trader timers, command storageCustomBossEvents, TimerQueue, RandomSequences, Stopwatches, WanderingTraderData, CommandStoragedata/minecraft/<id>.dat, and one data/<namespace>/command_storage.dat per namespaceboss bars only
player dataPlayerDataStorageplayers/data/<uuid>.dat—

What is left in level.dat

LevelData is the read-only interface every Level exposes, and it is small: LevelData.getRespawnData, LevelData.getGameTime, LevelData.isHardcore, LevelData.getDifficulty and LevelData.isDifficultyLocked. No day time — that has left level data entirely for ServerClockManager (who owns the clock) — no weather, no rules, no border. LevelData.RespawnData is the world spawn as a GlobalPos with yaw and pitch — it carries a dimension, so /setworldspawn can point anywhere. WritableLevelData adds WritableLevelData.setSpawn; ServerLevelData adds eight more — the level name, the game type and its setter, ServerLevelData.isInitialized and its setter, ServerLevelData.isAllowCommands and its setter, and ServerLevelData.setGameTime; WorldData is the server-wide view (WorldData.getLevelSettings, WorldData.getDataConfiguration, WorldData.enabledFeatures, WorldData.wasModded, WorldData.getKnownServerBrands, WorldData.overworldData).

PrimaryLevelData implements all of them — the overworld’s level data and the whole server’s — with twelve fields: PrimaryLevelData.settings (a LevelSettings: name, GameType, a LevelSettings.DifficultySettings of difficulty, hardcore and lock, allow-commands, WorldDataConfiguration), PrimaryLevelData.respawnData, PrimaryLevelData.gameTime, PrimaryLevelData.initialized, PrimaryLevelData.knownServerBrands, PrimaryLevelData.wasModded, PrimaryLevelData.removedFeatureFlags, PrimaryLevelData.singlePlayerUUID, PrimaryLevelData.version, PrimaryLevelData.specialWorldProperty (PrimaryLevelData.SpecialWorldProperty.FLAT / PrimaryLevelData.SpecialWorldProperty.DEBUG / none), PrimaryLevelData.versionHistory (every data version that has saved the world, written as version_history), and PrimaryLevelData.worldGenSettingsLifecycle, the DFU Lifecycle that makes the world-select screen warn about experimental settings. That is the whole stub: seed, dimensions, rules, border, weather, dragon fight, boss bars, scheduled events and trader timers are all SavedData files. PrimaryLevelData.createTag builds the payload — flat, no wrapper — and it is LevelStorageSource.LevelStorageAccess.saveDataTag that nests it under Data; PrimaryLevelData.parse reads it back.

DerivedLevelData is what every other ServerLevel gets, built in MinecraftServer.createLevels: spawn, game time and initialised state forwarded to the overworld’s data, and everything the whole server shares — name, game type, hardcore, allow-commands, difficulty and its lock — read straight off the WorldData. Only DerivedLevelData.setSpawn writes anything; DerivedLevelData.setGameTime, DerivedLevelData.setGameType, DerivedLevelData.setAllowCommands and DerivedLevelData.setInitialized are no-ops. That forwarding is how game time comes to be shared by every level.

The file is written by LevelStorageSource.LevelStorageAccess.saveDataTag → LevelStorageSource.LevelStorageAccess.saveLevelData: a temp file, then Util.safeReplaceFile renames the old level.dat to level.dat_old and the temp into place, ten retries per step with a rollback.

One method saves all three

Nothing in the table above saves itself, and but for player data MinecraftServer.saveAllChunks is the single call behind every row. It does four things in order: it flushes the live ServerScoreboard into its ScoreboardSaveData (which is what the scoreboard row means by buffered at save time — the scoreboard is a live object all tick and becomes saved data only here), saves every level’s chunks, writes level.dat through LevelStorageSource.LevelStorageAccess.saveDataTag, and then either joins the saved-data storage or schedules it. So the rows with no section below them — day time, the scoreboard, maps, raids, chunk tickets, the dragon fight and the boss-bar row’s six owners — are saved by being SavedData, and that one flush is the only special case among them. Player data is the other road: PlayerDataStorage is not SavedData, and PlayerList.save writes a player’s file, both from PlayerList.saveAll and whenever the player leaves. MinecraftServer.saveEverything wraps MinecraftServer.saveAllChunks with PlayerList.saveAll in front, and that is the pair autosave and /save-all reach; shutdown calls the two itself.

LevelSummary — the world-select row — is read without opening the world at all, by LevelStorageSource.readLevelSummary, and it is where the numbers on that screen come from: a LevelVersion (LevelVersion.levelDataVersion, LevelVersion.lastPlayed, LevelVersion.minecraftVersionName, LevelVersion.minecraftVersion — the DataVersion the needs upgrading and from a newer version warnings compare — and LevelVersion.snapshot) plus the LevelSettings. Two subclasses stand for worlds it could not read: LevelSummary.CorruptedLevelSummary, and LevelSummary.SymlinkLevelSummary for a world whose level.dat is a link to a place the allow list refuses.

Every path a world folder has a name for

LevelResource names thirteen paths under a world folder, and the per-player files are three separate ones rather than one player data row:

LevelResourcethe pathwhat is in it
LevelResource.LEVEL_DATA_FILElevel.datwhat this page’s table calls level data
LevelResource.OLD_LEVEL_DATA_FILElevel.dat_oldthe previous one, kept by the rename dance above
LevelResource.LOCK_FILEsession.lockheld for as long as the world is open (starting a server)
LevelResource.DATAdata/the server-global SavedData files: the scoreboard, the game rules, the world-gen settings, the boss bars
LevelResource.PLAYER_DATA_DIRplayers/data/one .dat per player, read twice on join (players and sessions)
LevelResource.PLAYER_ADVANCEMENTS_DIRplayers/advancements/one JSON per player, the progress half of every advancement (advancements)
LevelResource.PLAYER_STATS_DIRplayers/stats/one JSON per player, and every statistic is also a scoreboard criterion (scores, teams and stored data)
LevelResource.PLAYER_OLD_DATA_DIRplayers/the parent of the three, and where the oldest worlds’ name-keyed player files sit until a dedicated server’s boot moves them, through OldUsersConverter, into players/data/
LevelResource.ROOT · LevelResource.ICON_FILE · LevelResource.GENERATED_DIR · LevelResource.DATAPACK_DIR · LevelResource.MAP_RESOURCE_FILE. , icon.png, generated/, datapacks/, resourcepacks/resources.zipthe world folder itself, its world-select thumbnail, what a structure block saves (jigsaw and templates), the world’s own packs, and the world’s own resource pack, which the client loads when it opens the world

Thirteen constants, and nothing else in a world folder has a name here: the dimensions/ tree is named by DimensionType.getStorageFolder, and the region files inside it by ChunkPos arithmetic (chunk storage).

The spawn every level reports is the server’s, not each level’s

ServerLevel.getRespawnData forwards to MinecraftServer.getRespawnData, which returns MinecraftServer.effectiveRespawnData — recomputed by MinecraftServer.updateEffectiveRespawnData through Level.getWorldBorderAdjustedRespawnData, which relocates a spawn that has fallen outside the border to the border centre’s surface, and by MinecraftServer.findRespawnDimension, which falls back to the overworld when the stored dimension no longer exists. So every level reports the same spawn, and it need not be the one level.dat holds: it is the stored one wherever that is inside the border it is tested against — the overworld’s, when the stored dimension no longer exists — and a relocated one where it is not.

Two saved-data storages, neither of them the overworld’s

MinecraftServer.savedDataStorage (MinecraftServer.getDataStorage) is server-global at <world>/data/; maps, scoreboard, rules, weather and clocks are server-wide. ServerChunkCache.savedDataStorage (ServerChunkCache.getDataStorage, forwarded by ServerLevel.getDataStorage) is per dimension at dimensions/<namespace>/<path>/data/ — the overworld included (chunk storage); raids, tickets, the border and the dragon fight are per dimension. The overworld’s per-dimension files sit under dimensions/ like everyone else’s, so neither storage is “the overworld’s”: one is the server’s and one is each dimension’s.

What a saved-data file is

SavedData is one flag, SavedData.dirty (SavedData.setDirty); a SavedDataType is an id, a constructor, a Codec and a DataFixTypes. SavedDataStorage caches them per folder (SavedDataStorage.computeIfAbsent, SavedDataStorage.get, SavedDataStorage.set) and writes each as { data, DataVersion }, gzip-compressed. How a dirty entry reaches the disk — encoded straight from the live object on the caller’s thread, with no copy first, written on the IO pool, and joined by a flush save, shutdown or the world upgrader — is half the bargain a chunk makes, and chunk storage is where that bargain is explained.

The id is an Identifier, so every saved-data file lives under a namespace folder — the path is data/<namespace>/<path>.dat — and vanilla’s are all under data/minecraft/. Command storage is the one place the namespace is not minecraft: each gets its own data/<namespace>/command_storage.dat.

Game rules are a registry

GameRule is a registry entry in Registries.GAME_RULE / BuiltInRegistries.GAME_RULE, bootstrapped by GameRules.bootstrap: GameRule.category (a GameRuleCategory — GameRuleCategory.PLAYER, GameRuleCategory.MOBS, GameRuleCategory.SPAWNING, GameRuleCategory.DROPS, GameRuleCategory.UPDATES, GameRuleCategory.CHAT, GameRuleCategory.MISC), GameRule.gameRuleType (GameRuleType.INT or GameRuleType.BOOL), GameRule.argument (a Brigadier type), GameRule.valueCodec, GameRule.defaultValue and GameRule.requiredFeatures. Ids are snake_case and namespaceable — GameRules.ADVANCE_TIME, GameRules.SPAWN_MOBS, GameRules.SPAWN_MONSTERS, GameRules.KEEP_INVENTORY, GameRules.RANDOM_TICK_SPEED, GameRules.PLAYERS_SLEEPING_PERCENTAGE, GameRules.RESPAWN_RADIUS, GameRules.MAX_ENTITY_CRAMMING, GameRules.MAX_SNOW_ACCUMULATION_HEIGHT, GameRules.MAX_MINECART_SPEED (feature-gated) … fifty-nine of them, with their defaults, in the generated table; the old names and the nested value classes have rows in naming drift.

The values are saved data, not level data: a GameRuleMap — SavedData, game_rules.dat, server-global — wrapped by the GameRules instance in MinecraftServer.gameRules. ServerLevel.getGameRules returns the server’s: one set for every dimension, and Level has no rules accessor at all, so no ClientLevel can read a rule — the client’s GameRules objects belong to the two AbstractGameRulesScreens, WorldCreationGameRulesScreen and InWorldGameRulesScreen, and to WorldCreationUiState, whose rules become the new world’s when it is created. The accessors are GameRules.get, GameRules.set (which calls MinecraftServer.onGameRuleChanged) and GameRules.visitGameRuleTypes (how GameRuleCommand.register builds two literals per rule — the bare id and the namespaced one).

What the client hears

Five rules are pushed, and the rest reach a client only when a gamemaster asks — as data through the editor below, or as the chat feedback of /gamerule. All three of GameRules.REDUCED_DEBUG_INFO, GameRules.LIMITED_CRAFTING and GameRules.IMMEDIATE_RESPAWN ride in ClientboundLoginPacket at join (the last inverted, as showDeathScreen); a change afterwards goes as a ClientboundEntityEventPacket for the first and a ClientboundGameEventPacket for the other two (ClientboundGameEventPacket.LIMITED_CRAFTING, ClientboundGameEventPacket.IMMEDIATE_RESPAWN); GameRules.LOCATOR_BAR through ServerWaypointManager; and GameRules.ADVANCE_TIME, which broadcasts a full clock sync because a paused clock is expressed on the wire as rate 0 (environment attributes). One mechanism in this area sends the client nothing at all: MinecraftServer.updateMobSpawningFlags, which pushes a flag down the server’s own chain instead (below).

There is an in-game editor: ServerboundClientCommandPacket.Action.REQUEST_GAMERULE_VALUES → ServerGamePacketListenerImpl.sendGameRuleValues → ClientboundGameRuleValuesPacket → InWorldGameRulesScreen, and edits back as ServerboundSetGameRulePacket → ServerGamePacketListenerImpl.handleSetGameRule (gated on Permissions.COMMANDS_GAMEMASTER).

The border is per dimension

The border has no lecture. It is the one mechanism in Part IV’s packages whose home is this page: it sits on no conveyor, it belongs to none of the four side-systems, and what a reader needs of it is a set of numbers, a pair of extents and a list of packets — which is what a Reference page is for. Part IV’s landing page declares it as such.

WorldBorder is SavedData — world_border.dat, per dimension, fetched by ServerLevel.getWorldBorder through the cache on every call. Nothing scales the Nether’s border by DimensionType.coordinateScale — each dimension’s file has its own values, so the Nether has the same numbers unless someone sets them.

WorldBorder.settings (WorldBorder.Settings: centre, damage per block, safe zone, warning blocks and time, size, lerp time and target; WorldBorder.Settings.DEFAULT is 0,0 / 0.2 / 5 / 5 / 300 / WorldBorder.MAX_SIZE) is the loaded snapshot, never written again; WorldBorder.applyInitialSettings pushes it into the live fields once, resuming a lerp in progress from where the save left it, and saving reads the live fields back out. The live defaults are not the persisted ones — a fresh WorldBorder starts with a warning time of 15, not 300.

The live extent is a WorldBorder.BorderExtent — WorldBorder.StaticBorderExtent or WorldBorder.MovingBorderExtent, which WorldBorder.tick advances (the level tick). A moving border marks itself for saving every tick: WorldBorder.MovingBorderExtent marks the saved data dirty on every advance, and a stationary one never does. WorldBorder.MAX_SIZE is 59,999,968; MinecraftServer.getAbsoluteMaxWorldSize is applied to every level’s border in MinecraftServer.createLevels — 29,999,984, which is MinecraftServer’s own answer and so the integrated server’s, while DedicatedServer overrides it with max-world-size. WorldBorder.isWithinBounds, WorldBorder.clampToBounds, WorldBorder.getDistanceToBorder, WorldBorder.getCollisionShape are the readers; BorderStatus colours the client’s wall.

For sync, PlayerList.addWorldborderListener registers a BorderChangeListener per level that broadcasts, dimension-scoped, ClientboundSetBorderSizePacket, ClientboundSetBorderLerpSizePacket, ClientboundSetBorderCenterPacket, ClientboundSetBorderWarningDelayPacket and ClientboundSetBorderWarningDistancePacket; PlayerList.sendLevelInfo sends ClientboundInitializeBorderPacket on join and dimension change. ClientLevel.worldBorder is a plain WorldBorder ticked in ClientLevel.tick.

Dimensions and the seed

DimensionType is a record of sixteen components, and they fall into four kinds:

kindcomponents
the shape of the spaceDimensionType.minY, DimensionType.height, DimensionType.logicalHeight, DimensionType.coordinateScalehow tall, how deep, how high a portal or a chorus fruit may reach, and what a coordinate becomes when you step through a portal
what the sky doesDimensionType.hasSkyLight, DimensionType.hasCeiling, DimensionType.hasFixedTime, DimensionType.ambientLight, DimensionType.skybox (a DimensionType.Skybox), DimensionType.cardinalLightTypethe six that decide whether there is daylight, a roof over it, whether it ever counts as day or night, and what is drawn where the blocks stop
what may live hereDimensionType.monsterSettings, DimensionType.infiniburn, DimensionType.hasEnderDragonFightthe two monster-spawn light tests (DimensionType.MonsterSettings.monsterSpawnLightTest and DimensionType.MonsterSettings.monsterSpawnBlockLightLimit), the block tag that burns for ever, and the dragon fight as a flag rather than hard-wired to Level.END
the three that are a system of their ownDimensionType.attributes (an EnvironmentAttributeMap), DimensionType.timelines, DimensionType.defaultClock (a WorldClock holder; WorldClocks.OVERWORLD, WorldClocks.THE_END)where the gameplay booleans a 1.21-era reader will look for on this record have gone (the stack a value falls through); naming drift has the row for each

DimensionType.getStorageFolder names the on-disk folder. Defaults are DimensionDefaults (DimensionDefaults.OVERWORLD_MIN_Y −64, DimensionDefaults.OVERWORLD_LEVEL_HEIGHT 384, DimensionDefaults.NETHER_LOGICAL_HEIGHT 128); the built-in keys are BuiltinDimensionTypes.OVERWORLD, BuiltinDimensionTypes.NETHER, BuiltinDimensionTypes.END, BuiltinDimensionTypes.OVERWORLD_CAVES.

LevelStem is a DimensionType holder plus a ChunkGenerator (LevelStem.OVERWORLD, LevelStem.NETHER, LevelStem.END). Registries.LEVEL_STEM and Registries.DIMENSION share the id dimension (identifiers and registries); Level.OVERWORLD, Level.NETHER, Level.END are ResourceKeys under the latter, and Level.dimension / Level.dimensionType are the accessors. Level.canHaveWeather is sky light, no ceiling, not the End.

The seed and the dimension list are WorldGenSettings — SavedData, world_gen_settings.dat, server-global, and written not at world creation but by the server’s first save, the MinecraftServer constructor having pushed it into the storage, as it does on every boot (creating a world is where it comes from) — holding WorldOptions (WorldOptions.seed, WorldOptions.generateStructures, WorldOptions.generateBonusChest) and WorldDimensions (the stem map; WorldDimensions.bake produces the registry). It is read before the server exists by LevelStorageSource.getLevelDataAndDimensions and pushed in with SavedDataStorage.set; if the file is missing or unreadable the loader logs an error and substitutes a whole default WorldGenSettings — a random seed, structures on, no bonus chest, and the data packs’ dimension list in place of the saved one — then carries on loading the world. MinecraftServer.createLevels makes one ServerLevel per stem; MinecraftServer.levelKeys go out in ClientboundLoginPacket, the dimension type via registry sync, and the biome-zoom-obfuscated seed in CommonPlayerSpawnInfo.

Difficulty and weather

Difficulty (Difficulty.PEACEFUL … Difficulty.HARD) sits in LevelSettings.DifficultySettings. MinecraftServer.setDifficulty writes it, then calls MinecraftServer.updateMobSpawningFlags and MinecraftServer.sendDifficultyUpdate → ClientboundChangeDifficultyPacket.

Peaceful does not turn the spawner off. The flag MinecraftServer.updateMobSpawningFlags pushes down — Level.setSpawnSettings, forwarded to ServerChunkCache.setSpawnSettings, one boolean per level — is ServerLevel.isSpawningMonsters, and that reads two game rules, GameRules.SPAWN_MOBS and GameRules.SPAWN_MONSTERS, and never the difficulty. Difficulty is spent on the mobs themselves instead: EntityType.isAllowedInPeaceful gates each spawn attempt, and Mob.checkDespawn discards a mob that is not allowed in peaceful on its next despawn check. So the two paths are independent — GameRules.SPAWN_MOBS empties the spawner and GameRules.SPAWN_MONSTERS its hostile half, while peaceful empties the world — and MinecraftServer.setDifficulty calls MinecraftServer.updateMobSpawningFlags only because a rule may have changed under it since the last call. DedicatedServer.forceDifficulty applies server.properties at boot with the lock ignored; there is no getForcedDifficulty. DifficultyInstance — local difficulty — is built by ServerLevel.getCurrentDifficultyAt from ChunkAccess.getInhabitedTime, Level.getOverworldClockTime and the moon phase (an environment attribute, EnvironmentAttributes.MOON_PHASE, indexed into DimensionType.MOON_BRIGHTNESS_PER_PHASE — the one piece of the old moon logic still on DimensionType; see environment attributes). What reads it is Part VI: every Mob.finalizeSpawn is handed one (entity lifecycle), which is why a zombie that can break doors is a roll made once, at spawn, in the chunk you have been standing in (AI).

WeatherData — server-global SavedData, MinecraftServer.getWeatherData — was covered in the level tick. PrimaryLevelData stores no rain fields.

Where to look

PrimaryLevelData.setTagData · DerivedLevelData · LevelSettings · LevelStorageSource.getLevelDataAndDimensions · LevelStorageSource.LevelStorageAccess.saveDataTag · SavedDataStorage.scheduleSave · SavedDataType · MinecraftServer.createLevels · MinecraftServer.getDataStorage · ServerChunkCache.getDataStorage · GameRules.bootstrap · GameRules.set · MinecraftServer.onGameRuleChanged · GameRuleMap · GameRuleCommand.register · WorldBorder.applyInitialSettings · WorldBorder.tick · PlayerList.addWorldborderListener · DimensionType · LevelStem · WorldGenSettings · WorldDimensions.bake · MinecraftServer.setDifficulty · ServerLevel.getCurrentDifficultyAt


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Naming drift

Verified against Minecraft 26.3 · Reference · The translation layer: the names a 1.21-era reader will reach for that 26.3 dropped, moved or repurposed, and what it is called now.

Rule three of this corpus is newest version only: no page says “in 1.21 this was…”, because version-difference prose is the first thing to rot and the last thing anyone rereads. That rule costs something, and this page is where the cost is paid back once. Every page assumes you are reading the 26.3 tree; this page assumes you are not, yet, and are still typing the names you learned somewhere else.

Two audiences. A reader coming from 1.21 — the version most public writing, most tutorials and most model weights are anchored to — needs the version tables: the old name on the left, what to grep for on the right. A reader coming from Yarn (Fabric’s community mappings) needs the last table: not a version difference at all, just a different name for the same class in the same version.

The one sentence: if a name in your head does not appear in the tree, it is probably on this page.

How to read the tables

The left column is italic, not backticked. Most of these names do not exist in 26.3, and tools/verify_names.py — which checks every backticked identifier on every page against the decompile — would reject the page if they were. Italics is the corpus’s mark for a name, but not a 26.3 name. The right column’s class and member names are backticked and therefore verified: those names are in the tree; its italics are file paths, save-file keys and old names. The left column is a name a 1.21-era reader may remember, and 1.21.11 — the last of them, and the only one this project can open — is the one the rows are checked against: most rows record drift that had already happened by 1.21.11, so a reader coming from 1.21.11 will find those left names missing from their own tree too.

“gone” in the right column means no class replaced the name one for one: the responsibility moved into something structurally different or was dropped, and the entry names where it went when it went somewhere. Those are the interesting rows — a rename is a nuisance, a disappearance is a design change, and the page named beside each part explains it.

The rows are the names the corpus’s own pages needed, so the table is not exhaustive — not even over the corpus: a few names the For a 1.21-era reader notes use, such as ItemBlockRenderTypes and LootItemFunctionType, have no row. Three hundred and nine rows, and the distribution is itself a finding: the three biggest tables are commands (41), rendering (39) and items (38), and the fourth is the server (31). The client was rewritten around extract-then-render, which is why almost nothing at the top of the render stack kept its name — but the permission rewrite and the game-rule registry moved more names than the renderer did.

The four you will hit in the first ten minutes

Identifier is ResourceLocation. Lightmap is LightTexture. DeltaTracker is Timer, and partialTick is now a DeltaTracker.Timer you ask rather than a float you are handed. And Gui no longer means the HUD: the HUD is Hud, held as Gui.hud, while Gui is the screen and overlay manager that also owns Gui.screen and Gui.setScreen — so a 1.21-era Minecraft.setScreen call site is now on Gui. Both Gui and Hud exist, which is the single most confusing pair of names in the tree.

The tables

Everywhere

the name you remember26.3
ResourceLocationIdentifier
LightTextureLightmap
TimerDeltaTracker

Part II — Foundations

the name you remember26.3
TagManagergone
Minecraft.reloadResourcesMinecraft.reloadResourcePacks
ItemStack.save / parseValueOutput / ItemStack.CODEC
ChunkPos.asLongChunkPos.pack / ChunkPos.unpack (a record)
CompositePackResourcesOverlayedPackResources
VanillaPackResources as a pack, VanillaPackResources.asProvidera holder of FixedPathPackResources layers, VanillaPackResources.asResourceManager
SimpleJsonResourceReloadListener.scanDirectorygone — data-pack registry files are read by RegistryDataLoader
RegistryDataLoader.WORLDGEN_REGISTRIES / RegistryLayer.WORLDGENRegistryDataLoader.WORLD_REGISTRIES / RegistryLayer.WORLD, beside a fourth list, RegistryDataLoader.RELOADABLE_REGISTRIES
RegistryCodecs.homogeneousListRegistryCodecs.holderSet
HolderOwner.canSerializeInHolderOwner.canSerialize, asked of the context rather than the holder
BuiltInRegistries.BLOCK_TYPE / BlockTypes / Block.codecgone — a block has no codec
PlacementModifierType / StructurePlacementType / BlockStateProviderTypegone — BuiltInRegistries.PLACEMENT_MODIFIER_TYPE, BuiltInRegistries.STRUCTURE_PLACEMENT and BuiltInRegistries.BLOCK_STATE_PROVIDER_TYPE hold the MapCodecs

Part III — The server

the name you remember26.3
DO_DAYLIGHT_CYCLEGameRules.ADVANCE_TIME
DO_MOB_SPAWNINGGameRules.SPAWN_MOBS
DO_WEATHER_CYCLEGameRules.ADVANCE_WEATHER
GameRules packageworld/level/gamerules
GameRules.Key<T> / GameRules.Value / BooleanValue / IntegerValue / GameRules.Type (all nested)top-level GameRule, with GameRuleType, GameRuleTypeVisitor, GameRuleMap for the values and GameRuleCategory for the grouping
game rules as a hard-coded mapa registry — Registries.GAME_RULE / BuiltInRegistries.GAME_RULE, bootstrapped by GameRules
level.dat field GameRules, ids camelCase and unnamespacedfield game_rules, ids namespaced (minecraft:advance_time) — the whole rename table is GameRuleRegistryFix
doEntityDropsGameRules.ENTITY_DROPS
doImmediateRespawnGameRules.IMMEDIATE_RESPAWN
doInsomniaGameRules.SPAWN_PHANTOMS
doLimitedCraftingGameRules.LIMITED_CRAFTING — the name survives elsewhere, as a component of ClientboundLoginPacket and a field on LocalPlayer
doPatrolSpawning / doTraderSpawning / doWardenSpawningGameRules.SPAWN_PATROLS / GameRules.SPAWN_WANDERING_TRADERS / GameRules.SPAWN_WARDENS
doVinesSpreadGameRules.SPREAD_VINES
enableCommandBlocks and commandBlocksEnabledone rule, GameRules.COMMAND_BLOCKS_WORK
spawnerBlocksEnabledGameRules.SPAWNER_BLOCKS_WORK
commandModificationBlockLimitGameRules.MAX_BLOCK_MODIFICATIONS
minecartMaxSpeedGameRules.MAX_MINECART_SPEED
snowAccumulationHeightGameRules.MAX_SNOW_ACCUMULATION_HEIGHT
spawnRadiusGameRules.RESPAWN_RADIUS
disableElytraMovementCheckGameRules.ELYTRA_MOVEMENT_CHECK — inverted
disablePlayerMovementCheckGameRules.PLAYER_MOVEMENT_CHECK — inverted
disableRaidsGameRules.RAIDS — inverted
doFireTick + allowFireTicksAwayFromPlayer (two booleans)one integer, GameRules.FIRE_SPREAD_RADIUS_AROUND_PLAYER (0 none, 128 near players only, −1 everywhere)
spawnChunkRadius, entitiesWithPassengersCanUsePortals, gameLoopFunctiongone with no replacement — the fix simply deletes them
day time on ServerLevelServerClockManager (world/clock)
weather on the overworld’s level dataserver-global WeatherData
GameProfile on the player listsNameAndId (a record of UUID and name) — PlayerList.canPlayerLogin, PlayerList.isWhiteListed, PlayerList.op, the ban/op/whitelist files
ServerPlayer.sendAllPlayerInfo / sendActivePlayerEffectsPlayerList.sendAllPlayerInfo / PlayerList.sendActivePlayerEffects
ServerLevelData.getScheduledEvents, the overworld’s level data holding the queueMinecraftServer.getScheduledEvents, returning a server-wide TimerQueue saved data, advanced only by the overworld’s ServerLevel.tickTime
ServerLevel.updateSkyBrightness reading day timethe method survives, declared on Level, and now reads EnvironmentAttributes.SKY_LIGHT_LEVEL through EnvironmentAttributeSystem
ChunkMap.forEachBlockTickingChunk meaning block-tickingit walks the entity-ticking set; the name did not follow the split

Part IV — The world

the name you remember26.3
ChunkStoragegone — ChunkMap extends SimpleRegionStorage
ChunkSerializerSerializableChunkData, a record with its own parse and write halves — and a blending_data component
DimensionDataStorageSavedDataStorage (one for the server, one per level)
getLightBlockBlockBehaviour.BlockStateBase.getLightDampening
PalettedContainer.Strategytop-level Strategy + Configuration
ForcedChunksSavedDataTicketStorage
TicketType<T>a registry record with flag bits
DimensionType booleansEnvironmentAttributeMap
DimensionType.ultraWarmattributes the Nether sets, EnvironmentAttributes.FAST_LAVA and EnvironmentAttributes.WATER_EVAPORATES chief among them
DimensionType.piglinSafeEnvironmentAttributes.PIGLINS_ZOMBIFY — inverted
DimensionType.bedWorksEnvironmentAttributes.BED_RULE, a BedRule record, not a boolean
DimensionType.hasRaidsEnvironmentAttributes.CAN_START_RAID
DimensionType.naturalEnvironmentAttributes.NETHER_PORTAL_SPAWNS_PIGLINS and neighbours
DimensionType.fixedTimeDimensionType.hasFixedTime, a bare boolean — the time itself moved to WorldClock and Timelines.OVERWORLD_DAY
DimensionType.ambientLightunchanged; one of the three visual fields that did not become an attribute, with DimensionType.skybox and DimensionType.cardinalLightType
Schedule (the villager’s)EnvironmentAttributes.VILLAGER_ACTIVITY on Timelines.VILLAGER_SCHEDULE
Level.getDayTime / ServerLevel.setDayTimeServerClockManager, keyed by WorldClock
data/<id>.datdata/<namespace>/<path>.dat — every saved-data file gained a namespace folder
ChunkStatus.NOISE / SURFACE / CARVERSChunkStatus.TERRAIN, one task doing all three
ProtoChunk.carvingMask (saved with the chunk)gone — the CarvingMask lives for one terrain run
BiomeManager.NoiseBiomeSourceBiomeResolver, which ChunkAccess implements

Part V — Blocks

the name you remember26.3
ItemInteractionResultgone — InteractionResult.TryEmptyHandInteraction
DirectionPropertygone — EnumProperty<Direction>
Level.markAndNotifyBlockgone — inline in Level.setBlock
BlockBehaviour.onRemoveBlockBehaviour.affectNeighborsAfterRemoval + BlockEntity.preRemoveSideEffects
doTileDropsGameRules.BLOCK_DROPS
BlockModelShaperBlockStateModelSet / BlockModelSet
RenderShape.ENTITYBLOCK_ANIMATEDgone — RenderShape.INVISIBLE / RenderShape.MODEL only
Player.canInteractWithBlockPlayer.isWithinBlockInteractionRange
Block.rebuildCachegone — BlockBehaviour.BlockStateBase.initCache from the Blocks static init
Material (the block property)gone — individual BlockBehaviour.Properties flags. The name survives on a different class; see survived and changed meaning below
BlockEntity.saveToItemBlockItem.setBlockEntityData + BlockEntity.collectComponents
MobEffects.DIG_SPEED / DIG_SLOWDOWNMobEffects.HASTE / MobEffects.MINING_FATIGUE
RedStoneWireBlockRedstoneWireBlock; its connection rule is each block’s BlockBehaviour.shouldRedstoneWireConnectTo
BedBlock.OCCUPIED / BedBlock.PARTAbstractBedBlock.OCCUPIED / AbstractBedBlock.PART
LevelEvent.PARTICLES_DESTROY_BLOCK as the break event (2001)LevelEvent.PARTICLES_AND_SOUND_DESTROY_BLOCK; the old name is now 2014, particles only

Part VI — Entities

the name you remember26.3
Player extends LivingEntityPlayer extends Avatar extends LivingEntity
EntityType.PIG (constants)EntityTypes.PIG + EntityTypeIds.PIG
BlockUtil in coreBlockUtil, in net/minecraft/util — the name did not move, the package did
MobSpawnTypeEntitySpawnReason (+ EntitySpawnRequest)
SpawnPlacements.TypeSpawnPlacementType / SpawnPlacementTypes
Entity.hurt(DamageSource, float) returning a booleansplit into Entity.hurtServer and Entity.hurtClient. Both old shapes survive as deprecated finals — Entity.hurt delegating to the server half, Entity.hurtOrSimulate as the boolean-returning successor — so grep still finds the name
doMobLootGameRules.MOB_DROPS
LivingEntity.isDamageSourceBlockedgone — DataComponents.BLOCKS_ATTACKS
Schedule / ScheduleBuildergone — Timeline + EnvironmentAttribute
BlockPathTypesPathType
LivingEntity.brainProviderLivingEntity.makeBrain(Brain.Packed)
Entity.moveTo / absMoveToEntity.snapTo / Entity.absSnapTo
Entity.maxUpStep (field)Attributes.STEP_HEIGHT
Entity.updateFluidHeightAndDoFluidPushingEntityFluidInteraction
Entity.lerpToEntity.moveOrInterpolateTo + InterpolationHandler
EntityDataSerializers.OPTIONAL_UUID / COMPOUND_TAGgone
UUID-keyed AttributeModifierIdentifier-keyed record
AttributeMap.getDirtyAttributesAttributeMap.getAttributesToSync + AttributeMap.getAttributesToUpdate
PlayerRendererAvatarRenderer (serves players and mannequins, keyed by skin model)
Entity.invulnerableTime as the hurt cooldownLivingEntity.damageCooldownTime; Entity.invulnerableTime is a separate full-invulnerability timer
Entity.hurtMarkedEntity.syncVelocity
TryFindWaterGoalTryFindLiquidGoal

Part VII — Items and inventories

the name you remember26.3
InteractionResultHoldergone — InteractionResult.Success.heldItemTransformedTo
UseAnimItemUseAnimation
Item.getFoodPropertiesDataComponents.FOOD on the stack
ItemStack.getTag / getOrCreateTaggone — components
LivingEntity.triggerItemUseEffectsConsumable.emitParticlesAndSounds
FoodProperties effects listConsumable.onConsumeEffects
ClickTypeContainerInput
MultiPlayerGameMode.handleInventoryMouseClickMultiPlayerGameMode.handleContainerInput
ClientboundSetCarriedItemPacketsplit: ClientboundSetCursorItemPacket + ClientboundSetHeldSlotPacket
ClientboundSetSlotPacketClientboundContainerSetSlotPacket
ClientboundHorseScreenOpenPacketClientboundMountScreenOpenPacket
Container.startOpen(Player)Container.startOpen(ContainerUser)
Recipe.getResultItem / getIngredientsRecipe.assemble / PlacementInfo. The first is gone outright; the second is off the Recipe interface and survives only as a test-visible method on ShapedRecipe
Ingredient.EMPTYgone — Ingredient.CODEC rejects an empty literal list, but a tag that resolves to nothing still yields an empty one, hence Ingredient.isEmpty
ClientboundUpdateRecipesPacket carrying recipesproperty sets + the stonecutter input set; the book gets RecipeDisplayEntrys
net.minecraft.advancements.CriteriaTriggersCriteriaTriggers, moved to net/minecraft/advancements/triggers
Player.permissionLevel / hasPermissions(int)Player.permissions → a PermissionSet, queried by named Permissions keys
ServerboundPlayerCommandPacket.Action.PRESS_SHIFT_KEY / RELEASE_SHIFT_KEYgone — sneak rides ServerboundPlayerInputPacket → Entity.setShiftKeyDown
Mannequin on the clientClientMannequin, installed by swapping the mutable Mannequin.constructor factory at client startup
data/<ns>/recipes/data/<ns>/recipe/ (singular)
EnchantmentCategoryEnchantment.EnchantmentDefinition item sets
Enchantment.getDamageBonus, EnchantmentHelper.getFireAspect…gone — EnchantmentEffectComponents
EnchantedBookItemgone — DataComponents.STORED_ENCHANTMENTS
Item.getEnchantmentValueDataComponents.ENCHANTABLE
LootContextParam / LootContextParamSetContextKey / ContextKeySet (util/context)
LootDataManager / LootTablesReloadableServerRegistries + BuiltInLootTables
LootTableReferenceNestedLootTable
LootingEnchantFunctionEnchantedCountIncreaseFunction
SetCountFunctionSetItemCountFunction
LootContextParams.KILLER_ENTITYLootContextParams.ATTACKING_ENTITY
AxeItem / ShovelItem / HoeItemgone — DataComponents.BLOCK_TRANSFORMER on a plain Item
RecipeManager as a reload listenerRegistries.RECIPE, read by RegistryDataLoader; the RecipeManager constructor builds the RecipeMap
LootPoolSingletonContainerUniformContainerBase
LootItemFunctions.compose, lists of functions and conditionsone optional modifier per level, and one optional condition on a pool, an entry or a conditional function; several functions are one SequenceFunction
LootItemConditionsLootItemConditionTypes
NumberProvidersplit in two: ContextFloatProvider and ContextIntProvider
ContextAwarePredicate / ConditionReferencegone — a Holder of a LootItemCondition
LootContext.getParameter / getOptionalParameterLootContext.getOptional

Part VIII — The player

the name you remember26.3
Inventory.armor / offhand / compartmentsone 36-slot Inventory.items, and the armour and offhand in the entity’s EntityEquipment through Inventory.EQUIPMENT_SLOT_MAPPING
Inventory.setPickedItemInventory.addAndPickItem / Inventory.pickSlot
Entity.moveToEntity.absSnapTo / Entity.snapTo
GameRenderer.pickMinecraft.pick → LocalPlayer.raycastHitResult
ServerboundInteractPacket.ActionType.ATTACKServerboundAttackPacket (a record of one int)
GameRules.NATURAL_REGENERATIONGameRules.NATURAL_HEALTH_REGENERATION
isCritArrow / Player.sweepAttackPlayer.canCriticalAttack / Player.isSweepAttack + Player.doSweepAttack, all three private. isCritArrow was never a Player method and is still live on AbstractArrow
LivingEntity.eat / Player.eatgone — Consumable.onConsume → FoodProperties → FoodData.eat
MobEffect.createModifierMobEffect.createModifiers (plural)
ServerboundSwingPacketServerboundPunchPacket up, with no fields; ClientboundSwingAnimationPacket down
ServerGamePacketListenerImpl.handleAnimateServerGamePacketListenerImpl.handlePunch
LocalPlayer.swinggone — Minecraft.startAttack and Minecraft.continueAttack send the punch
LivingEntity.swinging / swingingArm / swingTime / attackAnimLivingEntity.SwingState
ItemStack.getSwingAnimation, DataComponents.SWING_ANIMATIONItemStack.getAttackAnimation + ItemStack.getInteractAnimation, DataComponents.ATTACK_ANIMATION + DataComponents.INTERACT_ANIMATION

Part IX — Networking

the name you remember26.3
Connection.setListener / setProtocol / getCurrentProtocolgone — Connection.setupInboundProtocol / Connection.setupOutboundProtocol
ConnectionProtocol.getById / packet tablesgone — a bare enum; ids are ProtocolInfoBuilder.addPacket order in IdDispatchCodec
Connection.NETWORK_WORKER_GROUP etc.EventLoopGroupHolder (in server/network)
MemoryConnectiongone — Connection.isMemoryConnection
ensureRunningOnSameThread(…, BlockableEventLoop)PacketUtils.ensureRunningOnSameThread with a PacketProcessor
Packet.write(FriendlyByteBuf)gone — a per-packet StreamCodec constant the protocol reads
ClientboundAddPlayerPacket / ClientboundAddMobPacketgone — ClientboundAddEntityPacket
ClientboundUpdateViewPositionPacketClientboundSetChunkCacheCenterPacket
ClientboundUpdateViewDistancePacketClientboundSetChunkCacheRadiusPacket
ClientboundLevelChunkPacketClientboundLevelChunkWithLightPacket
routine ClientboundTeleportEntityPacketClientboundEntityPositionSyncPacket
ClientboundGameProfilePacketClientboundLoginFinishedPacket (+ a session id)
ServerboundLoginStartPacketServerboundHelloPacket
ClientboundEncryptionRequestPacket / responseClientboundHelloPacket / ServerboundKeyPacket
ClientboundSetCompressionPacketClientboundLoginCompressionPacket
ClientboundResourcePackPacketClientboundResourcePackPushPacket / …PopPacket
MinecraftServer.getSessionServiceMinecraftServer.services
PlayerChunkSender in server/levelserver/network
Component.Serializer (Gson)ComponentSerialization (codecs; NBT on the wire)
TextComponent / TranslatableComponent / …network/chat/contents — PlainTextContents etc.
ComponentUtils.updateForEntityComponentUtils.resolve with a ResolutionContext
SignedMessageHeader / MessageSignerSignedMessageLink / SignedMessageChain.Encoder
ChatPreview and its packetsgone
ClientboundSetTimePacket(gameTime, dayTime, …)a game time plus a WorldClock update map
FriendlyByteBuf.readList / readMap / readCollection / limitValuegone — ByteBufCodecs.list, ByteBufCodecs.map and ByteBufCodecs.collection, which clamp the allocation
FriendlyByteBuf.readBlockHitResultBlockHitResult.STREAM_CODEC

Part X — The client

the name you remember26.3
Gui (the HUD)Hud, held as Gui.hud; the name Gui now means the screen/overlay manager
Minecraft.screen / Minecraft.setScreenGui.screen / Gui.setScreen
GuiGraphicsGuiGraphicsExtractor (records states; does not draw)
Screen.render / every render* on GuiScreen.extractRenderState / every extract* on Hud
LayeredDrawbounding-box inference and call order, plus GuiRenderState.nextStratum
Options.hideGuiHud.isHidden, published as GuiRenderState.isHudHidden
Minecraft.getPartialTick, TimerDeltaTracker.Timer and its three questions
Minecraft.destroygone — Minecraft.stop, then Minecraft.exitWorldAndClose and Minecraft.close
key-mapping categories as stringsKeyMapping.Category is a registrable record, not a string
MouseHandler.lastMouseEventTimegone
raw (key, scancode, modifiers, action) on every Screen methodthe client/input records: KeyEvent, MouseButtonEvent, CharacterEvent, PreeditEvent
Font.drawInBatch and every drawString variantFont.prepareText → Font.PreparedText; the drawing verbs are on GuiGraphicsExtractor, drawString’s as GuiGraphicsExtractor.text
Font.StringRenderOutputFont.PreparedText plus Font.GlyphVisitor
BakedGlyph (a class)an interface; the sheet implementation is BakedSheetGlyph, effects are EffectGlyph
RawGlyph / SheetGlyphInfoUnbakedGlyph (info and bake) and GlyphBitmap (pixels and upload)
GlyphProviderBuilder / GlyphProviderBuilderTypeGlyphProviderDefinition / GlyphProviderType
Style.withFont taking an idstill Style.withFont, but the type is FontDescription, which may be a sprite rather than a font
FontSet.getGlyph as public APIprivate — FontSet.source then GlyphSource.getGlyph
AbstractSoundInstance.resolveAbstractSoundInstance.getOrResolve
GLFW and its callbacksSDL3: SDLEventHandler.pollEvents drains its events inside RenderSystem.pollEvents, input to KeyboardHandler and MouseHandler, the rest to Window.handleEvent
KeyboardHandler.setup / MouseHandler.setupgone — SDLEventHandler calls the handlers’ public methods
LocalPlayer.tick sending the move packetsLocalPlayer.sendChanges, called by Minecraft.tick after the level’s entities and block entities

Part XI — Rendering

Thirty-nine rows. Four refactors account for more than half of them — extract then render, the GPU layer’s move into com/mojang/renderpearl, the window’s move to SDL, and order-independent transparency — and the rest are the renames and moves around them — models, particles, sections, the camera, and the sky and fog.

the name you remember26.3
MultiBufferSource / BufferSourceSubmitNodeCollector / SubmitNodeStorage / FeatureRenderDispatcher
ShaderInstance, RenderStateShardRenderPipeline + RenderPipelines + BindGroupLayouts
VertexBuffer, Tesselator, BufferUploaderGpuBuffer / GpuBufferSlice, BufferBuilder → MeshData (over a ByteBufferBuilder’s memory), UberGpuBuffer
RenderSystem.setShader / enableBlend / depthMask …fields of a RenderPipeline
VertexFormat.Mode, VertexFormat.IndexType, TextureFormatPrimitiveTopology, IndexType, GpuFormat
Window.updateDisplay, vsync as a swap intervalGpuSurface.present, vsync as a GpuSurface.PresentMode
LightTexture.pack and friendsLightCoordsUtil
DimensionSpecialEffectsDimensionType.skybox + EnvironmentAttributes + Timeline
FogParameters, RenderSystem.setShaderFogColorFogData, RenderSystem.setShaderFog (a uniform slice)
Level.getSkyColor, ClientLevel.getStarBrightness, ClientLevel.effectsEnvironmentAttributeProbe.getValue on an EnvironmentAttribute
LevelRenderer.renderLevel / renderSky / renderChunkLayerLevelRenderer.render and the LevelRenderer.addSkyPass family of frame-graph passes
LevelRenderer.blockChanged / setSectionDirty / allChangedthe same names on LevelExtractor
ChunkRenderDispatcher, RenderChunk, CompiledChunkSectionRenderDispatcher, its SectionRenderDispatcher.RenderSection, CompiledSectionMesh
RenderType.chunkBufferLayers, ChunkSectionLayer.TRIPWIREChunkSectionLayer, with three layers — the tripwire layer is gone
BakedModel, ModelResourceLocationBlockStateModel / ItemModel; block models keyed by BlockState
BlockModelShaper, ItemModelShaper, BlockRenderDispatcher, ItemRendererBlockStateModelSet, ItemModelResolver, ModelBlockRenderer, and for the item draw ItemFeatureRenderer
BlockElement / BlockElementFace, AtlasSet, ItemColorsCuboidModelElement / CuboidFace, AtlasManager, ItemTintSource
EntityRenderer.render, RenderLayer.renderEntityRenderer.extractRenderState + EntityRenderer.submit
TextureSheetParticle, sheet ParticleRenderTypesSingleQuadParticle + SingleQuadParticle.Layer
ParticleGroup (a limit record)ParticleLimit; ParticleGroup is now the per-render-type bucket
Camera.setupCamera.update + Camera.extractRenderState
Camera.getPosition, getEntity, getRotation, getLookVectorCamera.position, Camera.entity, Camera.rotation, Camera.forwardVector — the get prefix dropped from these four, though Camera.getNearPlane and Camera.getFluidInCamera kept theirs and new getters such as Camera.getCullFrustum and Camera.getFov carry one
RenderStateShard composition (the texture and layering half)RenderType over a RenderPipeline, catalogued in RenderTypes, built by RenderSetup
BakedQuad as four verticesa ten-component record, with a BakedQuad.MaterialInfo of eight
LiquidBlockRendererFluidRenderer, over a FluidModel
ItemOverrides / getPropertyOverrideSelectItemModel / RangeSelectItemModel / ConditionalItemModel
ScreenManager (the Blaze3D monitor manager)MonitorManager, with Monitor and VideoMode — same package, now fed by SDL’s display events
Window.updateVsynca GpuSurface.PresentMode in the surface configuration
GpuDevice, CommandEncoder, RenderPass in com/mojang/blaze3dstill interfaces, reworked and moved to com/mojang/renderpearl/api, implemented by FrontendGpuDevice and its siblings over the backends in com/mojang/renderpearl/backend/opengl and /vulkan
GLXgone — RenderSystem.initBackendSystem starts SDL, and a backend that fails throws BackendCreationException
Window.isMinimized / Monitor.getPreferredVidModeWindow.isIconified / Monitor.getPreferredVideoMode
ItemInHandRendererFirstPersonHandsAndItems (the tick state and the extract) + FirstPersonHandsAndItemsRenderer (the pose)
NameTagFeatureRenderergone — a name tag is a TextFeatureRenderer.Submit
the transparency post chain (Fabulous)order-independent transparency inside the main pass: LevelRenderer.executeOit, three OitStages
LevelRenderer.addCloudsPass / addWeatherPassgone — clouds and weather draw inside LevelRenderer.addMainPass
ShaderManager.prepare, moj_importShaderManager.loadConfigs, #include resolved by GlslCompiler.compileToSpv
ShaderManager.CompilationCachePipelineCache, swapped in by RenderSystem.setCurrentPipelineCache
GameRenderer.postEffectId / effectActiveGameRenderer.spectatedEntityPostEffect / GameRenderer.spectatedEntityEffectActive, beside the per-frame GameRenderer.requestedPostEffects
DynamicUniforms / DynamicUniformStorageDynamicGpuData / DynamicGpuDataStorageMapped

Part XII — World generation

the name you remember26.3
GenerationStep.Carvinggone — BiomeGenerationSettings.carvers is one flat HolderSet
DensityFunctions.WeirdScaledSamplerIntervalSelectFunction
StructureFeature / ConfiguredStructureFeatureStructure / Registries.STRUCTURE
Feature.RANDOM_PATCH, Feature.FLOWERgone — composed from SimpleBlockFeature + placement
Feature.POINTED_DRIPSTONE / DRIPSTONE_CLUSTERSpeleothemFeature / SpeleothemClusterFeature
AbstractTreeGrower and its subclassesone final TreeGrower with constants
TreeConfiguration.dirtProviderTreeFeature.belowTrunkProvider
Biome.BiomeCategory / Biome.getDownfallgone
MultiNoiseBiomeSource.PresetMultiNoiseBiomeSourceParameterList.Preset
BiomeSpecialEffects.fogColor / skyColor / music / ambient soundEnvironmentAttributes.* via Biome.getAttributes
the +8 chunk population offsetgone — decoration starts at the chunk corner, InSquarePlacement scatters
StructureTemplateManager’s folder for saved templates, generated/<namespace>/structures/generated/<namespace>/structure/, the singular a data pack’s templates were already read from
level.dat field WorldGenSettingsits own file — WorldGenSettings is SavedData, written to data/minecraft/world_gen_settings.dat; PrimaryLevelData.OLD_WORLD_GEN_SETTINGS is all that is left of the key
ConfiguredFeature / FeatureConfiguration / FeaturePlaceContextgone — a Feature carries its own parameters; its instances are Registries.FEATURE
PlacedFeature.placeWithBiomeCheck / PlacementModifier.getPositionsFeaturePlacer.placeWithBiomeCheck / PlacementModifier.modify
RandomOffsetPlacementOffsetPlacement
ConfiguredWorldCarver / CarverConfiguration / NetherWorldCarvergone — WorldCarver instances in Registries.CARVER; the nether carver is a cave carver
SurfaceRules / SurfaceSystemMaterialRule / MaterialCondition / MaterialSystem
OreVeinifierOreVeinRule
NoiseChunk.forChunk / NoiseChunk.wrapNew and the four cache markersgone — DensityFunctionCompiler compiles once per dimension; one cache node; a SamplerContext per run
DensityFunctions.Marker / BlendAlpha / BlendOffset / BeardifierMarker / Mapped / Ap2 / MulOrAddCacheFunction and InterpolatedFunction, SimpleDensityFunction, UnaryFunction and BinaryFunction, under world/level/levelgen/densityfunction
TreeConfigurationTreeFeature, a record holding the fields
Climate.findSpawnPositionNoiseSpawnFinder.findSpawnPosition
Biome.getMobSettingsEnvironmentAttributes.NATURAL_MOB_SPAWNS
RandomState.samplerRandomState.createClimateSampler

Part XIII — Commands and data packs

The permission rewrite is the largest single break in this table: the integer permission level is gone from the whole command API, replaced by PermissionSet and PermissionCheck in net/minecraft/server/permissions. The ints survive only in ops.json, in server.properties and on the wire.

the name you remember26.3
ResourceLocationArgumentIdentifierArgument (the registry id is unchanged)
CommandSourceStack.hasPermission(int)CommandSourceStack.permissions + PermissionSet.hasPermission
CommandSourceStack.getPermissionLevelgone — a source carries a PermissionSet. PermissionLevel itself is very much alive: LevelBasedPermissionSet, server.properties, ServerOpListEntry and the JSON-RPC schema all still speak it
CommandSourceStack.withPermission(int)CommandSourceStack.withPermission taking a PermissionSet
SharedSuggestionProvider.hasPermission(int)gone — the interface extends PermissionSetSupplier
Commands.LEVEL_GAMEMASTERS as an intsame name, now a PermissionCheck
Commands.hasPermission(int)Commands.hasPermission taking a PermissionCheck, returning a PermissionProviderCheck
ServerPlayer.hasPermissions(int)ServerPlayer.permissions
MinecraftServer.getProfilePermissions returning an intthe same name returning a LevelBasedPermissionSet
MinecraftServer.getFunctionCompilationLevelMinecraftServer.getFunctionCompilationPermissions
Commands.LEVEL_ALL / LEVEL_MODERATORS / LEVEL_ADMINS / LEVEL_OWNERS as intsall four are PermissionChecks too — PermissionCheck.AlwaysPass for the first, PermissionCheck.Require for the rest
ServerPlayer.setPermissionLevel(int)PlayerList.sendPlayerPermissionLevel on the server; LocalPlayer.setPermissions on the client
ColorArgumentTeamColorArgument, yielding a TeamColor rather than a ChatFormatting
PlayerTeam.getColor returning a ChatFormattingreturns an optional TeamColor, its own enum carrying a TextColor
TestFunctionArgument / TestClassNameArgumentgone — /test addresses tests as registry ids through ResourceSelectorArgument and TestFinder
net.minecraft.advancements.Criterion / CriterionTrigger / SimpleCriterionTriggerall moved to net/minecraft/advancements/triggers; CriterionTriggerInstance and CriterionProgress stayed behind in net/minecraft/advancements
ServerOpListEntry.getLevelServerOpListEntry.permissions
ParserUtils.parseJsongone — SnbtGrammar plus ParserBasedArgument
ItemInput.createItemStack(int, boolean)ItemInput.createItemStack with one argument; the guard is GiveCommand.MAX_ALLOWED_ITEMSTACKS
ServerFunctionManager.ExecutionContext (nested)top-level ExecutionContext in net/minecraft/commands/execution
CommandFunction.Entry / CommandEntry / FunctionEntrygone — a line is a BuildContexts.Unbound, a macro line a MacroFunction.MacroEntry
CommandFunction.CacheableFunction (nested)top-level CacheableFunction, codec-backed
Commands.performCommand returning a success countreturns nothing; results are a CommandResultCallback pair
data/<ns>/functions/, data/<ns>/tags/functions/singular — function/ and tags/function/
maxCommandChainLengthGameRules.MAX_COMMAND_SEQUENCE_LENGTH
maxCommandForkCountGameRules.MAX_COMMAND_FORKS
announceAdvancementsGameRules.SHOW_ADVANCEMENT_MESSAGES
net.minecraft.advancements.critereon / criterionsplit three ways: net/minecraft/advancements/triggers, net/minecraft/advancements/predicates, and advancements/predicates/entity for the entity half
AdvancementListAdvancementTree (+ AdvancementNode, AdvancementHolder)
FrameTypeAdvancementType
CriterionTrigger.addPlayerListener / removePlayerListenergone — triggers are stateless; subscriptions live in PlayerAdvancements
LootContextParamSetContextKeySet
@GameTest, @GameTestGenerator, @BeforeBatch, @AfterBatchgone — GameTestInstance in Registries.TEST_INSTANCE
GameTestRegistry / TestFunctiongone — Registries.TEST_FUNCTION + TestFunctionLoader, and TestData
a test’s batch as a stringGameTestInstance.batch — the batch is a TestEnvironmentDefinition, in one dimension
the structure block as the test hostTestInstanceBlock / TestInstanceBlockEntity
ServerAdvancementManager as a reload listenerRegistries.ADVANCEMENT, read by RegistryDataLoader; the manager’s constructor lays the tree out
AdvancementTree.ListenerClientAdvancements.Listener
Advancement.readAdvancement.STREAM_CODEC
EntityPredicate.ADVANCEMENT_CODECgone — LootItemCondition.CODEC
PlayerList.allowCommandsForAllPlayersgone — IntegratedServer.getProfilePermissions answers for singleplayer first, with a guest command-access toggle

The shape changes, not just the names

A rename table flatters the reader: it suggests that if you learn the three hundred and nine rows above you can read the tree. You cannot, because many of those rows are one of seven design changes, and the change is what the corresponding page is about:

  • Tags on an item became components. ItemStack.getTag / getOrCreateTag have no replacement; a stack is an Item plus a PatchedDataComponentMap and every former tag key is a DataComponentType in DataComponents — data components, items and stacks.
  • Hand-written serialisation became codecs. Packet.write is gone: every packet but the bundle carries a StreamCodec the protocol table reads, and most packets are records. ItemStack.save is gone: there is ItemStack.CODEC and, for saved data, the ValueOutput façade — packets and stream codecs, codecs, NBT and JSON.
  • Rendering split into extract and render. GuiGraphics, EntityRenderer.render, LevelRenderer.renderLevel, MultiBufferSource and every RenderSystem state setter are gone or repurposed, because the frame now copies the live game into render state and then draws from that copy, both halves on the Render thread — the frame, Blaze3D, entity rendering.
  • Per-dimension and per-biome constants became one attribute system. DimensionSpecialEffects and most of BiomeSpecialEffects are gone; fog, sky, water fog colour, ambient sound and music are EnvironmentAttributes resolved through an EnvironmentAttributeProbe over a stack of layers — lightmap, fog and sky, biomes.
  • Enums of behaviour became registries of data. EnchantmentCategory, GenerationStep.Carving and Biome.BiomeCategory are all gone, replaced by item sets, registry records, HolderSets or nothing at all; MobSpawnType and BlockPathTypes stayed enums under new names.
  • UUIDs became identifiers. An AttributeModifier is keyed by Identifier, not a UUID, which is why a data pack can now name one — attributes.
  • Sides split. Entity.hurt split into Entity.hurtServer and Entity.hurtClient — the old name survives, as a @Deprecated final that returns void and forwards to the server half only, so grep finds it and it does nothing on the client; Player.attack is still there but the packet that reaches it is ServerboundAttackPacket, a record of one integer, and ServerboundInteractPacket is right-click only. The general rule: where 1.21 had one method that checked Level.isClientSide, 26.3 tends to have two methods — damage and death, the sword swing.

Yarn

Yarn is Fabric’s community mapping set. It is not a different version of the game and nothing on this list is a change: it is the same 26.3 class under the name a Fabric modder has in their head. Only the ones that trip people are listed — where the Yarn and Mojang names differ enough that grep fails.

Both columns are italic here, because Yarn names are not in the decompile and verify_names.py cannot check them; the Mojang column is the one this corpus uses everywhere else, and every one of those names is backticked and verified on its own page.

YarnMojang (this corpus)
World / ServerWorld / ClientWorldLevel / ServerLevel / ClientLevel
WorldChunkLevelChunk
MinecraftClientMinecraft
PlayerEntity / ServerPlayerEntity / ClientPlayerEntityPlayer / ServerPlayer / LocalPlayer
ClientPlayNetworkHandlerClientPacketListener
ServerPlayNetworkHandlerServerGamePacketListenerImpl
ClientConnectionConnection
Text / MutableTextComponent / MutableComponent
TextRendererFont
TextHandlerStringSplitter
TextVisitFactoryStringDecomposer
OrderedTextFormattedCharSequence
StringVisitableFormattedText
FontStorageFontSet
GlyphAtlasTextureFontTexture
DrawContextGuiGraphicsExtractor — and in 26.3 it does not draw; see the drift table
NbtCompound / NbtList / NbtElementCompoundTag / ListTag / Tag
RegistryEntry / RegistryEntryListHolder / HolderSet
Registries / RegistryKeysBuiltInRegistries / Registries
Vec3dVec3
BoxAABB
HandInteractionHand
ActionResultInteractionResult
Inventory (the interface)Container
PlayerInventoryInventory
ScreenHandler / ScreenHandlerTypeAbstractContainerMenu / MenuType
StatusEffect / StatusEffectInstanceMobEffect / MobEffectInstance
EntityAttribute / EntityAttributeInstanceAttribute / AttributeInstance
ParticleEffectParticleOptions
BlockPos.MutableBlockPos.MutableBlockPos
IdentifierIdentifier — Yarn was right first; Mojang renamed to match

The last row is the joke that keeps giving: the single most-cited example of “Yarn names are better” stopped being an example, and a decade of Fabric code now compiles against a Mojang-named class with the Yarn name.

What a rename table cannot tell you

  • A verified name is not a correct claim. verify_names.py proves the right-hand column exists; it cannot prove the left-hand column ever did. The 1.21 side of this table, like the Yarn column above and the italic names in the pages’ For a 1.21-era reader notes, is what the corpus cannot verify, and 1.21.11 is the only 1.21 it can check a name against.
  • The names did not move where you would guess. Rendering is only the second-largest table, behind commands. Two rewrites nobody advertised — permissions ceasing to be integers, and game rules becoming a registry — renamed more identifiers than the render-stack refactor did, and the render one is the famous half only because its classes are the ones tutorials name.
  • Renames cluster with rewrites. Where a name changed, the responsibility often moved too — though not always: ClickType is ContainerInput with the same constants, and RedStoneWireBlock is RedstoneWireBlock with the same methods. Reading the row is not enough, which is what the linked page is for.
  • Two subsystems have no rows at all, and that is the answer. Dialogs (net/minecraft/server/dialog and its client screens) and the JSON-RPC management server postdate 1.21 entirely: there is no old name to look up, and a reader who cannot find one is not missing a row. Game tests are the opposite case — the whole 1.21 API is gone, which is why they have five.
  • Minecraft.setScreen is a trap rather than a rename. Minecraft.setScreenAndShow exists in 26.3 and a 1.21-era reader grepping for the old name will land on it, then wonder why the screen stack behaves differently. The method that replaced the old one is Gui.setScreen.
  • One name a 1.21 reader reaches for was never a class. There is no DebugPackets type in 26.3 and there is nothing it was renamed to: the debug traffic is a subscription protocol with one packet per kind (debugging the running game), not a static sender, so the grep that finds nothing is telling the truth.
  • Some names survived and changed meaning, which is worse than a rename because grep still finds them: Gui (now the screen manager, not the HUD), Material (now a texture reference in client/resources/model/sprite, not a block property), ParticleGroup (now a per-render-type bucket, not a count limit), Strategy (now top-level, not nested in PalettedContainer), and MultiVariant, whose name survives only in the data-generator package while the runtime type is gone.

Where to look

Identifier, then Holder and HolderSet (net/minecraft/core), then DataComponents. After that, pick the part you are lost in and read its page rather than its rows.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Glossary

Verified against Minecraft 26.3 · Reference · A short entry per term the rest of the corpus uses, and a link to the page that owns it.

Every page in this corpus assumes the vocabulary of the pages before it. That is deliberate — a lecture that redefines “chunk holder” every time it appears is unwatchable — but it means a reader who arrives in the middle has no way back. This page is the way back: the terms, alphabetically, a short entry each, each pointing at the page where the term is actually explained.

An entry here is a reminder, not a definition to rely on. If the entry is all you needed, good; if it is not, the link is the point of the entry. Where a term is a class name, the class name is the entry — this corpus uses Mojang’s names for concepts as well as for types, and inventing a second vocabulary to sit beside them would only double the work.

A

Activity — the filter that decides which of a brain’s behaviours may start at all, rather than a mode it runs in: the active set is always the core activities plus exactly one other, and an ActivityData declares each one’s prioritised behaviour list. → AI

Advancement — a data-pack-defined goal: criteria, a requirements expression over them, an optional display entry and a reward. → advancements

Aquifer — the worldgen component that decides what a point is made of once the density is known — stone, air, water or lava — from its own barrier and fluid-level noises; a carver only marks a shape, and the aquifer decides what fills each block of it. → terrain

Argument type — a Brigadier ArgumentType that parses one argument off the command line, however many words that takes — three for a Vec3Argument, all of the rest for a MessageArgument; vanilla’s live in net/minecraft/commands/arguments and are described to the client through an ArgumentTypeInfo. → Brigadier and commands

Atlas — one large texture stitched at load time out of many sprite files, so a chunk section’s quads can all be drawn with one bound colour texture. → models and atlases

Attribute — a named, ranged, modifiable number on a LivingEntity; modifiers are keyed by Identifier, and eight attributes are not client-syncable at all. → attributes

Authority — whose copy of an entity produces the position that counts: the server for a mob, the owning client for its own player and for the boat that player is steering — the server still runs your player’s physics, then overwrites its answer with the number your client sent. Five predicates on Entity decide it, starting with the final Entity.isLocalInstanceAuthoritative. → authority

Avatar — the class between LivingEntity and Player, holding the player-shaped body and nothing else; a Mannequin is an Avatar that is not a player. → player anatomy

B

Batch (game tests) — a group of game tests that share an environment and a dimension, and the key they are grouped by is that pair rather than a name. The renderer’s batch is a different word: quads grouped by render state, which belongs to section meshing and entity rendering. → game tests

Beardifier — the density-function term that bends terrain around a structure; terrain adaptation writes no blocks, it changes the noise. → structure placement

Behaviour — one unit of brain AI, gated on memories and asked once a tick; unless it overrides Behavior.canStillUse it stops inside the same Brain.tick that started it, so everything it does it does in Behavior.start, or, for a one-shot, in its trigger. → AI

Biome — a named bundle of generation settings, block tints and environment attributes, attached to a 4×4×4 volume of the world; its mob spawns are one of those attributes. → biomes

BiomeSource — the object that answers “which biome is at this quart position”: by a climate search, from one fixed biome, from a checkerboard of a listed few, or, in the End, from the central island’s disc and a single erosion sample beyond it; all but the fixed and checkerboard sources make a separate BiomeResolver to answer. → biomes

Blaze3D — Mojang’s graphics layer: com/mojang/blaze3d keeps RenderSystem, the window and the frame graph, and the GPU abstraction under it is com/mojang/renderpearl, with OpenGL and Vulkan backends behind one GpuDevice. → Blaze3D

Blend alpha — the mixing weight Blender computes from the distance to the nearest measured old column: zero against the seam, where the old measurement sets the offset and fixed values the factor and jaggedness, and one out of range (use the noise). → blending at the old-chunk border

BlendingData — the sixteen-column ring of heights, densities and biomes one of these measures out of its own chunk’s blocks, on the sides facing ground the game has yet to generate; its presence on a chunk is what makes the chunk old. → blending at the old-chunk border

Block — the singleton describing a kind of block: its behaviour, its property set and its state table. One Block, many BlockStates. → blocks and states

Block entity — the per-position mutable state a block cannot fit into its state (a chest’s contents, a furnace’s progress), stored on the chunk. → block entities

Block event — a message from a block to itself (a piston push, a note block, a chest lid), queued on ServerLevel and drained at one named phase of the level tick — a wait for a phase rather than a delay, so it usually runs in the very tick it was raised — and mirrored to nearby clients as a packet. → pistons and block events

BlockState — one combination of a block’s property values, built once by the block’s StateDefinition and compared by identity; Block.BLOCK_STATE_REGISTRY is the flat table that numbers it for the wire and the global palette. → blocks and states

Border tick — a position along an old chunk’s seam queued for post-processing, so a leaf or a fluid there is re-evaluated when the chunk goes live. → blending at the old-chunk border

Boss bar — a ServerBossEvent holding a name, a progress float, a colour, an overlay style and three client effects (darkened screen, boss music, world fog), broadcast to the players attached to it; the /bossbar kind adds an integer value and maximum, persists its membership as UUIDs, and is execute store’s third sink. → scores, teams and stored data

Brain — the memory-and-behaviour AI that twenty mob classes build, villagers, piglins and axolotls among them, as opposed to the older goal system. → AI

Brigadier — Mojang’s command-parsing library: a tree of literal and argument nodes with per-node requirements, shared by client and server. → Brigadier and commands

Built-in block model — a block model attached in code by BuiltInBlockModels rather than by a resource pack, living in a second model table that terrain never reads; how a minecart or a block display draws a chest. → block-entity rendering

C

Camera — the client’s eye: position, rotation and the cull frustum, copied into a CameraRenderState once per frame — plus an EnvironmentAttributeProbe that Camera.tick advances and no render state carries. → the frame

Carver — a worldgen pass that hollows out caves and ravines by marking their shape in a mask and writing no block itself; what each marked block becomes — air, water or lava — is the Aquifer’s answer, in the Nether as everywhere else. → terrain

Cell — the lattice unit of terrain noise, four blocks wide and deep and eight tall in the overworld, 768 to a chunk: the expensive three-dimensional density terms are evaluated at its corners and interpolated within it. → terrain, density functions

Chunk — a 16-by-16 column of the world’s full height: sections, heightmaps, block entities, tick queues and a status. → chunk anatomy

Chunk layer — which of the three ChunkSectionLayer buffers a block’s quads are meshed into — solid, cutout or translucent — decided at bake time from the alpha inside that quad’s own patch of its sprite, unless its material forces translucency, rather than from the block; the mesher only reads the answer, except for leaves, which it overrides. → models and atlases

ChunkHolder — the server’s per-chunk record of the level the loading graph computed for it, a future per threshold and status, and what changed in it this tick; which tickets asked for that level is TicketStorage’s business, not the holder’s. → tickets and loading

ChunkMap — the server’s chunk table: holders, the ticket-driven level graphs, entity tracking, and the region-file storage underneath. → tickets and loading

ChunkStatus — one rung of the generation ladder; a chunk advances one status at a time, and the ChunkStep for each status declares the neighbour radius that step needs. → the chunk generation pipeline

Climate — the noise sample (temperature, humidity, continentalness, erosion, depth, weirdness) a biome is chosen by. → biomes

Codec — a DataFixerUpper object that both encodes and decodes one type against any DynamicOps; the corpus’s universal serialisation vocabulary. → codecs, NBT and JSON

CommandSourceStack — who is running this command, and from where: position, rotation, level, entity, a PermissionSet and an output sink, immutable, so a with… that changes something returns a copy. → Brigadier and commands

Compiled query — the immutable EntitySelector a parse produces: thirteen fields and no grammar, resolvable any number of times against different sources. → entity selectors

Component — two different things the corpus keeps apart: a data component on an item stack, and a Component of chat text. → data components, text components

Connection — the tail handler of one Netty pipeline plus the channel it holds, with exactly one packet listener at a time, swapped when the protocol phase changes. → the connection

Container — the interface a thing with item slots implements (a chest, a hopper, an inventory), as distinct from the menu a player interacts with it through. → containers and menus

Criterion — two different things the corpus keeps apart: one condition inside an advancement, backed by a CriterionTrigger the server fires when the relevant thing happens; and a scoreboard objective’s ObjectiveCriteria, which is what the objective counts. → advancements, scores, teams and stored data

D

DamageSource — the what hit you, and who is responsible record every damage calculation and death message reads. → damage and death

Data component — a typed, codec-backed value keyed by a DataComponentType: a patch over the item’s prototype on a stack, a whole map on a block entity, a view over its own fields and its type’s prototype on an entity; what NBT item tags became. → data components

Data-driven type — the book’s name for the shape a dozen systems share: a registry of types, each type a codec, and an instance a Codec produced from JSON, dispatched on its own type field. Fifty-six registries follow it. → the data-driven type pattern

Data pack — a pack of JSON, structure NBT and function files supplying the server’s data-driven content; the server half of the resource system. → the resource system

DataLayer — the nibble array one section’s block light or sky light lives in, owned by the light engine and never by the section. → lighting

Debug subscription — a registered kind of debug value a client can ask the server for; half the kinds the server polls, diffs and sends only when they change, and the rest it pushes as they happen. → debugging the running game

DeltaTracker — the client’s clock: how much of a tick has elapsed, and the source of every partial tick in the frame but the lightmap’s and a frozen camera entity’s, each a literal one. → the client loop

Density function — a node in the JSON-defined graph that turns a position into a number, but only once compiled into a DensitySampler: the graph in the registry is never the graph that actually runs. → density functions

Dialog — a data-pack-defined form the server can put on a player’s screen, whose submitted values come back as a packet. → dialogs

Dimension — one ServerLevel and its DimensionType: a height range, a set of environment attributes and its own chunk storage. → level data and rules

E

Enchantment — a data-pack record of effect components, most of them conditioned on loot predicates; its registry is synchronised, but a client that already has the pack is sent only the id. → enchantments

Entity — a thing the level ticks in its own right: a position, a bounding box, synched data and a tick method. A block entity is not one. → entity anatomy

EntityType — the registry entry for a kind of entity: its factory, category, size, feature flags and the two numbers that decide how it reaches clients. Spawn rules are keyed by the type in SpawnPlacements, not held on it. → entity anatomy

EnvironmentAttribute — a per-dimension, per-biome, per-time-of-day, per-weather value resolved through a stack of layers: directly on the server, through the camera’s smoothing probe on the client. Not only the visual ones: alongside fog and sky colour sit twenty-three gameplay attributes — whether lava flows fast, whether piglins zombify, whether a bed works, and the villager’s schedule. → environment attributes and timelines

Event loop — the queue-and-thread pairing BlockableEventLoop is: an owning thread that drains posted tasks and, through BlockableEventLoop.managedBlock, keeps draining while it waits. Minecraft and MinecraftServer are both one — but only the server rations the drain against a time budget; the client empties the queue every frame. → the server tick

Experiment — a built-in data pack whose PackSource is PackSource.FEATURE, enabling one non-vanilla FeatureFlag; switching one on in the create-world screen is a data-pack reload. → creating a world

Extract — the first half of the client’s frame: walk the game state, cull it, and write the render states, so that the drawing half reads no live game state from LevelRenderer.render down but what a few snapshots hold by reference (a moving block’s light, a sign’s text, a section compiled on the spot) — the top of the render half still does. The top-level states are single objects re-filled each frame, not fresh immutable values. → the frame

F

Feature — the algorithm half of decoration: what to build, with no say in which positions it is offered. → features and placement

Feature flag — a bit in a FeatureFlagSet, which is a 64-bit mask and the FeatureFlagUniverse it belongs to. There is one universe and four flags in it: vanilla and three experiments. A registry element that is a FeatureElement carries a set, and a lookup narrowed by the world’s enabled set hides it — the registry is still frozen, and the view is what changed. → identifiers and registries

Flat level generator preset — a display item plus a FlatLevelGeneratorSettings, one row of the Superflat Presets screen; nine ship. → creating a world

Fluid — the registry object behind a FluidState, a source and a flowing instance per liquid, with FlowingFluid holding the spread algorithm and LiquidBlock the block form. → fluids

Font — a resource-pack-defined glyph source plus the measuring and wrapping API on top of it; a glyph is baked into a texture the first time its font set is asked for it, and again after a reload or a font-option toggle. → text and fonts

Frame — two different things the corpus keeps apart. In Part XI it is one pass of the client’s render loop, the unit partial tick, extract and the frame graph are all relative to (the frame). In Part XIII it is the execution engine’s unit of a running function: a depth, a result callback that /return feeds sideways, and a control that can delete the frame’s pending work — one object shared by reference across a whole function body, and deliberately not a stack frame. → the execution engine

Frame graph — the client’s per-frame declaration of render passes and the targets each reads and writes, resolved before anything is drawn. → visibility and the frame graph

Function — a .mcfunction file: a list of commands loaded as a CommandFunction, optionally with macro lines. → functions and macros

G

Game event — a broadcast fact about something that just happened at a position (a block placed, a door opened) that sculk sensors and mobs listen for. → game events and vibrations

Game rule — one typed, server-wide switch or number in GameRules, saved with the world and sometimes sent to the client. → level data and rules

Game test — a data-driven test instance: a structure, an environment and a check the server runs and reports on. → game tests

Globally-rendered block entity — one whose renderer says BlockEntityRenderer.shouldRenderOffScreen, so the client keeps it in a level-wide set and draws it whether or not its section is visible; three renderers qualify. → block-entity rendering

Goal — one unit of the older mob AI: a start condition, an answer to whether it may be interrupted, and the set of Goal.Flag controls it claims while running. The priority belongs to the WrappedGoal that holds it, and the flag table rather than the priority is what arbitrates. → AI

GpuDevice — the façade every GPU resource is created through; both graphics backends sit behind it as GpuDeviceBackend implementations. A draw reaches the driver through the CommandEncoder it hands out and the RenderPass that opens. → Blaze3D

GuiRenderState — the 2D render tree: strata of nodes that infer their own layering from bounding boxes and are batched into draw calls at the end of the frame. → the GUI render tree

H

Heightmap — a per-chunk 2D array holding the first free Y above the topmost block matching a predicate; six types exist, and a live chunk keeps the four that survive worldgen and any asked for since. → chunk anatomy

Holder — a reference to a registry entry that can exist before the entry is bound: Holder.Reference for a registered value, Holder.Direct for an inline one. → identifiers and registries

HolderSet — a set of holders: either a tag (HolderSet.Named) or a literal list. → tags

HUD — the in-world overlay (hotbar, hearts, chat, boss bars), which is the class Hud; Gui is the screen and overlay manager that holds it. → the HUD

I

Identifier — a namespace and a path; the id of everything. A 1.21-era reader knows it as ResourceLocation. → identifiers and registries

Ingredient — a recipe’s “any of these items” test. It cannot be an empty inline list, but a tag that resolves to nothing makes one empty — and a recipe holding it is never placeable. → recipes

Integrated server — the MinecraftServer a singleplayer client runs on its own Server thread. Every change to the world still crosses as a packet; a handful of settings cross by direct call. → anatomy

InteractionResult — the answer a block or item gives to a click: was the input consumed, should the arm swing, did the held item change. → block interaction

Item — the singleton for a kind of item, holding none of the components a stack shows; a stack is a holder to one of these, a count, a pop time and a patched component map — the item’s defaults plus the ways this stack differs from them. → items and stacks

ItemStack — the object in a slot: a final class holding a count, a pop time, a Holder of Item, and a patch of data components over the item’s prototype. Only the last is really data; equality is the prototype plus the patch, which is why two identical-looking stacks may refuse to merge. → items and stacks

ItemStackTemplate — the immutable item-shaped record (an item holder, a count, a component patch) that data uses where a live, mutable ItemStack would be wrong. → items and stacks

J

Jigsaw — the structure-assembly system that grows a village out of template pieces by matching connector blocks. → jigsaw and templates

K

KeyMapping — one bindable action: whether its key is down, plus a counter of owed clicks that KeyMapping.consumeClick drains rather than edge-detects. → input and keybinds

L

Level — a world: ServerLevel on the server, ClientLevel on the client, sharing an abstract Level that carries most of the behaviour — ClientLevel differs by hollowing out inherited methods rather than by implementing fewer of them. The word is also a number elsewhere: a ticket level is how strongly a chunk is held, a permission level is one of five ranks, and an enchantment level is an enchantment’s strength. → the level tick, the client level

Lightmap — the small texture the client samples to turn a block-light / sky-light pair into a colour; drawn on the GPU once per tick. → lightmap, fog and sky

LivingEntity — the rung of the entity tree that can be hurt and healed: health, attributes, mob effects, equipment and the shared movement code. It holds 124 of Entity’s 192 descendants and has exactly three direct subclasses — Avatar, ArmorStand and Mob. → entity anatomy

LocalPlayer — the Player a human steers: its own ClientInput, and the last input and position it sent. The block predictions it seems to own are the level’s. → player anatomy

Loot condition — a predicate in the loot package’s own vocabulary (LootItemCondition), asked with a loot context; the same objects the /execute if predicate check runs, under a different name. → contexts and predicates

Loot context — the bag of what was involved — the level, the position, the tool, the killer, the block entity — that every loot roll, predicate and conditional enchantment effect is asked against; a LootContext built to satisfy exactly one parameter set. → contexts and predicates

Loot table — the data-driven roll that turns an event (a block broken, a mob killed, anything at all reading a container that has not been rolled yet) into item stacks. → loot tables

M

Macro — a function line beginning with a $ substitution. The plain lines of the file are parsed once at load; a macro line is substituted and re-parsed per distinct argument tuple, cached only eight deep. → functions and macros

Memory — one typed, optionally expiring value in a Brain; behaviours are gated on which memories are present, absent or merely registered. → AI

Menu — the object behind an open container screen, one copy on each side: slots and a state id, and on the server’s copy the synchroniser that sends its changes to the client. → containers and menus

MinecraftServer — the object that is the server: the thread, the tick loop, every ServerLevel, the player list and the registries the world was loaded with. Singleplayer runs one too, as an IntegratedServer inside the client’s process. → the server tick

MultiPlayerGameMode — the client’s channel for acting on the world: every break, place, use and attack goes through it, and the ones that predict open a prediction window before they send. → prediction and acknowledgement

N

NBT — Minecraft’s binary tag format: a sealed Tag hierarchy whose scalars are records and whose containers (CompoundTag, ListTag) are final classes, read and written through NbtIo and reached by codecs through NbtOps. → codecs, NBT and JSON

NBT path — a compiled query over a tag, six node kinds deep — a named child, an index, all elements, and three kinds of match — that /data uses to read and write, and which materialises the compounds and lists it walks through on a write, though an index never creates the element it names. → scores, teams and stored data

Neighbour update — the server-only notification a block sends its six neighbours after a change; distinct from a shape update, which runs on both sides. → blocks and states

NoiseChunk — the per-chunk workspace (a height query builds a one-column one of its own): it binds the dimension’s compiled samplers to one context that grants the caches the density-function graph asked for and carries the chunk’s beardifier and blender, and it holds the chunk’s Aquifer. → density functions

NoiseRouter — the eight density functions a generator asks for, as one record: six are the climate, which NoiseRouter.createClimateSampler turns into a Climate.Sampler; chunk_surface_level is the surface pass’s; and final_density is the one the fill samples. → density functions

O

Objective — a named scoreboard column: a criterion, a display name, a render type and a number format. Only the dummy and trigger criteria wait for commands; every other one — including every statistic in the game, since Stat extends ObjectiveCriteria — is driven from ServerPlayer. → scores, teams and stored data

Occlusion — four unrelated questions the corpus asks with one word, and no page owns all four. Does this section hide that one is the visibility graph’s, and the answer is a walk rather than a test (visibility and the frame graph). Does this face stop light, or hide the face against it is a shape question every BlockState precomputes — a field on the state itself rather than anything in its shape cache (math and primitives) — and it is, beside a block’s light dampening, what ends a sky column (lighting); the fluid wall test asks the same question of two blocks’ collision shapes, to decide whether fluid may pass between them (why the wall test is affordable). Ambient occlusion is none of that: it is the per-quad shading the mesher bakes in (section meshing). And a vibration’s occlusion is a six-ray raycast for BlockTags.OCCLUDES_VIBRATION_SIGNALS that reports occluded only if all six are stopped (game events and vibrations). A page always means exactly one of the four; the word is the only thing they share.

Old chunk — a chunk whose ChunkAccess.blendingData is non-null, which is to say one whose save data carried a blending_data compound; ChunkAccess.isOldNoiseGeneration is the test. → blending at the old-chunk border

P

Packet — an interface: a PacketType, which is a name and a direction, and one handler method. It comes in three shapes, each class but the bundle packet and its delimiter declares its wire form as a StreamCodec, which the phase’s protocol description dispatches to by the packet’s type, and two dozen types are registered into more than one phase. → packets and stream codecs

PalettedContainer — the bit-packed storage a chunk section keeps its block states and biomes in, with a palette that grows as the section gets more varied. → chunk anatomy

Parameter set — the declared contract between a loot context and the things asked against it: a ContextKeySet naming which ContextKeys must be present and which may be. Building a context with a parameter the set does not allow, or without one it requires, throws; asking for one that is absent gives nothing; and loading a table reports an element that reads a key its set does not allow — the three ways a parameter can be missing. All thirty-one sets are listed in loot context parameter sets. → contexts and predicates

Partial tick — the fraction of a tick elapsed at the moment a frame is drawn, used to interpolate the world. There is no single one: a frame carries six values, they disagree on purpose, and two of them, the one screens are handed among them, are not fractions of a tick at all. → the frame

Particle — a client-only object with a position, a velocity and an age, most of them a textured quad, ticked by the ParticleEngine and extracted from it each frame. Nothing about it is authoritative: it is spawned locally, by prediction, by the ambient scatter or on a packet or level event, and the server is told the player’s particle setting and never acts on it. → particles

Path — the list of nodes a navigator is following, produced by PathFinder’s A* over a snapshot of already-loaded chunks, with a NodeEvaluator deciding what each candidate block is to this mob. → pathfinding

Permission atom — a named capability with an Identifier (Permission.Atom), the other kind of permission besides a command level; an operator’s level-based set grants exactly one, the entity-selector atom, from gamemaster up. → permissions

Permission level — one rung of PermissionLevel (all, moderators, gamemasters, admins, owners), and only the ordered half of a permission: a command source carries a PermissionSet and a node requires a PermissionCheck, neither of which is an integer. → permissions

Permission set — what a command source carries and a node’s check is asked against: on the server a level-based set (a rung plus one atom). The client rebuilds four of those from the op level it is told — rung zero maps to PermissionSet.NO_PERMISSIONS instead — and keeps a chat set built by subtraction beside them. No packet carries a PermissionSet itself. → permissions

PlacedFeature — a feature plus an ordered list of placement modifiers; the unit a biome names. → features and placement

Player — the shared player class: the inventory and ender chest, the open menu, the abilities, the food and experience bars, sleep, and the cooldowns. ServerPlayer is its server subclass, LocalPlayer and RemotePlayer its client ones (through AbstractClientPlayer), and an Avatar is the rung above it. → player anatomy

Point of interest — a block state the game has decided is worth walking to: a bed, a job site, a portal. PoiManager indexes them by position, in its own files beside the chunks. → points of interest

Prediction ledger — the corpus’s name for BlockStatePredictionHandler: the client’s record of what the server is known to have at a block it changed optimistically. The ack is a receipt for a number, not a verdict — it settles every entry at or below it, writing back a correction if one arrived and rolling the block back if none did. → prediction and acknowledgement

Protocol phase — one of handshaking, status, login, configuration and play; each has its own packet table and its own listener. → protocol phases

Q

Quart — a four-block cell, the resolution biomes are stored and sampled at; QuartPos is the arithmetic, tabulated with the other coordinate spaces in math and primitives. → biomes

R

Random tick — a turn a block gets that nobody booked: aimed at no block, carrying no promise, and drawn from the entity-ticking chunk set rather than the wider block-ticking one a scheduled tick uses — so there is a ring of chunks where appointments come due and nothing is ever chosen at random. → scheduled ticks

Recipe — a server-side matcher and assembler; no Recipe ever crosses the wire. The client gets a RecipeDisplay and a RecipeDisplayId, which is a position in a list rather than the recipe’s name. → recipes

Region file — the 32-by-32-chunk container file chunks are stored in, addressed by a sector table at its head. → chunk storage

Registry — a frozen, id-assigning table of one kind of thing; some are built into the jar, some are loaded from data packs, some are sent to the client, and the 161 registry keys are listed in registries. → identifiers and registries

Reload listener — the unit of a reload: one object that reads what it needs off the worker pool and swaps its live state on the owning thread, every apply running in order behind a PreparableReloadListener.PreparationBarrier. → the resource system

Render state — the snapshot of what to draw, produced by the extract half of the frame and consumed by the drawing half; the property that matters is that the drawing half reads no game state below LevelRenderer.render but what a few snapshots hold by reference (see Extract for where the wall actually is). → the frame

RenderPipeline — the client’s declaration of how to rasterise: shaders, blend, depth, cull, vertex format, topology. It says nothing about which textures to bind, which RenderType adds, or which target to draw into, which is the one its RenderPass was opened on. → Blaze3D

RenderType — a RenderPipeline plus the textures to bind and the layering and batching rules. Neither says which target to draw into: a RenderType draws into whatever RenderPass its caller has opened. → Blaze3D

Resource pack — a pack of assets; the client half of the same pack machinery data packs use. → the resource system

S

SavedData — a named, codec-backed blob stored beside the world (the border, the weather, the rules, raids, the dragon fight); level.dat itself is nearly a stub. → level data and rules

Scheduled tick — a block or fluid position queued to run at a named future tick, with a priority breaking ties inside that tick. → scheduled ticks

Score — one number for one holder under one objective, reached through a ScoreAccess handle rather than a setter. → scores, teams and stored data

Screen — one full-window client UI with its own widget tree and lifecycle; the server is told nothing about most of them. → GUI and screens

Section — a 16-cubed piece of a chunk: one paletted container of block states, one of biomes, and four counters. Its light lives in the light engine’s own storage, not on the section. → chunk anatomy

Section mesh — what one section compiled to (CompiledSectionMesh): a draw record per layer into the shared vertex arenas, the visibility set, the block entities and the translucent layer’s sort state, rebuilt when the section is both dirty and visible — usually on a worker, but inline on the Render thread when the chunk-builder option asks for it. → section meshing

Selector head — the single character after the @ (a, e, n, p, r, s) that sets a selector’s default limit, order and entity scope before any option is read; three of the six also pin the type to player, and two instead add an aliveness test. → entity selectors

Sensor (brains) — the half of brain AI that writes memories from the world, on a fixed interval. A sculk sensor is a block and a different subject (one footstep, and the ticks it takes to arrive). → AI

ServerEntity — the server’s per-tracked-entity bookkeeping: what the watching clients were last told, and what to send them next. → what the client is told

ServerPlayer — the server’s copy of a player: the entity, the connection’s listener, the game-mode object and the session state, all on one class. LocalPlayer is the client’s answer to it, RemotePlayer the client’s copy of anyone else, and all three share Player. → player anatomy

Shape update — the “your neighbour changed, recompute yourself” call that runs on both client and server, unlike a neighbour update. → blocks and states

Signed message — a chat message carrying a signature over its content and its place in a per-player chain, so the server, and every client that holds the sender’s session, can check who said it. → chat and signing

Simulation distance — how far the world ticks, as against how far you can see: the radius behind TicketType.PLAYER_SIMULATION, deciding which chunks tick blocks, fluids and entities. → tickets and loading

Special model renderer — a hand-written submitter for a shape no cuboid model can express, reached from an item model or a block state rather than from a block entity; thirteen of them. → block-entity rendering

Staging buffer — in the command engine, the list an executing action appends its spawned commands to, spliced onto the head of the queue after it runs — which is what makes an ArrayDeque behave as a call stack. → the execution engine

Status effect — a MobEffect held on a LivingEntity with an amplifier and a duration; the ones that change a number mostly do it by attaching an attribute modifier, and others are read where the number is used, jump boost, slow falling and levitation among them. → status effects

StreamCodec — the wire counterpart of a Codec: encodes to and decodes from a ByteBuf, with no schema and no field names. → packets and stream codecs

Structure — a generated building or landmark: a placement lottery, a start assembled in memory, and pieces written a chunk at a time. → structure placement

StructurePiece — one room, corridor or slab of a structure. In the hand-built half it is a Java class that writes its own blocks and, in the procedural graphs, constructs its own neighbours, chosen by no pool; the jigsaw half’s PoolElementStructurePiece is one too. Every piece carries a registered StructurePieceType, which is how it comes back off disk. → hand-built structures

StructureStart — one decided structure: the Structure, the chunk it started in, a PiecesContainer, a reference count and a cached bounding box, stored on the chunk it began in. → structure placement

Submit node — one thing to draw that is not terrain, written into SubmitNodeStorage by the submit pass out of the render states extract left behind, and sorted into a phase before the feature renderers turn it into vertices; the fifteen phases and the twelve renderers are listed in submit phases. → entity rendering

SynchedEntityData — the per-entity table of small values the server pushes to watching clients, keyed by class-tree ordinal. → synched entity data

T

Tag — a named set of registry entries defined by data packs and merged across them, unless a higher pack sets replace. (The unrelated NBT sense of the word belongs to NBT.) → tags

Team — a named set of score holders carrying a colour, a friendly-fire flag, a collision rule and a nametag rule — so a class in the scores package is read by collision and by rendering. → scores, teams and stored data

Tick — one step of the server’s simulation, 50 ms at the default rate that /tick rate can change, or one step of the client’s; a client behind the clock catches up to ten accumulated ticks in a frame and discards the rest. → the server tick, the client loop

Ticket — the reason a chunk is loaded: a type and a level, fed by its type’s flags into one or both of two separate graphs, with ChunkLevel deciding what the level buys — a holder still generating toward some status, then full, then block-ticking, then entity-ticking. → tickets and loading

Timeline — one clock’s data-driven curve set: an optional period, the named instants on that clock, and one AttributeTrack per environment attribute — keyframed over modifier arguments, which for an override are the values themselves. → environment attributes and timelines

Trigger (advancements) — the server-side hook that tells one player’s advancement state that something happened, by sweeping that player’s listener map for this trigger. Nothing broadcasts. The scoreboard’s trigger criterion and the /trigger command are a different thing entirely (scores, teams and stored data). → advancements

U

Unattended command — a command the player did not type: a dialog button or a chat click event, sent through ClientPacketListener.sendUnattendedCommand. The client re-parses it and asks first if it fails to parse, needs a signature, or needs any permission at all; a clean one goes without a prompt. A sign’s command is not one of these — it runs on the server at gamemaster level, only on a sign whose allow_op_features flag is set, and the client is never asked. → permissions

V

View distance — how far the server sends chunks. A client’s render-distance request only clamps what it is sent; the ticket radius comes from the server’s own number. Not simulation distance, which is how far the world ticks. → tickets and loading

VoxelShape — the collision or outline volume of a block state, a set of boxes over a bit grid, with fast merge and sweep operations. → math and primitives

W

Watchdog — ServerWatchdog, the daemon that treats a tick longer than max-tick-time as a dead server: it writes a crash report, calls System.exit, and halts the JVM ten seconds later if the shutdown has not ended it by then. → how a server dies

Widget — an AbstractWidget, the reusable piece of a screen: a button, a slider, an edit box. It is usually in all three of a screen’s lists at once — the children that get input, the renderables that get extracted, and the narratables that get described. → GUI and screens

Window — the SDL window handle the whole client hangs off: the framebuffer size, the GUI scale, fullscreen, and the nineteen kinds of event Window.handleEvent answers — not the input ones, which SDLEventHandler hands to KeyboardHandler and MouseHandler. → the window

World clock — the identity a timeline is sampled against: a unit record in Registries.WORLD_CLOCK, two of them in vanilla, holding nothing at all. The tick count, the rate and the paused flag are ServerClockManager.ServerClockInstance’s, one per clock, and /time can move or pause each independently. → environment attributes and timelines

World gen settings — WorldGenSettings: a WorldOptions (the seed, generate structures, bonus chest and a legacy string) and the LevelStem map, a SavedData written to data/minecraft/world_gen_settings.dat. It is the one piece of world generation’s own settings that is saved; the worldgen registries are re-read from the enabled packs on every world open, and a template is looked for in the world’s own files before the packs’. → creating a world

World-limited — a parse-time flag, set by any of seven positional selector options, that confines a selector’s resolve to the source’s own level instead of every level on the server. → entity selectors

World preset — a WorldPreset registry entry holding one LevelStem per dimension; what the world-type button selects, and what level-type names on a dedicated server. → creating a world

World stem — WorldStem, the four things WorldLoader.load hands the server constructor in one bundle: the resource manager, the reloadable server resources, the layered registries, and the level data with its gen settings. → starting a server

WorldGenRegion — the bounded, write-guarded view of the world a generation step is given; it throws rather than loading a chunk, which is why cascading worldgen cannot happen. → the chunk generation pipeline


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.

Diagram lanes

Generated from TEMPLATE.md’s lane key by python tools/check_lanes.py --index. Do not edit by hand.

Almost every lane in a sequence diagram is a class, and a lane means the same thing on every page: the key in TEMPLATE.md is the authority, and check_lanes.py fails a deploy on a page that disagrees with it. The last rows are the exceptions — lanes that stand for a thread, the process, the wire, the disk, a service or the game’s own code rather than for one class.

273 lanes are classes and 10 are not. A lane is normally the initials of the class’s CamelCase words, a trailing Impl dropped (ServerGamePacketListenerImpl is SGPL), but more than a third are not: a short one-word class is its own lane (Player, Sheep), a longer one-word class takes a fixed prefix (Connection is Conn, Enchantment is Ench), and a collision is resolved by lengthening the later claimant, never by reassigning a row — which is why ChestMenu is ChestM and not CM (ChunkMap had it first); and a class a page draws twice, as two copies, has a second lane; and a few follow none of these (MC is Minecraft, PDM is PatchedDataComponentMap). Derive nothing from a lane; read it off this table.

laneclass
AAAbstractArrow
ABAbstractBoat
ACMAbstractContainerMenu
AFBEAbstractFurnaceBlockEntity
APAcquirePoi
ATSAttributeTrackSampler
AttrIAttributeInstance
AttrMAttributeMap
BCBuildContexts
BDBlendingData
BDFSBlendDensityFunction.Sampler
BDRBrainDebugRenderer
BERDBlockEntityRenderDispatcher
BIBucketItem
BIRBuiltInRegistries
BLEBlockLightEngine
BlenderBlender
BlockBlock
BootBootstrap
BrainBrain
BSPHBlockStatePredictionHandler
CAChunkAccess
CAdvClientAdvancements
CameraCamera
CBEChestBlockEntity
CCCClientChunkCache
CComPLClientCommonPacketListenerImpl
CCPLClientConfigurationPacketListenerImpl
CDSClientDebugSubscriber
CECommandEncoder
CEMEnchantmentMenu
CGTChunkGenerationTask
CHChunkHolder
ChanAChannelAccess
ChatCChatComponent
CHelpContainerHelper
ChestMChestMenu
ChestRChestRenderer
CHPLClientHandshakePacketListenerImpl
ChunkGChunkGenerator
CLClientLevel
ClimSClimate.Sampler
CLisChatListener
CMChunkMap
CMapContextMap
CmdsCommands
CMTEChunkMap.TrackedEntity
CNUCollectingNeighborUpdater
ConnConnection
ConsConsumable
CPLClientPacketListener
CPListClimate.ParameterList
CraftMCraftingMenu
CRUComponentRenderUtils
CScrChatScreen
CSEDSynchedEntityData
CSPClientSuggestionProvider
CSTChunkStatusTasks
CSugCommandSuggestions
CTCombatTracker
CTDChunkTaskDispatcher
CWSCreateWorldScreen
DataCDataCommands
DBDoorBlock
DCPDataComponentPatch
DCSDialogControlSet
DlgCDialogCommand
DlgSDialogScreen
DMDistanceManager
DMRDefaultedMappedRegistry
DRWEDefaultRedstoneWireEvaluator
DSDedicatedServer
DScrDeathScreen
EAPEnvironmentAttributeProbe
EASEnvironmentAttributeSystem
EffCEffectCommands
EHEnchantmentHelper
EMEnchantmentMenu
EnchEnchantment
EntityEntity
ERDEntityRenderDispatcher
EScrEnchantmentScreen
ETEntityType
ETLEntityTickList
EVSEnvironmentAttributeSystem.ValueSampler
ExecCExecuteCommand
FBRFormattedBidiReorder
FDFoodData
FeatPFeaturePlacer
FeatureFeature
FFFlowingFluid
FGBFrameGraphBuilder
FMFurnaceMenu
FolPFoliagePlacer
FontFont
FPFoodProperties
FPHAIFirstPersonHandsAndItems
FPHAIRFirstPersonHandsAndItemsRenderer
FRFogRenderer
FRDFeatureRenderDispatcher
FSetFontSet
GCGiveCommand
GDGpuDevice
GGEGuiGraphicsExtractor
GIGameTestInfo
GlCEGlCommandEncoder
GlRPGlRenderPass
GpuSGpuSurface
GRGameRenderer
GSGaussianSampler
GStitGlyphStitcher
GTRGameTestRunner
GTTGameTestTicker
GuiGui
GuiRGuiRenderer
HudHud
ICTInventoryChangeTrigger
IgniteIgnite
InvSInventoryScreen
IOWIOWorker
IPItemParser
ISIntegratedServer
IStackItemStack
ItemItem
ItemsItems
JPJigsawPlacement
JPPJigsawPlacement.Placer
JSJigsawStructure
KHKeyboardHandler
KIKeyboardInput
KMKeyMapping
KTSKeyframeTrackSampler
LanguageLanguage
LBLiquidBlock
LCLevelChunk
LCSLevelChunkSection
LCTsLevelChunkTicks
LDSLevelDebugSynchronizers
LELivingEntity
LevBLeverBlock
LibraryLibrary
LICLootItemCondition
LIFLootItemFunctions
LLELevelLightEngine
LLSSLayerLightSectionStorage
LMLightmap
LOLoadingOverlay
LootCLootContext
LootPLootParams
LPLocalPlayer
LRLevelRenderer
LRSELightmapRenderStateExtractor
LSALevelStorageSource.LevelStorageAccess
LTLootTable
LTsLevelTicks
LXLevelExtractor
MCMinecraft
MEMobEffect
MEIMobEffectInstance
MFRModelFeatureRenderer
MNBSMultiNoiseBiomeSource
MobMob
MoveCMoveControl
MPGMMultiPlayerGameMode
MRMappedRegistry
MSMinecraftServer
MTSMoveToTargetSink
NBCNoiseBasedChunkGenerator
NbtIoNbtIo
NCNoiseChunk
NENodeEvaluator
PAPlayerAdvancements
ParrotParrot
PBBPistonBaseBlock
PChainPostChain
PCSPlayerChunkSender
PDecPacketDecoder
PDMPatchedDataComponentMap
PDSPlayerDataStorage
PEncPacketEncoder
PESMPersistentEntitySectionManager
PESPPoolElementStructurePiece
PFPathFinder
PLPlayerList
PlayerPlayer
PMPoiManager
PMBEPistonMovingBlockEntity
PModPlacementModifier
PNPathNavigation
PNRPathNavigationRegion
PPPacketProcessor
PPassPostPass
PSRPistonStructureResolver
PSTPrepareSpawnTask
RBRepeaterBlock
RCPLClientPacketListener
RDCRegistryDataCollector
RemSRemoteSlot
ResultCResultContainer
ResultSResultSlot
RGLReportGameListener
RMRecipeManager
RootPRootPlacer
RPRenderPass
RRMReloadableResourceManager
RSRReloadableServerResources
RSRegReloadableServerRegistries
RWBRedstoneWireBlock
SAScoreAccess
SABAbstractBoat
SAISpatialAttributeInterpolator
SBLSoundBufferLibrary
SCCServerChunkCache
SCDSerializableChunkData
SCLServerConnectionListener
SCPLServerConfigurationPacketListenerImpl
ScreenScreen
SDSServerDebugSubscribers
SEServerEntity
SectCSectionCompiler
SEDSynchedEntityData
SEESoundEngineExecutor
SGPLServerGamePacketListenerImpl
ShadMShaderManager
SheepSheep
SIBSleepInBed
SICFSetItemCountFunction
SLServerLevel
SLPLServerLoginPacketListenerImpl
SndESoundEngine
SNSSubmitNodeStorage
SPServerPlayer
SPBStructurePiecesBuilder
SPGMServerPlayerGameMode
SPieStrongholdPieces
SRSkyRenderer
SRBServerRecipeBook
SRDSectionRenderDispatcher
SRISimpleReloadInstance
SRTSynchronizeRegistriesTask
SSServerScoreboard
SSBSculkSensorBlock
SSplStringSplitter
SStrStrongholdStructure
SSVUSculkSensorBlockEntity.VibrationUser
STempStructureTemplate
SumCSummonCommand
SUTSectionUpdateTracker
SWServerWatchdog
TagPTagParser
TCTestCommand
TCCTransientCraftingContainer
TDecTreeDecorator
TDSSTrackingDebugSynchronizer.SourceSynchronizer
TFTreeFeature
TIBTestInstanceBlockEntity
TLTagLoader
TLEThreadedLevelLightEngine
TPTrunkPlacer
TrCTranslatableContents
TSTicketStorage
TVOTagValueOutput
UAFSUpdateActivityFromSchedule
VSelVibrationSelector
VSLVibrationSystem.Listener
VSTVibrationSystem.Ticker
WCUSWorldCreationUiState
WGLWorldGenLevel
WLWorldLoader
WOFWorldOpenFlows
WRWorldgenRandom
ZMZombieModel
ZRZombieRenderer
Auththe User Authenticator thread, not a class
Diskthe save on disk, not a class
Gamethe game’s own code above Blaze3D, not a class
Hookthe Server Shutdown Thread JVM hook, not a class
JVMthe process itself, not a class
Mainthe thread running whichever program’s Main the diagram is about — the client’s on the JVM main thread, the server’s on the thread the jar’s bundler starts — so not one class
Nettythe Netty event loop, not a class
Sessthe Mojang session service both machines call over HTTPS, not a class
Wirethe network between the two programs, not a class
Workerthe Util.backgroundExecutor pool, not a class

Class index

Generated by python tools/verify_names.py --index on every deploy. Do not edit by hand.

Every class backticked on a page — a system page, a Reference page, the atlas, the introduction or the lecture map — or named inside one of its figures (a lane, a node, a message; tools/check_figure_names.py reads those), and the pages that name it. Outside it: the generated pages: the eleven views, whose rows are read off the decompile, and the lane index, read off the page template’s key.

3169 names across 132 pages. A row is a simple name, not a class: a few names belong to more than one class (there are five Mains), and a few are library classes from Brigadier, DataFixerUpper or authlib.

classpages
AABBmath-and-primitives, VI · Entities, movement-and-collision
Abilitiesentity-anatomy, input-to-movement, player-anatomy
AboveRootPlacementtrees
AbsorptionMobEffectstatus-effects
AbstractArrownaming-drift, non-living-damage, the-client-level, damage-and-death, enchantments, using-an-item, what-the-client-is-told, the-sword-swing
AbstractBedBlocknaming-drift, points-of-interest
AbstractBoatthe-client-level, ai-goals-and-brains, authority, input-to-movement
AbstractButtongui-and-screens
AbstractChestBoatgui-and-screens
AbstractClientPlayerglossary, player-anatomy
AbstractConsecutiveExecutorchunk-generation-pipeline
AbstractContainerEventHandlerhierarchy
AbstractContainerMenublock-entities, diodes-and-observers, gui-and-screens, advancements, codecs-nbt-json, data-components, containers-and-menus, enchanting, loot-tables, recipes, using-an-item, the-two-phase-tick
AbstractContainerScreenhierarchy, gui-and-screens, containers-and-menus
AbstractCookingReciperecipes
AbstractCraftingMenurecipes
AbstractDebugCharthud
AbstractDeviceTrackersound-engine
AbstractEndPortalRendererblock-entity-rendering
AbstractFishentity-lifecycle
AbstractFurnaceBlockblock-entities
AbstractFurnaceBlockEntityblock-entities, recipes
AbstractFurnaceMenublock-entities, recipes
AbstractGameRulesScreenlevel-data-and-rules
AbstractHorsegui-and-screens, entity-lifecycle
AbstractHurtingProjectilenon-living-damage
AbstractMinecartthe-client-level, movement-and-collision
AbstractMountInventoryMenucontainers-and-menus
AbstractNautilusgui-and-screens
AbstractOreFeatureterrain
AbstractSchoolingFishentity-lifecycle
AbstractScrollAreagui-and-screens
AbstractSelectionListgui-and-screens
AbstractSignEditScreengui-and-screens
AbstractSignRendererblock-entity-rendering
AbstractSoundInstancenaming-drift, what-makes-a-sound
AbstractSpreadingStructurePlacementstructure-placement
AbstractTexturemodels-and-atlases
AbstractTickableSoundInstancesound-engine
AbstractVillagercontexts-and-predicates
AbstractWidgetglossary, gui-and-screens, what-makes-a-sound
AbstractZombieRendererentity-rendering
AcaciaFoliagePlacertrees
AccountProfileKeyPairManagerchat-and-signing
AcquirePoiai-goals-and-brains, pathfinding, points-of-interest
Actiondialogs, data-driven-types, text-components
ActionButtondialogs
ActionTypesdialogs
ActiveProfilerwhat-this-book-skips
ActiveTextCollectortext-and-fonts
Activityai-goals-and-brains, entity-anatomy, environment-attributes-and-timelines, points-of-interest
ActivityDataglossary, ai-goals-and-brains
AddressCheckthe-connection
AddValueenchantments
Advancementnaming-drift, advancements
AdvancementCommandsadvancements
AdvancementHoldernaming-drift, advancements
AdvancementNodenaming-drift, advancements
AdvancementProgressadvancements
AdvancementRequirementsadvancements
AdvancementRewardsadvancements, functions-and-macros, contexts-and-predicates
AdvancementsScreenadvancements
AdvancementTabadvancements
AdvancementTabTypeadvancements
AdvancementToasthud, advancements
AdvancementTreenaming-drift, advancements
AdvancementTypenaming-drift, advancements
AdvancementVisibilityEvaluatoradvancements
AdvancementWidgetadvancements
AgeableMobentity-anatomy, entity-lifecycle, synched-entity-data
AgeableMobRendererentity-rendering
Allayai-goals-and-brains, pathfinding, game-events-and-vibrations
AllayAigame-events-and-vibrations
AllOfenchantments
AlterGroundDecoratortrees
AlternativesEntryloot-tables
AmbientAdditionsSettingswhat-makes-a-sound
AmbientMoodSettingswhat-makes-a-sound
AmbientParticledata-driven-types, particles
AmphibiousNodeEvaluatorpathfinding
AmphibiousPathNavigationpathfinding
Animalentity-anatomy, entity-lifecycle, synched-entity-data
AnimationDefinitionentity-rendering
AnimationStateentity-rendering
AnvilMenuenchanting, hunger-and-experience
AnyBlockInteractionTriggercontexts-and-predicates
ApplyBonusCountenchantments
ApplyEntityImpulseenchantments
ApplyExhaustionenchantments, hunger-and-experience
ApplyMobEffectenchantments
ApplyStatusEffectsConsumeEffectusing-an-item, status-effects
AquaticFeatureswhat-this-book-skips
Aquiferglossary, terrain
AreaEffectCloudnon-living-damage
ARGBmath-and-primitives
ArgumentSignaturesbrigadier-and-commands, chat-and-signing
ArgumentTypeglossary
ArgumentTypeInfoglossary, brigadier-and-commands
ArgumentTypeInfosbrigadier-and-commands
ArgumentUtilsbrigadier-and-commands, permissions, data-driven-types
ArgumentVisitorbrigadier-and-commands
ArmedEntityRenderStateentity-rendering
ArmorModelSetentity-rendering
ArmorSlotenchantments
ArmorStandglossary, math-and-primitives, authority, damage-and-death, entity-anatomy, player-anatomy
ArmorTrimrecipes
ArrayVoxelShapemath-and-primitives
ArrowGizmodebugging-the-running-game
AssignProfessionFromJobSiteai-goals-and-brains, points-of-interest
AtlasGlyphProvidertext-and-fonts
AtlasIdswhat-this-book-skips, lightmap-fog-and-sky, models-and-atlases
AtlasManagernaming-drift, resource-system, models-and-atlases
AtlasSpritetext-components
AtmosphericFogEnvironmentlightmap-fog-and-sky
AttachedToLeavesDecoratortrees
AttachedToLogsDecoratortrees
AttachFacemath-and-primitives
Attackableentity-anatomy
AttackRangedata-components, VII · Items and inventories, the-spear, the-sword-swing
Attributeattributes
AttributeCommandattributes
AttributeInstanceattributes, status-effects
AttributeMapnaming-drift, attributes, what-the-client-is-told
AttributeModifiernaming-drift, attributes, enchantments, environment-attributes-and-timelines
AttributeRangeenvironment-attributes-and-timelines
Attributesnaming-drift, block-breaking, scoreboard-and-data, ai-goals-and-brains, attributes, damage-and-death, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, loot-tables, input-to-movement, player-anatomy, status-effects, the-spear, the-sword-swing
AttributeSupplierattributes
AttributeTrackglossary, environment-attributes-and-timelines
AttributeTrackSamplerenvironment-attributes-and-timelines
AttributeTypeenvironment-attributes-and-timelines
AttributeTypesdata-driven-types, environment-attributes-and-timelines
AudioStreamsound-engine
Avatarbiggest, glossary, VI · Entities, attributes, entity-anatomy, synched-entity-data, VIII · The player, player-anatomy, the-two-phase-tick, entity-rendering
AvatarRenderernaming-drift, player-anatomy, entity-rendering
AvatarRenderStateplayer-anatomy, entity-rendering
Axismath-and-primitives
AxisCyclemath-and-primitives
Axolotlai-goals-and-brains
BackendCreationExceptionnaming-drift, blaze3d, the-window
BackgroundMusicwhat-makes-a-sound
BackUpIfTooClosepathfinding
BadOmenMobEffectpoints-of-interest
BakedGlyphtext-and-fonts
BakedQuadnaming-drift, models-and-atlases
BakedSheetGlyphnaming-drift, text-and-fonts
BandwidthDebugCharthud
BandwidthDebugMonitordebugging-the-running-game
BanListEntryplayers-and-sessions
BannerDuplicateReciperecipes
BannerItemitems-and-stacks
BannerRendererblock-entity-rendering
BannerSpecialRendererblock-entity-rendering
BaseCommandBlockbrigadier-and-commands
BaseContainerBlockEntitycontexts-and-predicates, loot-tables
BaseEntityBlockhierarchy, block-entities, pistons-and-block-events
BasePressurePlateBlocksignal-and-dust
BaseRailBlockblocks-and-states, chunk-anatomy
BaseSpawnerblock-entities, entity-lifecycle
BeaconBlockEntityV · Blocks
BeaconMenucontainers-and-menus
BeaconRendererblock-entity-rendering
BeaconScreencontainers-and-menus
Beardifierdensity-function-nodes, contexts-and-predicates, density-functions, hand-built-structures, jigsaw-and-templates, structure-placement, terrain
BedPartpoints-of-interest
BedRenderStateblock-entity-rendering
BedRulenaming-drift
Beebiggest, debugging-the-running-game, pathfinding, points-of-interest
BeehiveBlockEntitydebugging-the-running-game, points-of-interest
BeehiveDecoratortrees
Behaviorglossary, ai-goals-and-brains
BehaviorBuilderai-goals-and-brains
BehaviorControlai-goals-and-brains
BellBlockEntitypistons-and-block-events
BelowZeroRetrogenchunk-anatomy, chunk-generation-pipeline, biomes, blending, terrain
BendingTrunkPlacertrees
BinaryFunctiondensity-function-nodes, naming-drift, density-functions
BinaryHeappathfinding
BindGroupLayoutblaze3d, post-processing
BindGroupLayoutsnaming-drift, blaze3d
Biomenaming-drift, identifiers-and-registries, lightmap-fog-and-sky, server-level-tick, environment-attributes-and-timelines, biomes, blending, density-functions
BiomeAmbientSoundsHandlerwhat-makes-a-sound
BiomeCheckentity-lifecycle
BiomeColorslightmap-fog-and-sky, biomes
BiomeFilterfeatures-and-placement
BiomeGenerationSettingsnaming-drift, biomes, features-and-placement
BiomeManagermath-and-primitives, biomes, terrain
BiomeResolverglossary, naming-drift, chunk-anatomy, biomes, blending
BiomeSourcedata-driven-types, biomes, structure-placement
BiomeSourcesbiomes
BiomeSpecialEffectswhat-makes-a-sound, lightmap-fog-and-sky, environment-attributes-and-timelines, biomes
BiomeTagstags
BitmapProvidertext-and-fonts
BitRandomSourcemath-and-primitives
BitSetDiscreteVoxelShapemath-and-primitives
BitStoragemath-and-primitives, chunk-anatomy
BlackholeTickAccessthe-client-level, scheduled-ticks
BlackstoneReplaceProcessorhand-built-structures
BlastFurnaceMenurecipes
Blazepathfinding
Blaze3DXI · Rendering
BlendDensityFunctiondensity-function-nodes, blending
BlendedNoisedensity-function-nodes, density-functions, terrain
Blenderdensity-function-nodes, glossary, contexts-and-predicates, chunk-generation-pipeline, blending, density-functions, terrain
BlendFunctionblaze3d
BlendingDataglossary, blending, density-functions
BlindnessFogEnvironmentlightmap-fog-and-sky
BlitRenderStatethe-gui-render-tree
BlobFoliagePlacertrees
Blockfanin, hierarchy, block-update-flags, glossary, V · Blocks, block-breaking, block-entities, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, prediction-and-acks, what-makes-a-sound, authority, movement-and-collision, identifiers-and-registries, tags, contexts-and-predicates, enchantments, items-and-stacks, particles, server-level-tick, chunk-anatomy, fluids, scheduled-ticks
BlockableEventLoopglossary, threads, anatomy, input-and-keybinds, the-client-loop, resource-system, the-connection, how-a-server-dies, players-and-sessions, server-tick, starting-a-server, chunk-generation-pipeline, points-of-interest, tickets-and-loading, creating-a-world
BlockAgeProcessorhand-built-structures
BlockAndLightGetterlighting
BlockAndTintGettersection-meshing
BlockAttachedEntitynon-living-damage, entity-anatomy, what-the-client-is-told, the-sword-swing
BlockBasedTestInstancegame-tests
BlockBehaviourfanin, hierarchy, block-update-flags, math-and-primitives, naming-drift, V · Blocks, block-breaking, block-entities, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, prediction-and-acks, what-makes-a-sound, movement-and-collision, identifiers-and-registries, tags, contexts-and-predicates, loot-tables, particles, server-level-tick, chunk-anatomy, fluids, lighting, scheduled-ticks
BlockBoxmath-and-primitives
BlockCollisionsmath-and-primitives, movement-and-collision
BlockColorsmodels-and-atlases, section-meshing
BlockDataAccessorscoreboard-and-data
BlockDataSourcetext-components
BlockDestructionProgressblock-breaking, block-entity-rendering
BlockEntityblock-update-flags, naming-drift, block-breaking, block-entities, blocks-and-states, diodes-and-observers, pistons-and-block-events, debugging-the-running-game, codecs-nbt-json, data-components, containers-and-menus, loot-tables, what-the-client-is-told, server-level-tick, chunk-anatomy
BlockEntityRenderDispatcherresource-system, block-entity-rendering
BlockEntityRendererglossary, block-entity-rendering
BlockEntityRendererProviderblock-entity-rendering
BlockEntityRenderersblock-entity-rendering
BlockEntityRenderStateblock-entity-rendering
BlockEntityTypeblock-entities
BlockEntityTypesblock-entities, block-entity-rendering
BlockEntityWithBoundingBoxRendererblock-entity-rendering
BlockEventDatapistons-and-block-events
BlockGettermovement-and-collision, game-events-and-vibrations
BlockHitResultmath-and-primitives, naming-drift, block-interaction
BlockIdswhat-this-book-skips, blocks-and-states, identifiers-and-registries
BlockIgnoreProcessorjigsaw-and-templates
BlockInputblock-update-flags, block-entities
BlockInWorldblocks-and-states
BlockItemhierarchy, naming-drift, blocks-and-states, data-components, VII · Items and inventories, section-meshing
BlockItemIdswhat-this-book-skips, blocks-and-states, identifiers-and-registries
BlockItemTagIdtags
BlockItemTagstags
BlockLightEnginelighting
BlockMarkermodels-and-atlases, particles
BlockMathmath-and-primitives
BlockModelblock-entity-rendering, models-and-atlases
BlockModelFeatureRenderersubmit-phases
BlockModelGeneratorsbiggest
BlockModelLightersection-meshing
BlockModelRenderStateblock-entity-rendering
BlockModelResolverblock-entity-rendering
BlockModelSetnaming-drift
BlockPatternblocks-and-states
BlockPatternBuilderblocks-and-states
BlockPosfanin, math-and-primitives, V · Blocks, signal-and-dust, scheduled-ticks
BlockPosArgumentbrigadier-and-commands
BlockPositionSourcegame-events-and-vibrations
BlockPredicatedata-driven-types, features-and-placement
BlockPredicateFilterfeatures-and-placement
BlockPredicateTypedata-driven-types
BlockQuadOutputsection-meshing
BlockRotProcessorjigsaw-and-templates
Blocksbiggest, fanin, packages, naming-drift, what-this-book-skips, block-breaking, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, entity-lifecycle, movement-and-collision, identifiers-and-registries, particles, chunk-anatomy, fluids, lighting, scheduled-ticks
BlocksAttacksdamage-and-death, data-components
BlockSetTypeblock-interaction, blocks-and-states
BlockStatefanin, glossary, naming-drift, blocks-and-states, models-and-atlases, the-frame, chunk-anatomy, fluids
BlockStateArgumentbrigadier-and-commands
BlockStateDatabiggest
BlockStateModelnaming-drift, models-and-atlases
BlockStateModelDispatchermodels-and-atlases
BlockStateModelLoadermodels-and-atlases
BlockStateModelPartsubmit-phases, block-entity-rendering, models-and-atlases
BlockStateModelSetnaming-drift, block-entity-rendering, models-and-atlases, section-meshing
BlockStateModelWrappermodels-and-atlases
BlockStateParserblocks-and-states, brigadier-and-commands
BlockStatePredicateblocks-and-states
BlockStatePredictionHandlerglossary, block-interaction, prediction-and-acks, input-to-movement
BlockStatePropertiesblocks-and-states, fluids, trees
BlockStateProviderdata-driven-types, features-and-placement, trees
BlockTagsglossary, block-breaking, block-interaction, pistons-and-block-events, movement-and-collision, tags, enchanting, chunk-anatomy, game-events-and-vibrations, points-of-interest, creating-a-world, terrain
BlockTintCachethe-client-level
BlockTransformeritems-and-stacks
BlockTransformersdata-components
BlockUtilmath-and-primitives, naming-drift
BodyRotationControlpathfinding
Boggedsynched-entity-data
BoneMealItemitems-and-stacks, using-an-item
BonusLevelTableConditionenchantments
BookCloningReciperecipes
BookSpecialRendererblock-entity-rendering
BookViewScreengui-and-screens
BooleanInputdialogs
BooleanModifierenvironment-attributes-and-timelines
BooleanOpmath-and-primitives
BooleanPropertyblocks-and-states
Bootstrapanatomy, brigadier-and-commands, entity-selectors, data-components, data-driven-types, identifiers-and-registries, how-a-server-dies, starting-a-server
BootstrapContextwhat-this-book-skips, identifiers-and-registries
BorderChangeListenerlevel-data-and-rules
BorderStatuslevel-data-and-rules
BossBarCommandsscoreboard-and-data
BossEventscoreboard-and-data
BossHealthOverlayhud-elements, hud
BoundingBoxmath-and-primitives, hand-built-structures
BowItemdata-components, using-an-item
Brainglossary, ai-goals-and-brains, entity-anatomy, what-the-client-is-told, environment-attributes-and-timelines, points-of-interest
BrainDebugRendererdebugging-the-running-game
BrandPayloadpackets-and-stream-codecs, protocol-phases
BreakDoorGoalai-goals-and-brains
BreakingItemParticleparticles
Breezedebugging-the-running-game
BrewingInputrecipes
BrewingStandBlockEntityrecipes
BrewingStandMenucontainers-and-menus, recipes
BrigadierExceptionsbrigadier-and-commands
Brightnessmath-and-primitives
Brokenmodels-and-atlases
BrushableBlockEntitycontexts-and-predicates
BrushableBlockRenderStateblock-entity-rendering
BrushItemusing-an-item
BubbleColumnAmbientSoundHandlerwhat-makes-a-sound
BubbleColumnBlockfluids
Bucketableentity-anatomy
BucketItemVII · Items and inventories, fluids
BufferBuildernaming-drift, blaze3d
BuildContextsnaming-drift, functions-and-macros, scoreboard-and-data, the-execution-engine
BuiltInBlockModelsglossary, block-entity-rendering, models-and-atlases
BuiltinDimensionTypeslevel-data-and-rules
BuiltInLootTablesnaming-drift, synched-entity-data, loot-tables
BuiltInPackSourceresource-system
BuiltInRegistriesfanin, density-function-nodes, level-data-and-rules, naming-drift, what-this-book-skips, blocks-and-states, debugging-the-running-game, advancements, dialogs, game-tests, permissions, ai-goals-and-brains, attributes, entity-lifecycle, II · Foundations, data-components, data-driven-types, identifiers-and-registries, resource-system, tags, containers-and-menus, contexts-and-predicates, enchantments, items-and-stacks, using-an-item, status-effects, chunk-generation-pipeline, environment-attributes-and-timelines, fluids, game-events-and-vibrations, points-of-interest, tickets-and-loading, density-functions, features-and-placement, structure-placement
BuiltinStructuresstructure-placement
BuiltinStructureSetsstructure-placement
BuiltinTestFunctionsgame-tests
BulkSectionAccesschunk-anatomy, chunk-generation-pipeline
BundleContentsdata-components, items-and-stacks
BundleDelimiterPacketpackets-and-stream-codecs
BundleFullnessmodels-and-atlases
BundleItemcontainers-and-menus, using-an-item
BundlePacketpackets-and-stream-codecs
BundlerInfopackets-and-stream-codecs
BundleSelectedItemSpecialRenderermodels-and-atlases
BuriedTreasurePieceshand-built-structures
BushFoliagePlacertrees
Buttongui-and-screens
ButtonBlocksignal-and-dust
ButtonListDialogdialogs
ButtonListDialogScreendialogs
ByIdMapentity-anatomy
ByteArrayTagcodecs-nbt-json
ByteBufCodecsfanin, naming-drift, blocks-and-states, entity-anatomy, codecs-nbt-json, identifiers-and-registries, tags, text-components, packets-and-stream-codecs
ByteBufferBuildernaming-drift, blaze3d
CacheableFunctionnaming-drift, advancements, functions-and-macros
CachedParseStatebrigadier-and-commands
CachedUserNameToIdResolverplayers-and-sessions
CacheFunctiondensity-function-nodes, naming-drift, density-functions
CachingDensitySamplerdensity-function-nodes, density-functions
CakeBlockblock-interaction, hunger-and-experience
CalibratedSculkSensorBlockgame-events-and-vibrations
CalibratedSculkSensorBlockEntitygame-events-and-vibrations
CallbackDeviceTrackersound-engine
CallFunctionfunctions-and-macros, the-execution-engine
Cameraglossary, naming-drift, sound-engine, block-entity-rendering, the-frame, environment-attributes-and-timelines
CameraRenderStateglossary, lightmap-fog-and-sky, the-frame
CampfireBlockrecipes
CampfireBlockEntityrecipes
CampfireRenderStateblock-entity-rendering
CanyonWorldCarverterrain
CapeLayerentity-rendering
CappedProcessorjigsaw-and-templates
CardinalLightinglightmap-fog-and-sky, section-meshing
CartographyTableMenucontainers-and-menus
CarverOutputterrain
CarvingMasknaming-drift, blending, terrain
Catentity-lifecycle
CatSpawnerentity-lifecycle, starting-a-server, points-of-interest
CauldronInteractionsidentifiers-and-registries
CaveFeatureswhat-this-book-skips
CaveWorldCarverterrain
ChainModifiersthe-execution-engine
ChangeItemDamageenchantments
Channelsound-engine
ChannelAccesssound-engine
CharacterEventnaming-drift, input-and-keybinds
ChargedProjectilesitems-and-stacks
ChaseClientthreads
ChaseCommandbrigadier-and-commands
ChaseServerthreads
ChatAbilitiespermissions, chat-and-signing
ChatComponenthud-elements, hud, text-and-fonts
ChatDecoratorchat-and-signing
ChatFormattingnaming-drift, text-components, status-effects
ChatListenerhud, text-components, chat-and-signing
ChatLoghud
ChatReportContextBuilderchat-and-signing
ChatRestrictionpermissions
ChatScreengui-and-screens, brigadier-and-commands, chat-and-signing
ChatTrustLevelhud, chat-and-signing
ChatTypechat-and-signing
ChatTypeDecorationchat-and-signing
ChatVisiblitychat-and-signing
Checkboxgui-and-screens
CheckerboardColumnBiomeSourcebiomes
CherryFoliagePlacertrees
CherryTrunkPlacertrees
ChestBlockblock-entities, containers-and-menus, loot-tables
ChestBlockEntityblock-entities, pistons-and-block-events, codecs-nbt-json, data-driven-types, containers-and-menus, loot-tables
ChestMenucontainers-and-menus, loot-tables
ChestModelblock-entity-rendering
ChestRendererblock-entity-rendering
ChestRenderStateblock-entity-rendering
ChestSpecialRendererblock-entity-rendering
ChestTypeblocks-and-states
Chickenentity-lifecycle
ChiseledBookShelfBlockblock-interaction
ChunkAccessglossary, level-data-and-rules, naming-drift, blocks-and-states, entity-lifecycle, server-level-tick, chunk-anatomy, chunk-generation-pipeline, chunk-storage, game-events-and-vibrations, lighting, biomes, blending, terrain
ChunkBatchSizeCalculatorthreads, what-the-client-is-told
ChunkedSampleByteBufsound-engine
ChunkGenerationTaskchunk-generation-pipeline
ChunkGeneratorlevel-data-and-rules, entity-lifecycle, data-driven-types, what-the-client-is-told, starting-a-server, chunk-generation-pipeline, biomes, creating-a-world, density-functions, features-and-placement, hand-built-structures, jigsaw-and-templates, structure-placement, terrain
ChunkGeneratorsterrain
ChunkGeneratorStructureStatestructure-placement
ChunkHolderblock-entities, block-interaction, what-the-client-is-told, how-a-server-dies, server-level-tick, chunk-generation-pipeline, chunk-storage, lighting, scheduled-ticks, tickets-and-loading
ChunkLevelglossary, server-level-tick, chunk-generation-pipeline, chunk-storage, tickets-and-loading
ChunkLoadCounterplayers-and-sessions, starting-a-server, chunk-generation-pipeline
ChunkMapintroduction, biggest, packages, math-and-primitives, anatomy, options, attributes, entity-anatomy, entity-lifecycle, synched-entity-data, what-the-client-is-told, III · The server, how-a-server-dies, players-and-sessions, server-level-tick, server-tick, chunk-generation-pipeline, chunk-storage, lighting, points-of-interest, scheduled-ticks, tickets-and-loading, blending, terrain
ChunkPoslevel-data-and-rules, math-and-primitives, naming-drift, IV · The world, chunk-generation-pipeline, chunk-storage
ChunkPyramidmath-and-primitives, chunk-generation-pipeline, lighting, tickets-and-loading, XII · World generation, biomes
ChunkResulttickets-and-loading
ChunkScanAccesschunk-storage, structure-placement
ChunkSectionLayerglossary, naming-drift, models-and-atlases, section-meshing
ChunkSectionLayerGroupvisibility-and-the-frame-graph
ChunkSectionsToRendervisibility-and-the-frame-graph
ChunkSkyLightSourceschunk-anatomy, lighting
ChunkSourcepathfinding, chunk-anatomy, tickets-and-loading
ChunkStatuslectures, naming-drift, entity-anatomy, starting-a-server, chunk-anatomy, chunk-generation-pipeline, chunk-storage, game-events-and-vibrations, lighting, points-of-interest, scheduled-ticks, tickets-and-loading, XII · World generation, biomes, blending, density-functions, features-and-placement, hand-built-structures, jigsaw-and-templates, structure-placement, terrain
ChunkStatusTasksblock-entities, chunk-anatomy, chunk-generation-pipeline, chunk-storage, lighting, scheduled-ticks, blending, creating-a-world, features-and-placement, structure-placement, terrain
ChunkStepglossary, chunk-generation-pipeline
ChunkTaskDispatcherthreads, anatomy, chunk-generation-pipeline, lighting
ChunkTaskPriorityQueuechunk-generation-pipeline
ChunkTrackerlighting, tickets-and-loading
ChunkTrackingViewwhat-the-client-is-told, players-and-sessions, tickets-and-loading
ChunkTypechunk-anatomy, chunk-generation-pipeline, chunk-storage
CipherDecoderthe-connection
CipherEncoderthe-connection
CircleGizmodebugging-the-running-game
ClampFunctiondensity-function-nodes
ClassInstanceMultiMapentity-lifecycle
ClassTreeIdRegistrysynched-entity-data
Clearableloot-tables
ClearAllStatusEffectsConsumeEffectusing-an-item
ClickActioncontainers-and-menus
ClickEventdialogs, text-components
ClientAdvancementsnaming-drift, advancements
ClientAvatarEntityplayer-anatomy
ClientBootstrapanatomy
ClientboundAddEntityPacketnaming-drift, entity-anatomy, entity-lifecycle, synched-entity-data, what-the-client-is-told
ClientboundAddTransientBlockPacketserver-level-tick
ClientboundAnimatePacketthe-sword-swing
ClientboundBlockChangedAckPacketblock-breaking, block-interaction, prediction-and-acks, what-the-client-is-told
ClientboundBlockDestructionPacketblock-breaking, what-the-client-is-told
ClientboundBlockEntityDataPacketblock-entities, packets-and-stream-codecs
ClientboundBlockEventPacketpistons-and-block-events, what-the-client-is-told, server-level-tick
ClientboundBlockUpdatePacketblock-breaking, block-entities, block-interaction, blocks-and-states, prediction-and-acks, what-the-client-is-told, server-level-tick, fluids
ClientboundBossEventPacketscoreboard-and-data, IX · Networking
ClientboundBundleDelimiterPacketpackets-and-stream-codecs
ClientboundBundlePacketsynched-entity-data, packets-and-stream-codecs, what-the-client-is-told
ClientboundChangeDifficultyPacketlevel-data-and-rules
ClientboundChunkBatchFinishedPacketwhat-the-client-is-told
ClientboundChunkBatchStartPacketwhat-the-client-is-told
ClientboundChunksBiomesPacketwhat-the-client-is-told, chunk-anatomy, biomes
ClientboundClearDialogPacketdialogs
ClientboundCodeOfConductPacketprotocol-phases
ClientboundCommandsPacketbrigadier-and-commands, permissions
ClientboundCommandSuggestionsPacketbrigadier-and-commands
ClientboundContainerSetContentPacketdata-components, containers-and-menus, loot-tables
ClientboundContainerSetDataPacketblock-entities, containers-and-menus, enchanting
ClientboundContainerSetSlotPacketnaming-drift, codecs-nbt-json, data-components, containers-and-menus, recipes
ClientboundCookieRequestPacketprotocol-phases
ClientboundCooldownPacketprediction-and-acks, using-an-item
ClientboundCustomChatCompletionsPacketbrigadier-and-commands
ClientboundCustomPayloadPacketpackets-and-stream-codecs
ClientboundCustomQueryPacketprotocol-phases
ClientboundDamageEventPacketdamage-and-death, what-the-client-is-told, the-sword-swing
ClientboundDebugBlockValuePacketdebugging-the-running-game
ClientboundDebugChunkValuePacketdebugging-the-running-game
ClientboundDebugEntityValuePacketdebugging-the-running-game
ClientboundDebugEventPacketdebugging-the-running-game
ClientboundDebugSamplePacketdebugging-the-running-game
ClientboundDisconnectPackettext-components, the-connection
ClientboundDisguisedChatPacketchat-and-signing, packets-and-stream-codecs
ClientboundEntityEventPacketlevel-data-and-rules, permissions, damage-and-death, entity-anatomy, synched-entity-data, using-an-item, hunger-and-experience, players-and-sessions
ClientboundEntityPositionSyncPacketnaming-drift, authority, entity-anatomy, movement-and-collision, synched-entity-data, what-the-client-is-told
ClientboundExplodePacketthe-client-level, what-makes-a-sound, particles
ClientboundFinishConfigurationPacketidentifiers-and-registries, protocol-phases
ClientboundForgetLevelChunkPacketwhat-the-client-is-told, tickets-and-loading
ClientboundGameEventPacketlevel-data-and-rules, what-the-client-is-told, player-anatomy, players-and-sessions, server-level-tick
ClientboundGameRuleValuesPacketlevel-data-and-rules
ClientboundGameTestHighlightPosPacketgame-tests
ClientboundHelloPacketnaming-drift, protocol-phases, the-connection
ClientboundHurtAnimationPacketdamage-and-death
ClientboundInitializeBorderPacketlevel-data-and-rules
ClientboundKeepAlivePacketpackets-and-stream-codecs
ClientboundLevelChunkPacketDatachunk-anatomy
ClientboundLevelChunkWithLightPacketnaming-drift, what-the-client-is-told, chunk-anatomy, lighting
ClientboundLevelEventPacketblock-breaking, what-makes-a-sound, particles
ClientboundLevelParticlesPacketparticles
ClientboundLightUpdatePacketserver-level-tick, lighting
ClientboundLightUpdatePacketDatachunk-anatomy, lighting
ClientboundLoginCompressionPacketnaming-drift, the-connection
ClientboundLoginDisconnectPacketcodecs-nbt-json, packets-and-stream-codecs, protocol-phases, the-connection
ClientboundLoginFinishedPacketnaming-drift, protocol-phases
ClientboundLoginPacketlevel-data-and-rules, naming-drift, chat-and-signing, protocol-phases, player-anatomy, players-and-sessions
ClientboundMapItemDataPacketlevel-data-and-rules
ClientboundMountScreenOpenPacketnaming-drift, gui-and-screens, containers-and-menus
ClientboundMoveEntityPacketauthority, movement-and-collision, synched-entity-data, what-the-client-is-told
ClientboundMoveMinecartPacketsynched-entity-data, what-the-client-is-told
ClientboundMoveVehiclePacketauthority, input-to-movement
ClientboundOpenScreenPacketgui-and-screens, containers-and-menus, loot-tables
ClientboundPacketListenerpackets-and-stream-codecs
ClientboundPlaceGhostRecipePacketrecipes
ClientboundPlayerAbilitiesPacketplayer-anatomy
ClientboundPlayerChatPacketchat-and-signing, packets-and-stream-codecs
ClientboundPlayerCombatKillPacketdamage-and-death, text-components, packets-and-stream-codecs
ClientboundPlayerInfoRemovePacketplayers-and-sessions
ClientboundPlayerInfoUpdatePacketplayer-anatomy, server-tick
ClientboundPlayerPositionPacketinput-to-movement
ClientboundPlayerRotationPacketinput-to-movement
ClientboundPongResponsePacketthreads, protocol-phases
ClientboundPostEffectsPacketpost-processing
ClientboundProjectilePowerPacketwhat-the-client-is-told
ClientboundRecipeBookAddPacketrecipes
ClientboundRegistryDataPacketidentifiers-and-registries, protocol-phases
ClientboundRemoveEntitiesPacketentity-anatomy, entity-lifecycle, what-the-client-is-told
ClientboundRemoveMobEffectPacketsynched-entity-data, status-effects
ClientboundResetChatPacketprotocol-phases
ClientboundResetScorePacketscoreboard-and-data
ClientboundResourcePackPopPacketresource-system
ClientboundResourcePackPushPacketnaming-drift, resource-system
ClientboundRespawnPacketplayer-anatomy, players-and-sessions
ClientboundRotateHeadPacketsynched-entity-data
ClientboundSectionBlocksUpdatePacketblock-entities, block-interaction, blocks-and-states, packets-and-stream-codecs, what-the-client-is-told, server-level-tick, fluids
ClientboundSelectKnownPacksidentifiers-and-registries, protocol-phases
ClientboundServerDataPackettext-components
ClientboundServerLinksPacketprotocol-phases
ClientboundSetBorderCenterPacketlevel-data-and-rules
ClientboundSetBorderLerpSizePacketlevel-data-and-rules
ClientboundSetBorderSizePacketlevel-data-and-rules
ClientboundSetBorderWarningDelayPacketlevel-data-and-rules
ClientboundSetBorderWarningDistancePacketlevel-data-and-rules
ClientboundSetChunkCacheCenterPacketnaming-drift, tickets-and-loading
ClientboundSetChunkCacheRadiusPacketnaming-drift, options, what-the-client-is-told
ClientboundSetCursorItemPacketnaming-drift, data-components, containers-and-menus
ClientboundSetDefaultSpawnPositionPacketlevel-data-and-rules
ClientboundSetDisplayObjectivePacketscoreboard-and-data
ClientboundSetEntityDataPacketdamage-and-death, entity-anatomy, synched-entity-data
ClientboundSetEntityLinkPacketwhat-the-client-is-told
ClientboundSetEntityMotionPacketdamage-and-death, movement-and-collision, synched-entity-data, what-the-client-is-told
ClientboundSetEquipmentPacketsynched-entity-data, data-components, what-the-client-is-told
ClientboundSetExperiencePackethunger-and-experience, the-two-phase-tick
ClientboundSetHealthPacketdamage-and-death, using-an-item, packets-and-stream-codecs, hunger-and-experience, the-two-phase-tick
ClientboundSetHeldSlotPacketnaming-drift, containers-and-menus, player-anatomy
ClientboundSetObjectivePacketlevel-data-and-rules, scoreboard-and-data
ClientboundSetPassengersPacketwhat-the-client-is-told
ClientboundSetPlayerInventoryPacketdata-components, containers-and-menus, player-anatomy
ClientboundSetPlayerTeamPacketlevel-data-and-rules, scoreboard-and-data, packets-and-stream-codecs
ClientboundSetScorePacketlevel-data-and-rules, scoreboard-and-data
ClientboundSetSimulationDistancePacketoptions, what-the-client-is-told
ClientboundSetTimePacketlevel-data-and-rules, server-level-tick, server-tick, environment-attributes-and-timelines
ClientboundShowDialogPacketdialogs, packets-and-stream-codecs
ClientboundSoundEntityPacketwhat-makes-a-sound
ClientboundSoundPacketblock-interaction, pistons-and-block-events, signal-and-dust, what-makes-a-sound, the-sword-swing
ClientboundStartConfigurationPacketprotocol-phases, players-and-sessions
ClientboundStatusResponsePacketcodecs-nbt-json, packets-and-stream-codecs
ClientboundStopSoundPacketwhat-makes-a-sound
ClientboundStoreCookiePacketprotocol-phases
ClientboundSwingAnimationPacketnaming-drift, the-connection, the-sword-swing
ClientboundSystemChatPackettext-components, packets-and-stream-codecs
ClientboundTagQueryPacketpackets-and-stream-codecs
ClientboundTakeItemEntityPackethunger-and-experience
ClientboundTeleportEntityPacketsynched-entity-data, what-the-client-is-told
ClientboundTickingStatePacketserver-tick
ClientboundTickingStepPacketserver-tick
ClientboundTransferPacketprotocol-phases
ClientboundUpdateAdvancementsPacketadvancements
ClientboundUpdateAttributesPacketattributes, synched-entity-data, status-effects
ClientboundUpdateEnabledFeaturesPacketprotocol-phases
ClientboundUpdateMobEffectPacketsynched-entity-data, status-effects
ClientboundUpdateRecipesPacketresource-system, recipes
ClientboundUpdateTagsPacketidentifiers-and-registries, resource-system, tags, enchantments, protocol-phases
ClientChunkCachethe-client-level, visibility-and-the-frame-graph, chunk-anatomy, lighting
ClientClockManagerthe-client-level, environment-attributes-and-timelines
ClientCommonPacketListenerpackets-and-stream-codecs
ClientCommonPacketListenerImplbiggest, threads, the-client-loop, dialogs, protocol-phases, the-connection
ClientConfigurationPacketListenerImpldata-components, identifiers-and-registries, tags, protocol-phases
ClientDebugSubscriberdebugging-the-running-game
ClientExplosionTrackerparticles
ClientGamePacketListenerpackets-and-stream-codecs
ClientHandshakePacketListenerImplanatomy, options, protocol-phases, the-connection
ClientInformationoptions, players-and-sessions
ClientInputglossary, input-to-movement, player-anatomy, the-two-phase-tick
ClientIntentprotocol-phases
ClientIntentionPacketprotocol-phases, the-connection
ClientItemmodels-and-atlases
ClientItemInfoLoadermodels-and-atlases
ClientLanguagetext-and-fonts, text-components
ClientLevelintroduction, packages, glossary, level-data-and-rules, math-and-primitives, threads, anatomy, block-breaking, block-entities, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, X · The client, prediction-and-acks, sound-engine, the-client-level, the-client-loop, what-makes-a-sound, entity-anatomy, entity-lifecycle, movement-and-collision, synched-entity-data, contexts-and-predicates, using-an-item, what-the-client-is-told, input-to-movement, player-anatomy, the-two-phase-tick, block-entity-rendering, entity-rendering, lightmap-fog-and-sky, particles, section-meshing, the-frame, visibility-and-the-frame-graph, chunk-anatomy, environment-attributes-and-timelines, fluids, game-events-and-vibrations, lighting, scheduled-ticks, biomes
ClientMannequinnaming-drift, player-anatomy
ClientPacketListenerbiggest, packages, glossary, threads, anatomy, block-entities, block-interaction, pistons-and-block-events, debugging-the-running-game, hud, options, prediction-and-acks, the-client-level, what-makes-a-sound, advancements, brigadier-and-commands, dialogs, permissions, scoreboard-and-data, attributes, authority, damage-and-death, entity-anatomy, movement-and-collision, synched-entity-data, codecs-nbt-json, data-components, identifiers-and-registries, tags, text-components, containers-and-menus, items-and-stacks, chat-and-signing, packets-and-stream-codecs, protocol-phases, the-connection, what-the-client-is-told, hunger-and-experience, input-to-movement, player-anatomy, status-effects, the-sword-swing, particles, chunk-anatomy, fluids, lighting
ClientPackSourceresource-system
ClientRecipeBookrecipes, player-anatomy
ClientRecipeContainerrecipes
ClientRegistryLayeridentifiers-and-registries
ClientShutdownWatchdogthreads, the-client-loop, the-window
ClientStatusPacketListenerprotocol-phases
ClientSuggestionProviderbrigadier-and-commands, entity-selectors
ClientTelemetryManagerwhat-this-book-skips
ClientWaypointManagerhud
Climateglossary, XII · World generation, biomes, density-functions
ClipBlockStateContextgame-events-and-vibrations
ClockInstanceenvironment-attributes-and-timelines
ClockManagerenvironment-attributes-and-timelines
ClockNetworkStateenvironment-attributes-and-timelines
ClockStateenvironment-attributes-and-timelines
ClockTimeMarkerenvironment-attributes-and-timelines
ClockTimeMarkersserver-level-tick, environment-attributes-and-timelines
CloneCommandsXIII · Commands and data packs, scheduled-ticks
CloudRendererlightmap-fog-and-sky
CloudStatuslightmap-fog-and-sky
CocoaDecoratortrees
Codecfanin, glossary, level-data-and-rules, codecs-nbt-json, data-components, data-driven-types, text-components, packets-and-stream-codecs
CodecModifierpackets-and-stream-codecs
CodepointMaptext-and-fonts
CollectFieldscodecs-nbt-json
CollectingNeighborUpdaterblock-update-flags, block-interaction, blocks-and-states, signal-and-dust, server-level-tick
CollectionContentsPredicateadvancements
CollectionCountsPredicateadvancements
CollectionPredicateadvancements
CollectionTagcodecs-nbt-json
CollisionContextmath-and-primitives
CollisionGettermovement-and-collision
ColoredRectangleRenderStatethe-gui-render-tree
ColorResolverlightmap-fog-and-sky, biomes
ColorRGBAmath-and-primitives
ColorTargetStateblaze3d
CombatEntrydamage-and-death, text-components
CombatRulesdamage-and-death
CombatTrackernon-living-damage, damage-and-death, text-components
CommandBlockbrigadier-and-commands
CommandBlockEntitybrigadier-and-commands
CommandBuildContextbrigadier-and-commands, identifiers-and-registries
CommandDispatcherbrigadier-and-commands
CommandEncoderglossary, blaze3d, the-frame
CommandEncoderBackendblaze3d
CommandFunctionglossary, functions-and-macros, tags
CommandQueueEntrythe-execution-engine
CommandResponseTrackerbrigadier-and-commands
CommandResultCallbacknaming-drift, the-execution-engine
Commandsnaming-drift, what-this-book-skips, brigadier-and-commands, functions-and-macros, game-tests, permissions, the-execution-engine, resource-system, how-a-server-dies, players-and-sessions
CommandSigningContextbrigadier-and-commands
CommandSourcebrigadier-and-commands, the-execution-engine
CommandSourceStacknaming-drift, brigadier-and-commands, entity-selectors, functions-and-macros, permissions, the-execution-engine, starting-a-server
CommandStoragelevel-data-and-rules, scoreboard-and-data, starting-a-server
CommandSuggestionsbrigadier-and-commands
CommandSyntaxExceptionscoreboard-and-data, the-execution-engine, codecs-nbt-json
CommandTemplatedialogs
CommonButtonDatadialogs
CommonColorsmath-and-primitives
CommonComponentstext-components
CommonDialogDatadialogs
CommonListenerCookieidentifiers-and-registries, protocol-phases, players-and-sessions
CommonPlayerSpawnInfolevel-data-and-rules, player-anatomy, players-and-sessions
ComparatorBlockhierarchy, diodes-and-observers, pistons-and-block-events, scheduled-ticks
ComparatorBlockEntitydiodes-and-observers, pistons-and-block-events
ComparatorModediodes-and-observers
CompassAnglemodels-and-atlases
CompassItemitems-and-stacks
CompiledSectionMeshglossary, naming-drift, section-meshing
Componentlectures, fanin, packages, glossary, X · The client, gui-and-screens, hud, text-and-fonts, brigadier-and-commands, functions-and-macros, text-components, enchantments, IX · Networking, chat-and-signing, packets-and-stream-codecs, protocol-phases, players-and-sessions
ComponentArgumentbrigadier-and-commands, text-components
ComponentCollectortext-and-fonts
ComponentContentstext-components
ComponentPathgui-and-screens
ComponentPredicateParserbrigadier-and-commands
ComponentRenderUtilstext-and-fonts
ComponentSerializationnaming-drift, entity-selectors, codecs-nbt-json, text-components, packets-and-stream-codecs
ComponentUtilsnaming-drift, text-components
ComposableEntryContainerloot-tables
CompositeBlockModelblock-entity-rendering
CompositeDirectionmath-and-primitives, blending
CompositeEntryBaseloot-tables
CompositeModelmodels-and-atlases
ComposterBlockblock-interaction, contexts-and-predicates
CompoundContainercontainers-and-menus, loot-tables
CompoundTagglossary, block-entities, codecs-nbt-json, packets-and-stream-codecs, chunk-anatomy, chunk-storage
CompressionDecoderpackets-and-stream-codecs, the-connection
CompressionEncoderthe-connection
ConcentricRingsStructurePlacementstructure-placement
ConcurrentHolderGetteridentifiers-and-registries
ConditionalBlockModelblock-entity-rendering
ConditionalEffectenchantments
ConditionalItemModelnaming-drift, models-and-atlases
ConditionalItemModelPropertiesmodels-and-atlases
ConduitBlockEntityV · Blocks
Configurationnaming-drift, chunk-anatomy
ConfigurationProtocolspackets-and-stream-codecs, protocol-phases
ConfigurationTaskprotocol-phases, players-and-sessions
ConfirmationDialogdialogs
ConfirmExperimentalFeaturesScreencreating-a-world
ConfirmScreenpermissions
Connectionnaming-drift, threads, anatomy, debugging-the-running-game, codecs-nbt-json, data-components, packets-and-stream-codecs, protocol-phases, the-connection, the-two-phase-tick, players-and-sessions, server-level-tick, server-tick
ConnectionProtocolpackets-and-stream-codecs, protocol-phases, the-connection
ConnectScreenprotocol-phases, the-connection
ConsecutiveExecutoranatomy, resource-system, chunk-generation-pipeline, chunk-storage, lighting
ConsoleInputstarting-a-server
ConstantFunctiondensity-function-nodes, data-driven-types, density-functions
ConstantValuecontexts-and-predicates
Consumablenaming-drift, data-components, data-driven-types, using-an-item, hunger-and-experience
ConsumableListenerusing-an-item, hunger-and-experience
Consumablesusing-an-item
ConsumeEffectdata-driven-types, using-an-item, packets-and-stream-codecs
ConsumeItemTriggercontexts-and-predicates
Containernaming-drift, block-entities, containers-and-menus, loot-tables, recipes, player-anatomy
ContainerDatacontainers-and-menus
ContainerEntitycontexts-and-predicates
ContainerEventHandlergui-and-screens
ContainerHelpercodecs-nbt-json
ContainerInputnaming-drift, gui-and-screens, containers-and-menus
ContainerLevelAccessdata-components, containers-and-menus, enchanting, recipes
ContainerListeneradvancements, containers-and-menus
ContainerObjectSelectionListgui-and-screens
Containersblock-entities
ContainerSingleItemscheduled-ticks
ContainerSynchronizercontainers-and-menus
ContainerUserentity-anatomy, player-anatomy
ContentsSlotSourcecontexts-and-predicates
ContextBoundSamplerdensity-function-nodes, density-functions
ContextChainscoreboard-and-data
ContextDimensionmodels-and-atlases
ContextFloatProvidernaming-drift, data-driven-types, contexts-and-predicates
ContextFloatProvidersdata-driven-types, contexts-and-predicates
ContextIntProvidernaming-drift, data-driven-types, contexts-and-predicates
ContextIntProvidersdata-driven-types, contexts-and-predicates
ContextKeyglossary, naming-drift, contexts-and-predicates
ContextKeySetglossary, naming-drift, contexts-and-predicates
ContextMapcontexts-and-predicates, recipes
ContextualBarhud-elements, hud
ContinuationTaskfunctions-and-macros, the-execution-engine
ContinuousProfilerthe-client-loop
Controlpathfinding
ControlsScreeninput-and-keybinds
ConversionTypeentity-lifecycle
Cooldownmodels-and-atlases
Coordinatesbrigadier-and-commands
CopperChestBlockblock-entities
CopperGolementity-anatomy, pathfinding, synched-entity-data
CopperGolemStatueBlockblock-entities
CopperGolemStatueBlockEntityblock-entities
CopperGolemStatueSpecialRendererblock-entity-rendering
CopyComponentsFunctiondata-components
CountOnEveryLayerPlacementfeatures-and-placement
CountPlacementfeatures-and-placement
Cowentity-lifecycle
CrafterBlockrecipes
CrafterMenucontainers-and-menus
CrafterSlotcontainers-and-menus
CraftingContainerrecipes
CraftingInputrecipes
CraftingMenucontainers-and-menus, recipes
CraftingReciperecipes
CrashReportanatomy, the-window, starting-a-server
Creakingpathfinding
CreakingHeartDecoratortrees
CreateBuffetWorldScreentext-and-fonts, creating-a-world
CreateFlatWorldScreencreating-a-world
CreateWorldCallbackcreating-a-world
CreateWorldScreencreating-a-world
CreativeModeInventoryScreengui-and-screens, containers-and-menus
CreativeModeTabsbiggest, enchanting
Creeperwhat-the-client-is-told, post-processing
CriteriaTriggersnaming-drift, block-interaction, advancements, containers-and-menus, enchanting, items-and-stacks, loot-tables, recipes, using-an-item, the-spear, game-events-and-vibrations
Criterionadvancements, data-driven-types
CriterionProgressnaming-drift, advancements
CriterionTriggerglossary, advancements, data-driven-types
CriterionTriggerInstancenaming-drift, advancements
CrossbowAttackusing-an-item
CrossbowItemdata-components, enchantments, using-an-item
CrossbowPullenchantments, using-an-item, models-and-atlases
CrossCollisionBlockmovement-and-collision
CrossFrameResourcePoolblaze3d, visibility-and-the-frame-graph
CrudeIncrementalIntIdentityHashBiMapsynched-entity-data, chunk-anatomy
Cryptprotocol-phases
CubeDefinitionentity-rendering
CubeDeformationentity-rendering
CubeListBuilderentity-rendering
CubeVoxelShapemath-and-primitives
CubicSplinedensity-function-nodes, math-and-primitives
CuboidFacenaming-drift, models-and-atlases
CuboidGizmodebugging-the-running-game
CuboidItemModelWrappermodels-and-atlases
CuboidModelmodels-and-atlases
CuboidModelElementnaming-drift, models-and-atlases
CuboidPlacementfeatures-and-placement
CuboidRotationmodels-and-atlases
Cursor3Dmath-and-primitives, movement-and-collision
CursorTypethe-window
Cushionnon-living-damage
CustomAlldialogs
CustomBossEventscoreboard-and-data
CustomBossEventslevel-data-and-rules, scoreboard-and-data
CustomCommandExecutorscoreboard-and-data, the-execution-engine
CustomDatacodecs-nbt-json, data-components, packets-and-stream-codecs
CustomFeatureRenderersubmit-phases
CustomHeadLayerentity-rendering
CustomModifierExecutorthe-execution-engine
CustomPacketPayloadpackets-and-stream-codecs
CustomQueryAnswerPayloadpackets-and-stream-codecs
CustomQueryPayloadpackets-and-stream-codecs
CustomReciperecipes
CustomSpawnerentity-lifecycle
CycleButtongui-and-screens
Damagemodels-and-atlases
DamageCommanddamage-and-death
Damagedmodels-and-atlases
DamageEffectsdamage-and-death
DamageEntityenchantments
DamageScalingdamage-and-death
DamageSourceVI · Entities, damage-and-death, text-components, enchantments, hunger-and-experience
DamageSourcePredicateadvancements
DamageSourcesdamage-and-death
DamageTypedamage-and-death, text-components
DamageTypesdamage-and-death, hunger-and-experience, the-spear
DamageTypeTagsnon-living-damage, damage-and-death, data-components, tags
DarknessFogEnvironmentlightmap-fog-and-sky
DarkOakFoliagePlacertrees
DarkOakTrunkPlacertrees
DataAccessorscoreboard-and-data
DataCommandsbrigadier-and-commands, scoreboard-and-data
DataComponentExactPredicatedata-components
DataComponentGetterentity-anatomy, data-components, items-and-stacks
DataComponentHolderdata-components
DataComponentInitializersdata-components, items-and-stacks
DataComponentLookupdata-components
DataComponentMapcodecs-nbt-json, data-components, identifiers-and-registries, enchantments, items-and-stacks
DataComponentMatchersadvancements, data-components
DataComponentPatchcodecs-nbt-json, data-components, containers-and-menus, items-and-stacks, packets-and-stream-codecs
DataComponentPredicatedata-components, data-driven-types
DataComponentPredicatesdata-components
DataComponentsfanin, Reference, naming-drift, V · Blocks, block-breaking, block-entities, attributes, damage-and-death, entity-anatomy, synched-entity-data, II · Foundations, data-components, text-components, VII · Items and inventories, enchanting, enchantments, items-and-stacks, loot-tables, using-an-item, hunger-and-experience, the-spear, the-sword-swing, models-and-atlases
DataComponentTypeReference, glossary, naming-drift, II · Foundations, codecs-nbt-json, data-components, items-and-stacks
DataFixercodecs-nbt-json
DataFixersbiggest, anatomy, what-this-book-skips, codecs-nbt-json, starting-a-server
DataFixTypeslevel-data-and-rules, what-this-book-skips, advancements, codecs-nbt-json, players-and-sessions, points-of-interest
DataLayerchunk-storage, lighting
DataLayerStorageMaplighting
DataPackConfigresource-system
DatapackStructureReportdata-driven-types
DataResultidentifiers-and-registries, chunk-anatomy
DataSlotcontainers-and-menus, enchanting
DataSourcetext-components
DataVersionlevel-data-and-rules
DaylightDetectorBlocksignal-and-dust
DaylightDetectorBlockEntitydiodes-and-observers
DeathMessageTypedamage-and-death, text-components
DeathProtectiondamage-and-death, data-driven-types
DeathScreengui-and-screens, damage-and-death, text-components
DebugBeeInfodebugging-the-running-game
DebugBrainDumpdebugging-the-running-game
DebugBreezeInfodebugging-the-running-game
DebugCommandfunctions-and-macros, the-execution-engine
DebugConfigCommandbrigadier-and-commands, dialogs, protocol-phases, players-and-sessions
DebugEntityBlockIntersectiondebugging-the-running-game
DebugEntryLightlighting
DebugEntryParticleRenderStatsparticles
DebugEntryPostEffectspost-processing
DebugGameEventInfodebugging-the-running-game
DebugGameEventListenerInfodebugging-the-running-game
DebugGoalInfodebugging-the-running-game
DebugHiveInfodebugging-the-running-game
DebugLevelSourcechunk-generation-pipeline, biomes, terrain
DebugLoggedPrintStreamstarting-a-server
DebugMobSpawningCommandbrigadier-and-commands
DebugOptionsScreenhud-elements
DebugPathCommandbrigadier-and-commands
DebugPathInfodebugging-the-running-game
DebugPoiInfodebugging-the-running-game
DebugQueryHandlerdebugging-the-running-game, input-and-keybinds
DebugRendererdebugging-the-running-game
DebugScreenDisplayerhud
DebugScreenEntrieshud
DebugScreenEntryhud
DebugScreenEntryListwhat-this-book-skips, hud
DebugScreenEntryStatushud
DebugScreenOverlaythreads, debugging-the-running-game
DebugScreenProfilehud
DebugStickItemitems-and-stacks
DebugStructureInfodebugging-the-running-game
DebugSubscriptiondebugging-the-running-game
DebugSubscriptionsdebugging-the-running-game, ai-goals-and-brains, pathfinding, what-the-client-is-told, server-level-tick, server-tick, game-events-and-vibrations, points-of-interest, jigsaw-and-templates
DebugValueAccessdebugging-the-running-game
DebugValueSourcedebugging-the-running-game, entity-anatomy
DecoratedPotBlockEntitypistons-and-block-events
DecoratedPotReciperecipes
DecoratedPotSpecialRendererblock-entity-rendering
DedicatedPlayerListplayers-and-sessions, starting-a-server
DedicatedServerlevel-data-and-rules, threads, anatomy, what-this-book-skips, block-interaction, what-the-client-is-told, how-a-server-dies, players-and-sessions, server-tick, starting-a-server, chunk-storage
DedicatedServerPropertiesthreads, anatomy, block-interaction, how-a-server-dies, server-tick, starting-a-server, chunk-storage, creating-a-world
DedicatedServerSettingsstarting-a-server
DefaultAttributesattributes
DefaultBlockInteractionTriggercontexts-and-predicates
DefaultedMappedRegistryentity-anatomy, identifiers-and-registries, game-events-and-vibrations
DefaultedRegistryidentifiers-and-registries
DefaultPlayerSkinentity-rendering
DefaultRedstoneWireEvaluatorsignal-and-dust
DefaultVertexFormatblaze3d
DelegatingOpscodecs-nbt-json
DeltaTrackernaming-drift, anatomy, the-client-loop, block-entity-rendering, models-and-atlases, the-frame
DemoModeplayer-anatomy
Densitydensity-functions
DensityBufferPooldensity-functions
DensityFunctiondensity-function-nodes, data-driven-types, XII · World generation, density-functions
DensityFunctionCompilerdensity-function-nodes, naming-drift, density-functions
DensityFunctionsdensity-function-nodes, data-driven-types, density-functions
DensitySamplerglossary, density-functions
DensityVolumedensity-function-nodes, density-functions, terrain
DependencySortertags
DepthStencilStateblaze3d
DerivedLevelDatalevel-data-and-rules, starting-a-server
DesertPyramidPiecehand-built-structures
DesertPyramidStructurehand-built-structures
DestructionQueuewhat-this-book-skips
DetectorRailBlocksignal-and-dust
DeviceFeaturesblaze3d
DeviceInfoblaze3d
DeviceLimitsblaze3d
DeviceListsound-engine
DeviceTypeblaze3d
DfRewriteRuledensity-functions
Dialogdialogs, data-driven-types, text-components
DialogActiondialogs
DialogBodydialogs, data-driven-types
DialogBodyHandlersdialogs
DialogBodyTypesdialogs
DialogCommanddialogs
DialogConnectionAccessdialogs
DialogControlSetdialogs
DialogListDialogdialogs
DialogListDialogScreendialogs
Dialogsdialogs
DialogScreendialogs
DialogScreensgui-and-screens, dialogs
DialogTagsdialogs
DialogTypesdialogs
Dictionarybrigadier-and-commands
Difficultylevel-data-and-rules
DifficultyInstancelevel-data-and-rules, what-this-book-skips
DimensionDefaultslevel-data-and-rules
DimensionPaddingjigsaw-and-templates
DimensionTypeglossary, level-data-and-rules, math-and-primitives, naming-drift, entity-lifecycle, lightmap-fog-and-sky, chunk-storage, environment-attributes-and-timelines, lighting
DiodeBlockdiodes-and-observers, scheduled-ticks
Directionfanin, math-and-primitives, submit-phases, V · Blocks, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, movement-and-collision, block-entity-rendering, models-and-atlases, game-events-and-vibrations
DirectionalBlockdiodes-and-observers
DirectoryListermodels-and-atlases
DirectoryLockhow-a-server-dies, starting-a-server
DirectoryTemplateSourcejigsaw-and-templates
DirectoryValidatorresource-system
DirectPoolAliasjigsaw-and-templates
DiscardedPayloadpackets-and-stream-codecs, the-connection
DiscardedQueryPayloadprotocol-phases
DiscardItemloot-tables
DisconnectionDetailsthe-connection
DiscreteVoxelShapemath-and-primitives
DispenserBlockpistons-and-block-events
DispenserMenucontainers-and-menus
Displaynon-living-damage, the-client-level, entity-anatomy, synched-entity-data, what-the-client-is-told, entity-rendering
DisplayContextmodels-and-atlases
DisplayDatathe-window
DisplayInfoadvancements, packets-and-stream-codecs
DisplayRendererentity-rendering
DisplaySlothud-elements, scoreboard-and-data
DistanceManagerblock-entities, entity-lifecycle, what-the-client-is-told, how-a-server-dies, players-and-sessions, server-level-tick, chunk-generation-pipeline, chunk-storage, scheduled-ticks, tickets-and-loading
DistanceToPointFunctiondensity-function-nodes
DistanceTriggercontexts-and-predicates
DoorBlockblock-interaction, blocks-and-states, pistons-and-block-events
DownloadCacheCleanerresource-system
DownloadedPackSourceresource-system
DownloadQueueresource-system
DrawableGizmoPrimitivessubmit-phases
DriedGhastBlockscheduled-ticks
DripParticleparticles
DropperBlockpistons-and-block-events
DryFoliageColorbiomes
DryFoliageColorReloadListenerresource-system
DSLfanin
DyeColorsynched-entity-data
DyeReciperecipes
DynamicAtlasAllocatorthe-gui-render-tree
DynamicGameEventListenerentity-lifecycle, game-events-and-vibrations
DynamicGpuDatanaming-drift, blaze3d
DynamicGpuDataStorageMappednaming-drift, blaze3d
DynamicGraphMinFixedPointlighting, points-of-interest, tickets-and-loading
DynamicLootloot-tables
DynamicOpsglossary, codecs-nbt-json, data-driven-types, containers-and-menus
DynamicTexturelightmap-fog-and-sky
EasingTypeenvironment-attributes-and-timelines
EditBoxgui-and-screens
EditWorldScreencreating-a-world
EffectCommandsattributes
EffectGlyphnaming-drift, text-and-fonts
ElderGuardianParticleparticles
EmoteCommandsentity-selectors
EmptyBlockModelblock-entity-rendering
EmptyLevelChunkchunk-anatomy
EmptyLootItemloot-tables
EmptyModelmodels-and-atlases
EmptyPoolElementjigsaw-and-templates
EmptySlotSourcecontexts-and-predicates
Enchantabledata-components, enchanting
EnchantCommandbrigadier-and-commands, enchanting
EnchantedCountIncreaseFunctionnaming-drift, enchanting, enchantments
EnchantedItemInUseenchantments
EnchantingTableBlockenchanting
Enchantmentnaming-drift, contexts-and-predicates, enchanting, enchantments
EnchantmentActiveCheckcontexts-and-predicates
EnchantmentAttributeEffectattributes, enchantments
EnchantmentEffectComponentsnaming-drift, attributes, damage-and-death, contexts-and-predicates, enchantments, hunger-and-experience
EnchantmentEntityEffectfunctions-and-macros, data-driven-types, enchantments
EnchantmentHelperReference, attributes, damage-and-death, data-components, VII · Items and inventories, enchanting, enchantments, items-and-stacks, using-an-item, the-spear, the-sword-swing
EnchantmentInstancedata-components, enchantments
EnchantmentLevelProvidercontexts-and-predicates
EnchantmentLocationBasedEffectdata-driven-types, enchantments
EnchantmentMenudata-components, enchanting
EnchantmentNamesenchanting
EnchantmentProviderdata-driven-types, enchanting
EnchantmentProviderTypesenchanting
Enchantmentsblock-breaking, enchantments
EnchantmentsByCostenchanting
EnchantmentsByCostWithDifficultyenchanting
EnchantmentScreenenchanting
EnchantmentTagstags, enchanting, enchantments
EnchantmentTargetenchantments
EnchantmentValueEffectdata-driven-types, enchantments
EnchantRandomlyFunctionenchanting
EnchantWithLevelsFunctionenchanting, loot-tables
EncoderCachedata-components
EndCityPieceshand-built-structures
EndCrystalnon-living-damage, damage-and-death
EnderChestBlockEntitypistons-and-block-events
EnderDragonnon-living-damage, damage-and-death, entity-lifecycle
EnderDragonFightlevel-data-and-rules, what-this-book-skips, scoreboard-and-data, server-level-tick, lighting, tickets-and-loading
EnderDragonPartnon-living-damage, entity-selectors, damage-and-death, entity-lifecycle, what-the-client-is-told
EnderEyeItemitems-and-stacks, using-an-item
Endermanpost-processing
EndFlashStatelightmap-fog-and-sky, environment-attributes-and-timelines
EndIslandFunctiondensity-function-nodes
EndTagcodecs-nbt-json
Enemyentity-anatomy
Entitybiggest, fanin, hierarchy, glossary, hud-elements, math-and-primitives, naming-drift, non-living-damage, blocks-and-states, debugging-the-running-game, prediction-and-acks, the-client-level, entity-selectors, scoreboard-and-data, VI · Entities, ai-goals-and-brains, attributes, authority, damage-and-death, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, synched-entity-data, codecs-nbt-json, data-components, tags, text-components, enchantments, what-the-client-is-told, VIII · The player, hunger-and-experience, input-to-movement, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, block-entity-rendering, entity-rendering, players-and-sessions, server-level-tick, chunk-storage, environment-attributes-and-timelines, game-events-and-vibrations, tickets-and-loading
EntityAccessentity-anatomy, chunk-storage
EntityAnchorArgumentbrigadier-and-commands
EntityArgumentbrigadier-and-commands, entity-selectors
EntityAttachmententity-anatomy
EntityAttachmentsentity-anatomy
EntityBlockhierarchy, block-entities, game-events-and-vibrations
EntityBoundSoundInstancesound-engine
EntityCollisionContextmath-and-primitives
EntityDataAccessorscoreboard-and-data, synched-entity-data, player-anatomy
EntityDataSerializersynched-entity-data
EntityDataSerializersblocks-and-states, synched-entity-data, text-components
EntityDataSourcetext-components
EntityDimensionsentity-anatomy
EntityEquipmentnaming-drift, items-and-stacks, player-anatomy, the-two-phase-tick
EntityEquipmentPredicateadvancements
EntityEventsynched-entity-data, using-an-item, points-of-interest
EntityFlagsPredicateadvancements
EntityFluidInteractionnaming-drift, movement-and-collision
EntityGetterentity-lifecycle, movement-and-collision
EntityHitResultmath-and-primitives
EntityInLevelCallbackentity-anatomy, entity-lifecycle
EntityLookupentity-selectors, entity-lifecycle
EntityModelentity-rendering
EntityModelSetentity-rendering, models-and-atlases
EntityPositionSourcegame-events-and-vibrations
EntityPredicateadvancements, data-driven-types, contexts-and-predicates
EntityReferenceentity-anatomy, entity-lifecycle
EntityRenderDispatcherresource-system, block-entity-rendering, entity-rendering, particles
EntityRenderernaming-drift, block-entity-rendering, entity-rendering, lightmap-fog-and-sky
EntityRendererProviderentity-rendering
EntityRenderersentity-rendering
EntityRenderStatesubmit-phases, block-entity-rendering, entity-rendering, particles
EntitySectionentity-lifecycle
EntitySectionStorageentity-selectors, entity-lifecycle
EntitySelectorglossary, brigadier-and-commands, entity-selectors, scoreboard-and-data, text-components
EntitySelectorOptionsbrigadier-and-commands, entity-selectors, contexts-and-predicates
EntitySelectorParserbrigadier-and-commands, entity-selectors, text-components
EntitySpawnReasonnaming-drift, entity-lifecycle
EntitySpawnRequestnaming-drift, entity-anatomy
EntityStorageentity-lifecycle, chunk-storage
EntitySubPredicateadvancements
EntityTickListthe-client-level, entity-lifecycle, server-level-tick, tickets-and-loading
EntityTypefanin, hierarchy, level-data-and-rules, VI · Entities, attributes, entity-anatomy, entity-lifecycle, synched-entity-data, data-components, identifiers-and-registries, text-components, what-the-client-is-told, entity-rendering, chunk-storage
EntityTypeIdsnaming-drift, entity-anatomy
EntityTypesnaming-drift, entity-anatomy, synched-entity-data, identifiers-and-registries
EntityTypeTagsdamage-and-death, entity-anatomy, movement-and-collision, tags, the-sword-swing
EntityTypeTestentity-selectors
EntryActionthe-execution-engine
EntryGrouploot-tables
EnumPropertyblocks-and-states
EnvironmentAttributenaming-drift, the-client-level, what-makes-a-sound, ai-goals-and-brains, environment-attributes-and-timelines, fluids, biomes
EnvironmentAttributeCheckdata-driven-types, environment-attributes-and-timelines
EnvironmentAttributeLayerenvironment-attributes-and-timelines
EnvironmentAttributeMaplevel-data-and-rules, naming-drift, lightmap-fog-and-sky, environment-attributes-and-timelines, biomes
EnvironmentAttributeProbeglossary, naming-drift, lightmap-fog-and-sky, the-frame, environment-attributes-and-timelines
EnvironmentAttributeReaderenvironment-attributes-and-timelines, fluids
EnvironmentAttributeslevel-data-and-rules, naming-drift, what-makes-a-sound, ai-goals-and-brains, entity-lifecycle, lightmap-fog-and-sky, particles, server-level-tick, environment-attributes-and-timelines, fluids, points-of-interest, biomes
EnvironmentAttributeSystemnaming-drift, ai-goals-and-brains, lightmap-fog-and-sky, server-level-tick, environment-attributes-and-timelines
EnvironmentAttributeValueenvironment-attributes-and-timelines
EnvironmentScanPlacementfeatures-and-placement
EqualSpacingLayoutgui-and-screens
EquipmentAssetManagerresource-system, entity-rendering
EquipmentClientInfoentity-rendering
EquipmentLayerRendererentity-rendering
EquipmentSlotenchantments, player-anatomy
EquipmentSlotGroupenchantments
EquipmentUserentity-anatomy, contexts-and-predicates, loot-tables
Equippablehud-elements, damage-and-death, data-components, VII · Items and inventories, enchantments
ErrorCollectorbrigadier-and-commands
EuclideanGameEventListenerRegistrychunk-anatomy, game-events-and-vibrations
Eulastarting-a-server
EventLoopGroupHoldernaming-drift, threads, anatomy, the-connection
EvokerFangsnon-living-damage
ExecuteCommandbrigadier-and-commands, scoreboard-and-data, the-execution-engine, contexts-and-predicates
ExecutionCommandSourcethe-execution-engine
ExecutionContextnaming-drift, functions-and-macros, the-execution-engine
ExecutionControlthe-execution-engine
ExpandableContainerBaseloot-tables
ExperienceBarhud-elements, hud
ExperienceCommandbrigadier-and-commands
ExperienceOrbnon-living-damage, the-client-level, damage-and-death, enchanting, enchantments, hunger-and-experience, the-sword-swing
ExperimentalRedstoneWireEvaluatorsignal-and-dust
ExperimentsScreencreating-a-world
ExplodeEffectdamage-and-death
ExplorationMapFunctionstructure-placement
ExplosionParticleInfoparticles
ExtraCodecscodecs-nbt-json, data-driven-types, text-components
EyeOfEndernon-living-damage
EyesLayerentity-rendering
FaceBakerymodels-and-atlases
FallbackResourceManagerresource-system
FallenTreeFeaturetrees
FallingBlockEntitynon-living-damage, the-client-level, server-level-tick
FallingBlockRenderStateblock-entity-rendering
FallLocationdamage-and-death
FallthroughTaskthe-execution-engine
FancyFoliagePlacertrees
FancyTrunkPlacertrees
Featurenaming-drift, data-driven-types, XII · World generation, features-and-placement, trees
FeatureElementhierarchy, glossary, block-interaction, identifiers-and-registries
FeatureFlagglossary
FeatureFlagssignal-and-dust, identifiers-and-registries, creating-a-world
FeatureFlagSetglossary, identifiers-and-registries, resource-system, items-and-stacks, recipes
FeatureFlagsMetadataSectionidentifiers-and-registries, resource-system
FeatureFlagUniverseglossary, identifiers-and-registries
FeaturePlacernaming-drift, features-and-placement
FeaturePoolElementjigsaw-and-templates
FeatureRenderDispatchernaming-drift, submit-phases, blaze3d, entity-rendering, models-and-atlases, post-processing, the-frame, visibility-and-the-frame-graph
FeatureSizedata-driven-types, trees
FeatureSizeTypedata-driven-types, trees
FeatureSorterfeatures-and-placement
FeatureTagstags
FeatureTypesdata-driven-types
FieldSelectorcodecs-nbt-json
FileFixerUpperwhat-this-book-skips
FilePackResourcesresource-system
FileToIdConverterdata-driven-types, identifiers-and-registries, resource-system, tags
FillBiomeCommandbrigadier-and-commands, biomes
FillCommandXIII · Commands and data packs
FillLayerFeaturecreating-a-world
Filterablechat-and-signing
FilteredSlotSourcecontexts-and-predicates
FilteredTextchat-and-signing
FilterMaskchat-and-signing
FindTopSurfaceFunctiondensity-function-nodes
FiniteAudioStreamsound-engine
FireBlockidentifiers-and-registries
FireworkParticlesparticles
FireworkRocketReciperecipes
FireworkStarFadeReciperecipes
FireworkStarReciperecipes
FirstPersonHandsAndItemsnaming-drift, enchantments, items-and-stacks, using-an-item, block-entity-rendering, entity-rendering
FirstPersonHandsAndItemsRenderernaming-drift, using-an-item, block-entity-rendering, entity-rendering, the-frame
FishingHookthe-client-level, contexts-and-predicates
FishingRodCastmodels-and-atlases
FishingRodHookedTriggeradvancements
FishingRodItemusing-an-item
FixedBiomeSourcebiomes, creating-a-world
FixedFormatscoreboard-and-data
FixedPathPackResourcesnaming-drift, resource-system
FixedPlacementfeatures-and-placement
FlameFeatureRenderersubmit-phases
FlatLayerInfocreating-a-world
FlatLevelGeneratorPresetscreating-a-world
FlatLevelGeneratorSettingsglossary, creating-a-world
FlatLevelSourcechunk-generation-pipeline, biomes, creating-a-world, terrain
FloatProviderdata-driven-types, features-and-placement
FloatProvidersdata-driven-types
FloatSampleSourcesound-engine
FlowingFluidglossary, fluids
Fluidfluids, scheduled-ticks
FluidModelnaming-drift, section-meshing
FluidRenderernaming-drift, section-meshing
Fluidsblocks-and-states, fluids, scheduled-ticks
FluidStateglossary, V · Blocks, blocks-and-states, movement-and-collision, tags, fluids, scheduled-ticks
FluidStateModelSetmodels-and-atlases
FluidTagsblock-breaking, tags, fluids
FlyingMoveControlpathfinding
FlyingPathNavigationpathfinding
FlyNodeEvaluatorpathfinding
FocusNavigationEventgui-and-screens
FogDatanaming-drift, lightmap-fog-and-sky
FogEnvironmentlightmap-fog-and-sky
FogRendererblaze3d, lightmap-fog-and-sky
FogTypelightmap-fog-and-sky
FolderRepositorySourceresource-system
FoliageColorbiomes
FoliageColorReloadListenerresource-system
FoliagePlacerdata-driven-types, trees
FoliagePlacerTypedata-driven-types, trees
Fontnaming-drift, submit-phases, text-and-fonts, block-entity-rendering
FontDescriptionnaming-drift, text-and-fonts, text-components
FontManagertext-and-fonts, resource-system
FontSetnaming-drift, text-and-fonts
FontTexturetext-and-fonts
FoodConstantshunger-and-experience
FoodDatanaming-drift, using-an-item, hunger-and-experience, the-two-phase-tick
FoodPropertiesnaming-drift, using-an-item, hunger-and-experience
Foodshunger-and-experience
ForbiddenSymlinkInforesource-system
ForkingTrunkPlacertrees
FormattedBidiReordertext-and-fonts
FormattedCharSequencetext-and-fonts, text-components
FormattedCharSinktext-and-fonts
FormattedTexttext-and-fonts
Foxbiggest, hierarchy, pathfinding
FpsDebugCharthud
Framethe-execution-engine
FrameGraphBuilderblaze3d, post-processing, section-meshing, visibility-and-the-frame-graph
FrameLayoutgui-and-screens
FramePasspost-processing, visibility-and-the-frame-graph
FramerateLimiterthe-client-loop
FramerateLimitTrackerthe-client-loop, the-frame, the-window
FreeTypeUtiltext-and-fonts
FriendlyByteBufbiggest, codecs-nbt-json, packets-and-stream-codecs, protocol-phases
FriendToasthud
Frogai-goals-and-brains, entity-lifecycle, pathfinding
FrogAiai-goals-and-brains
FrontAndTopmath-and-primitives
FrontendCommandEncoderblaze3d
FrontendGpuDevicenaming-drift, blaze3d
FrontendGpuSurfaceblaze3d
FrontendRenderPassblaze3d
Frustumparticles, visibility-and-the-frame-graph
FullChunkStatusblock-entities, blocks-and-states, entity-lifecycle, what-the-client-is-told, server-level-tick, chunk-anatomy, chunk-generation-pipeline, tickets-and-loading
FunctionArgumentbrigadier-and-commands, functions-and-macros
FunctionCommandbrigadier-and-commands, functions-and-macros, the-execution-engine
FunctionGameTestInstancegame-tests
FunctionInstantiationExceptionfunctions-and-macros
FurnaceMenublock-entities, recipes
FurnaceResultSlotblock-entities
FutureChainchat-and-signing
GameConfigI · Anatomy, anatomy, the-client-loop
GameEventnon-living-damage, block-breaking, block-interaction, blocks-and-states, authority, movement-and-collision, synched-entity-data, using-an-item, game-events-and-vibrations
GameEventDispatchergame-events-and-vibrations
GameEventListenergame-events-and-vibrations
GameEventListenerRegistrygame-events-and-vibrations
GameEventTagsgame-events-and-vibrations
GameMasterBlockblock-breaking
GameModeCommandpermissions
GameModeSwitcherScreenpermissions
GameNarratorgui-and-screens
GameProfileplayers-and-sessions
GameProfileArgumentbrigadier-and-commands, entity-selectors
GameProfileRepositoryplayers-and-sessions
GameProtocolspackets-and-stream-codecs, protocol-phases
GameRenderernaming-drift, submit-phases, hud, sound-engine, the-client-loop, the-gui-render-tree, resource-system, entity-rendering, lightmap-fog-and-sky, post-processing, the-frame
GameRenderStatethe-gui-render-tree, the-frame
GameRulehierarchy, level-data-and-rules, naming-drift, identifiers-and-registries, hunger-and-experience
GameRuleCategorylevel-data-and-rules, naming-drift
GameRuleCommandlevel-data-and-rules
GameRuleMaplevel-data-and-rules, naming-drift, data-driven-types
GameRuleRegistryFixnaming-drift, starting-a-server
GameRulesglossary, level-data-and-rules, naming-drift, non-living-damage, V · Blocks, block-breaking, advancements, brigadier-and-commands, the-execution-engine, damage-and-death, movement-and-collision, identifiers-and-registries, text-components, recipes, what-the-client-is-told, hunger-and-experience, input-to-movement, players-and-sessions, server-level-tick, server-tick, starting-a-server, environment-attributes-and-timelines, fluids, tickets-and-loading, creating-a-world
GameRuleTypelevel-data-and-rules, naming-drift
GameRuleTypeVisitornaming-drift
GameTestAssertExceptiongame-tests
GameTestBatchgame-tests
GameTestBatchFactorygame-tests
GameTestBlockHighlightRenderergame-tests
GameTestEnvironmentsgame-tests
GameTestExceptiongame-tests
GameTestHelpergame-tests
GameTestInfogame-tests, scheduled-ticks
GameTestInstancenaming-drift, game-tests, data-driven-types
GameTestInstancesgame-tests
GameTestListenergame-tests
GameTestMainUtilgame-tests
GameTestRunnergame-tests
GameTestSequencegame-tests
GameTestServeranatomy, debugging-the-running-game, game-tests, how-a-server-dies, chunk-storage
GameTestTickergame-tests, server-tick
GameTestTimeoutExceptiongame-tests
GameTypelevel-data-and-rules, player-anatomy
GateBehaviorai-goals-and-brains
GaussianSamplerenvironment-attributes-and-timelines
GeneratingChunkMapchunk-generation-pipeline
GenerationChunkHolderhow-a-server-dies, chunk-generation-pipeline, chunk-storage, tickets-and-loading
GenerationStepfeatures-and-placement
GenericMessageScreenhow-a-server-dies
GenericThreadthreads, how-a-server-dies
Ghastentity-anatomy, entity-lifecycle, pathfinding
GiantTrunkPlacertrees
GiveCommandnaming-drift, brigadier-and-commands, codecs-nbt-json
GizmoCollectordebugging-the-running-game
GizmoFeatureRenderersubmit-phases
GizmoPropertiesdebugging-the-running-game
Gizmosdebugging-the-running-game
GizmoStyledebugging-the-running-game
GlBackendanatomy, the-window
GlBufferblaze3d
GlCommandEncoderblaze3d
GlDeviceblaze3d
GlHeuristicsblaze3d
GlobalPalettechunk-anatomy
GlobalPoslevel-data-and-rules, math-and-primitives, points-of-interest
GlobalSettingsUniformblaze3d, post-processing
GlobalTestReportergame-tests
GlPipelineRecompilerblaze3d
GlRenderPassblaze3d
GlslCompilernaming-drift, what-this-book-skips, blaze3d
GlStateManagerblaze3d
GlSurfacethe-frame
GlTransientMemoryblaze3d
GlyphBitmapnaming-drift, text-and-fonts
GlyphProvidertext-and-fonts
GlyphProviderDefinitionnaming-drift, text-and-fonts
GlyphProviderTypenaming-drift, text-and-fonts
GlyphRenderStatetext-and-fonts, the-gui-render-tree
GlyphRenderTypestext-and-fonts
GlyphSourcenaming-drift, text-and-fonts
GlyphStitchertext-and-fonts
Goalhierarchy, glossary, ai-goals-and-brains
GoalSelectorai-goals-and-brains, entity-anatomy
Goatai-goals-and-brains
GolemSensorai-goals-and-brains
GoToPotentialJobSiteai-goals-and-brains, points-of-interest
GpuBackendanatomy, blaze3d, the-window
GpuBuffernaming-drift, blaze3d, post-processing
GpuBufferSlicenaming-drift, blaze3d
GpuDevicepackages, glossary, anatomy, XI · Rendering, blaze3d, post-processing, the-window
GpuDeviceBackendglossary, blaze3d
GpuFenceblaze3d
GpuFormatnaming-drift, blaze3d
GpuOutOfMemoryExceptionblaze3d
GpuSamplerblaze3d
GpuSurfacenaming-drift, blaze3d, the-frame, the-window
GpuSurfaceBackendblaze3d
GpuTextureblaze3d, lightmap-fog-and-sky
GpuTextureViewblaze3d
GpuWarnlistManagerresource-system
GradientFunctiondensity-function-nodes
GradientNoisedensity-functions
GraphicsPresetpost-processing
GraphicsResourceAllocatorblaze3d
GrassBlockwhat-this-book-skips, biomes
GrassColorbiomes
GrassColorReloadListenerresource-system
GravityProcessorjigsaw-and-templates
GridLayoutgui-and-screens
GrindstoneMenuenchanting
GroundPathNavigationpathfinding
GroupSlotSourcedata-driven-types, contexts-and-predicates
GsonHelpercodecs-nbt-json
Guibiggest, glossary, hud-elements, naming-drift, gui-and-screens, hud, input-and-keybinds, the-client-loop, the-gui-render-tree, resource-system, how-a-server-dies
GuiBannerResultRenderStatethe-gui-render-tree
GuiBookModelRenderStatethe-gui-render-tree
GuiElementRenderStatethe-gui-render-tree
GuiEntityRenderStatethe-gui-render-tree
GuiEventListenergui-and-screens
GuiGraphicsExtractorhud-elements, naming-drift, hud, text-and-fonts, the-gui-render-tree, items-and-stacks
GuiItemAtlasthe-gui-render-tree
GuiItemRenderStatethe-gui-render-tree
GuiMessagehud
GuiMessageSourcehud
GuiMessageTaghud
GuiProfilerChartRenderStatethe-gui-render-tree
GuiRenderertext-and-fonts, the-gui-render-tree, blaze3d, post-processing, the-frame
GuiRenderStatehud-elements, naming-drift, hud, the-gui-render-tree
GuiSkinRenderStatethe-gui-render-tree
GuiTextRenderStatetext-and-fonts, the-gui-render-tree
Halfblocks-and-states
HandlerNamesthe-connection
HandshakeProtocolspackets-and-stream-codecs, protocol-phases, the-connection
HangingSignBlockEntityblock-entities
HappyGhastentity-lifecycle, pathfinding
HarvestFarmlandai-goals-and-brains
HasCustomInventoryScreengui-and-screens
HashedPatchMapcodecs-nbt-json, containers-and-menus
HashedStackcodecs-nbt-json, data-components, containers-and-menus, packets-and-stream-codecs
HashMapPalettechunk-anatomy
HashOpscodecs-nbt-json, containers-and-menus
HeaderAndFooterLayoutgui-and-screens
Heightmapchunk-anatomy, chunk-generation-pipeline, creating-a-world, terrain
HeightmapPlacementfeatures-and-placement
HeightProviderdata-driven-types, features-and-placement
HeightProviderTypedata-driven-types
HeightRangePlacementfeatures-and-placement
HiddenByteBufthe-connection
HintsAndWorkaroundsblaze3d
HitResultmath-and-primitives
Hoglinai-goals-and-brains, damage-and-death
Holderfanin, glossary, naming-drift, brigadier-and-commands, synched-entity-data, II · Foundations, data-components, data-driven-types, identifiers-and-registries, tags, text-components, contexts-and-predicates, items-and-stacks, creating-a-world
HolderGetteridentifiers-and-registries, contexts-and-predicates
HolderLookupcodecs-nbt-json, identifiers-and-registries, items-and-stacks
HolderOwnernaming-drift, identifiers-and-registries
HolderSetglossary, naming-drift, data-components, identifiers-and-registries, tags, enchantments, recipes
HolderSetCodeccodecs-nbt-json, identifiers-and-registries, tags
HoneycombItemitems-and-stacks
HopperBlockEntityV · Blocks, block-entities
HopperMenucontainers-and-menus
HorizontalDirectionalBlockdiodes-and-observers
Horsedamage-and-death
HotbarVIII · The player
HotbarManagerVIII · The player
HoverEventtext-components, items-and-stacks
Hudbiggest, glossary, hud-elements, naming-drift, gui-and-screens, hud, scoreboard-and-data, resource-system, items-and-stacks, lightmap-fog-and-sky
HumanoidArmorLayerentity-rendering
HumanoidMobRendererentity-rendering
HumanoidModelentity-rendering
HumanoidRenderStateentity-rendering
HungerMobEffectstatus-effects
HurtByTargetGoalai-goals-and-brains
IdDispatchCodecnaming-drift, packets-and-stream-codecs
Identifierfanin, glossary, level-data-and-rules, math-and-primitives, naming-drift, hud, sound-engine, what-makes-a-sound, permissions, attributes, II · Foundations, identifiers-and-registries, tags, text-components, contexts-and-predicates, enchantments, recipes, using-an-item, packets-and-stream-codecs, blaze3d, models-and-atlases, post-processing
IdentifierArgumentnaming-drift
IdentifierExceptionidentifiers-and-registries
IdMapblocks-and-states, entity-anatomy, identifiers-and-registries, chunk-anatomy
IdMapperblocks-and-states, identifiers-and-registries
IglooPieceshand-built-structures
Igniteenchantments
ImbueReciperecipes
IMEPreeditOverlaythe-gui-render-tree
ImpossibleTriggeradvancements, contexts-and-predicates
ImposterProtoChunkchunk-anatomy, chunk-generation-pipeline, chunk-storage, scheduled-ticks
InactiveProfilerthe-client-loop
InclusiveRangemath-and-primitives
IndexMergermath-and-primitives
IndexTypenaming-drift, blaze3d
Ingredientnaming-drift, recipes
Inputinput-and-keybinds, dialogs, input-to-movement, player-anatomy
InputConstantsinput-and-keybinds, the-window
InputControldialogs, data-driven-types
InputControlHandlersdialogs
InputControlTypesdialogs
InputQuirksinput-and-keybinds
InputTypeinput-and-keybinds
InputWithModifiersinput-and-keybinds
InsideBlockEffectAppliermovement-and-collision
InsideBlockEffectTypemovement-and-collision
InSquarePlacementnaming-drift, features-and-placement
InstantaneousMobEffectstatus-effects
InstantiatedFunctionfunctions-and-macros
InstantNeighborUpdaterblocks-and-states
InstrumentItemusing-an-item
IntArrayTagcodecs-nbt-json
IntegerPropertyblocks-and-states
IntegratedServerintroduction, glossary, naming-drift, anatomy, debugging-the-running-game, options, permissions, identifiers-and-registries, what-the-client-is-told, how-a-server-dies, server-tick, starting-a-server, chunk-storage, creating-a-world
Interactionnon-living-damage, the-spear, the-sword-swing
InteractionHandblock-interaction
InteractionResultnaming-drift, block-interaction, synched-entity-data, using-an-item
InteractWithDoorblock-interaction
InterpolatedFunctiondensity-function-nodes, naming-drift, terrain
InterpolationHandlernaming-drift, the-client-level, authority, movement-and-collision
Intervaldensity-function-nodes
IntervalSelectFunctiondensity-function-nodes, naming-drift
IntProviderdata-driven-types, features-and-placement, trees
IntProvidersdata-driven-types
Inventorynaming-drift, block-breaking, brigadier-and-commands, scoreboard-and-data, entity-anatomy, containers-and-menus, items-and-stacks, recipes, player-anatomy, the-two-phase-tick
InventoryChangeTriggeradvancements
InventoryMenugui-and-screens, containers-and-menus, recipes
InventoryScreengui-and-screens
InvertableSetOptionStateentity-selectors
InvertedLootItemConditioncontexts-and-predicates
InWorldGameRulesScreenlevel-data-and-rules
IoSupplierresource-system
IOWorkerthreads, anatomy, codecs-nbt-json, how-a-server-dies, chunk-storage, blending
IpBanListplayers-and-sessions
IsCarriedmodels-and-atlases
IsKeybindDownmodels-and-atlases
IsolatedCallthe-execution-engine
IsSelectedmodels-and-atlases
IsUsingItemmodels-and-atlases
IsXmasblock-entity-rendering
Itemhierarchy, Reference, glossary, naming-drift, block-breaking, attributes, II · Foundations, codecs-nbt-json, data-components, identifiers-and-registries, VII · Items and inventories, items-and-stacks, recipes, using-an-item, the-spear, the-sword-swing
ItemArgumentbrigadier-and-commands, codecs-nbt-json
ItemAttributeModifiersattributes
ItemBlockStatemodels-and-atlases
ItemBodydialogs
ItemClusterRenderStateblock-entity-rendering
ItemCombinerMenucontainers-and-menus
ItemCombinerMenuSlotDefinitioncontainers-and-menus
ItemCombinerScreencontainers-and-menus
ItemCommandsbrigadier-and-commands, contexts-and-predicates
ItemContainerContentsdata-components, items-and-stacks
ItemCooldownsblock-interaction, items-and-stacks, using-an-item
ItemEnchantmentsdata-components, enchanting, enchantments
ItemEntityhierarchy, non-living-damage, block-breaking, the-client-level, damage-and-death, entity-anatomy, movement-and-collision, VII · Items and inventories, items-and-stacks, the-sword-swing, game-events-and-vibrations
ItemEntityRendererblock-entity-rendering
ItemFeatureRenderernaming-drift, submit-phases
ItemFramenon-living-damage, diodes-and-observers, synched-entity-data, what-the-client-is-told
ItemIdswhat-this-book-skips, identifiers-and-registries
ItemInHandLayerentity-rendering
ItemInputnaming-drift, brigadier-and-commands, codecs-nbt-json, items-and-stacks
ItemInstancedata-components, contexts-and-predicates, items-and-stacks
ItemLikehierarchy
ItemLoredata-components
ItemModelnaming-drift, block-entity-rendering, models-and-atlases
ItemModelGeneratormodels-and-atlases
ItemModelResolvernaming-drift, block-entity-rendering, entity-rendering, models-and-atlases
ItemModelsmodels-and-atlases
ItemOwnerentity-anatomy
ItemParserbrigadier-and-commands, codecs-nbt-json, data-components
ItemParticleOptionitems-and-stacks
ItemPickupParticleparticles
ItemPredicateadvancements, data-components, contexts-and-predicates
ItemPredicateArgumentbrigadier-and-commands
Itemsbiggest, hierarchy, what-this-book-skips, synched-entity-data, data-components, identifiers-and-registries, enchanting, enchantments, items-and-stacks, the-spear
ItemStackfanin, glossary, naming-drift, non-living-damage, block-breaking, block-interaction, prediction-and-acks, attributes, damage-and-death, synched-entity-data, II · Foundations, codecs-nbt-json, data-components, data-driven-types, tags, VII · Items and inventories, containers-and-menus, enchanting, enchantments, items-and-stacks, recipes, using-an-item, packets-and-stream-codecs, hunger-and-experience, the-spear, the-sword-swing, models-and-atlases
ItemStackRenderStateblock-entity-rendering, entity-rendering, models-and-atlases
ItemStackTemplatedata-components, text-components, items-and-stacks, recipes
ItemStackWithSlotcodecs-nbt-json
ItemSteerableentity-anatomy
ItemTagstags, the-sword-swing, game-events-and-vibrations
ItemTintSourcenaming-drift, models-and-atlases
ItemUseAnimationnaming-drift, using-an-item
ItemUsedOnLocationTriggercontexts-and-predicates
JigsawBlockhierarchy, jigsaw-and-templates
JigsawBlockEntityjigsaw-and-templates
JigsawJunctionjigsaw-and-templates
JigsawPlacementXII · World generation, density-functions, jigsaw-and-templates
JigsawReplacementProcessorjigsaw-and-templates
JigsawStructurejigsaw-and-templates, structure-placement
JoinWorldTaskidentifiers-and-registries, protocol-phases, players-and-sessions
JOrbisAudioStreamsound-engine
JsonEventLogresource-system
JsonOpscodecs-nbt-json, data-driven-types, packets-and-stream-codecs
JsonRpcthreads, what-this-book-skips, starting-a-server
JsonRpcNotificationServicestarting-a-server
JukeboxBlockblock-interaction
JumpableVehicleBarhud-elements, hud
JumpControlpathfinding
JungleTemplePiecehand-built-structures
JUnitLikeTestReportergame-tests
JvmProfilerserver-tick
KeybindContentstext-components
KeybindResolvertext-components
KeyBindsListinput-and-keybinds
KeyBindsScreeninput-and-keybinds
KeyboardHandlerglossary, naming-drift, gui-and-screens, input-and-keybinds, the-client-loop, permissions, resource-system, text-components, input-to-movement, the-window
KeyboardInputinput-and-keybinds, input-to-movement, player-anatomy, the-two-phase-tick
KeyDispatchCodecdata-driven-types
KeyEventnaming-drift, gui-and-screens, input-and-keybinds
Keyframeentity-rendering
KeyframeAnimationentity-rendering
KeyframeTrackenvironment-attributes-and-timelines
KeyframeTrackSamplerenvironment-attributes-and-timelines
KeyMappingpackages, glossary, naming-drift, input-and-keybinds, text-components, input-to-movement, the-window
KilledTriggeradvancements
KineticWeapondata-components, enchantments, the-spear, the-sword-swing
KnownPackidentifiers-and-registries, resource-system
KnownPacksManageridentifiers-and-registries
Languagetext-and-fonts, text-components
LanguageManagerresource-system, text-components
LanServerPingerthreads, anatomy
LastSeenMessageschat-and-signing
LastSeenMessagesTrackerchat-and-signing
LastSeenMessagesValidatorthreads, chat-and-signing, server-tick
LastSeenTrackedEntrychat-and-signing
LavaFluidenvironment-attributes-and-timelines, fluids, scheduled-ticks
LavaFogEnvironmentlightmap-fog-and-sky
LavaSubmergedBlockProcessorhand-built-structures
LayerDefinitionentity-rendering
LayerDefinitionsentity-rendering
LayeredRegistryAccessdata-driven-types, identifiers-and-registries
LayeringTransformblaze3d
LayerLightEventListenerlighting
LayerLightSectionStoragelighting
Layoutgui-and-screens
LayoutElementgui-and-screens
LayoutSettingsgui-and-screens
LeadItemitems-and-stacks
Leashableai-goals-and-brains, entity-anatomy
LeashFeatureRenderersubmit-phases
LeaveVineDecoratortrees
LecternBlockblock-interaction
LecternMenucontainers-and-menus
LecternScreencontainers-and-menus
LegacyFbmInitializerdensity-functions
LegacyQueryHandlerwhat-this-book-skips, the-connection
LegacyRandomSourcemath-and-primitives, density-functions, terrain
LegacyServerPingerthe-connection
LegacySinglePoolElementjigsaw-and-templates
LegacyTextFilterchat-and-signing
LenientJsonParsercodecs-nbt-json
LerpFunctiondensity-function-nodes, environment-attributes-and-timelines
LerpingBossEventhud
Levellectures, fanin, Reference, block-update-flags, glossary, level-data-and-rules, math-and-primitives, naming-drift, V · Blocks, block-breaking, block-entities, block-interaction, blocks-and-states, diodes-and-observers, pistons-and-block-events, signal-and-dust, X · The client, the-client-level, the-client-loop, what-makes-a-sound, entity-selectors, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, identifiers-and-registries, containers-and-menus, contexts-and-predicates, loot-tables, using-an-item, XI · Rendering, entity-rendering, lightmap-fog-and-sky, particles, section-meshing, server-level-tick, chunk-anatomy, chunk-generation-pipeline, environment-attributes-and-timelines, fluids, game-events-and-vibrations, lighting, points-of-interest, scheduled-ticks
LevelAccessorblock-interaction, the-client-level, scheduled-ticks
LevelBasedPermissionSetnaming-drift, entity-selectors, functions-and-macros, permissions, players-and-sessions
LevelBasedValuedata-driven-types, enchantments
LevelCallbackentity-lifecycle
LevelChunklectures, block-entities, blocks-and-states, diodes-and-observers, debugging-the-running-game, how-a-server-dies, IV · The world, chunk-anatomy, chunk-generation-pipeline, chunk-storage, fluids, game-events-and-vibrations, lighting, scheduled-ticks, tickets-and-loading
LevelChunkSectionserver-level-tick, chunk-anatomy, chunk-storage, lighting, points-of-interest, biomes
LevelChunkTicksserver-level-tick, chunk-anatomy, scheduled-ticks
LevelDatalevel-data-and-rules
LevelDebugSynchronizersdebugging-the-running-game, server-level-tick, points-of-interest
LeveledPriorityQueuelighting
LevelEntityGetterAdapterentity-selectors
LevelEventnaming-drift, block-breaking, particles
LevelEventHandlerwhat-makes-a-sound, particles
LevelExtractorblock-update-flags, naming-drift, block-breaking, block-interaction, blocks-and-states, debugging-the-running-game, options, the-client-level, entity-anatomy, resource-system, XI · Rendering, block-entity-rendering, entity-rendering, lightmap-fog-and-sky, models-and-atlases, particles, section-meshing, the-frame, visibility-and-the-frame-graph, lighting
LevelHeightAccessorblock-breaking, block-interaction, chunk-anatomy
LevelLightEngineblocks-and-states, the-client-level, chunk-anatomy, chunk-storage, lighting
LevelLoadingScreenhud-elements, gui-and-screens, hud
LevelLoadListenerplayers-and-sessions, starting-a-server
LevelLoadProgressTrackerstarting-a-server
LevelLoadTrackerstarting-a-server
LevelReaderthe-client-level, identifiers-and-registries, lightmap-fog-and-sky, lighting, scheduled-ticks, biomes
LevelRendererglossary, math-and-primitives, naming-drift, submit-phases, block-interaction, debugging-the-running-game, hud, the-client-level, XI · Rendering, blaze3d, block-entity-rendering, entity-rendering, lightmap-fog-and-sky, models-and-atlases, post-processing, section-meshing, the-frame, visibility-and-the-frame-graph
LevelResourcelevel-data-and-rules, advancements
LevelSettingslevel-data-and-rules, creating-a-world
LevelStemglossary, level-data-and-rules, starting-a-server, creating-a-world
LevelStorageSourcelevel-data-and-rules, resource-system, how-a-server-dies, starting-a-server, chunk-storage, creating-a-world
LevelSummarylevel-data-and-rules, what-this-book-skips, starting-a-server, creating-a-world
LevelTargetBundlepost-processing, visibility-and-the-frame-graph
LevelTickAccessdiodes-and-observers, scheduled-ticks
LevelTicksthe-client-level, server-level-tick, fluids, scheduled-ticks
LevelVersionlevel-data-and-rules
LevelWritersignal-and-dust, entity-lifecycle, server-level-tick, fluids, hand-built-structures
LeverBlocksignal-and-dust
Librarysound-engine
Lifecyclelevel-data-and-rules, data-driven-types, identifiers-and-registries
LightChunkchunk-anatomy, lighting
LightChunkGetterlighting
LightCoordsUtilnaming-drift, lightmap-fog-and-sky
LightEnginethe-client-level, chunk-anatomy, lighting
LightEventListenerlighting
Lightinglightmap-fog-and-sky, the-window
LightLayerlighting
Lightmapintroduction, naming-drift, blaze3d, lightmap-fog-and-sky, the-frame, lighting
LightmapRenderStatelightmap-fog-and-sky
LightmapRenderStateExtractorlightmap-fog-and-sky
LightningBolthierarchy, non-living-damage, what-makes-a-sound, entity-anatomy, environment-attributes-and-timelines
LightningRodBlockscheduled-ticks
LimitSlotSourcecontexts-and-predicates
LinearCongruentialGeneratormath-and-primitives
LinearInterpolationHandlerauthority
LinearLayoutgui-and-screens
LinearPalettechunk-anatomy
LineGizmodebugging-the-running-game
LinkFileSystemresource-system
LinkFSPathresource-system
LinkFSProviderresource-system
LiquidBlockglossary, V · Blocks, fluids, scheduled-ticks
LiquidBlockContainerfluids
LiquidSettingsjigsaw-and-templates
Listenersound-engine
ListenerTransformsound-engine
ListPoolElementjigsaw-and-templates
ListTagglossary, codecs-nbt-json
LivingEntitybiggest, fanin, hierarchy, Reference, glossary, hud-elements, naming-drift, non-living-damage, hud, the-client-level, entity-selectors, scoreboard-and-data, VI · Entities, ai-goals-and-brains, attributes, authority, damage-and-death, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, synched-entity-data, text-components, contexts-and-predicates, enchanting, enchantments, items-and-stacks, loot-tables, using-an-item, what-the-client-is-told, VIII · The player, hunger-and-experience, input-to-movement, player-anatomy, status-effects, the-spear, the-sword-swing, the-two-phase-tick, entity-rendering, game-events-and-vibrations, points-of-interest
LivingEntityRendererscoreboard-and-data, entity-rendering
LivingEntityRenderStateentity-rendering
Llamapathfinding
LoadingChunkTrackertickets-and-loading
LoadingOverlayhud-elements, anatomy, gui-and-screens, hud, resource-system
LocalChatSessionchat-and-signing
LocalCoordinatesbrigadier-and-commands
LocalFrameDecoderthe-connection
LocalFrameEncoderthe-connection
LocalMobCapCalculatorentity-lifecycle, server-level-tick
LocalPlayerbiggest, glossary, naming-drift, threads, anatomy, block-interaction, gui-and-screens, hud, input-and-keybinds, prediction-and-acks, the-client-level, the-client-loop, what-makes-a-sound, permissions, authority, damage-and-death, movement-and-collision, synched-entity-data, text-components, using-an-item, hunger-and-experience, input-to-movement, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, post-processing
LocalPlayerResolverplayer-anatomy
LocalSampleLoggerdebugging-the-running-game
LocalTimetext-and-fonts, block-entity-rendering, models-and-atlases
LocateCommandbrigadier-and-commands, points-of-interest
LocationCheckcontexts-and-predicates
LocationPredicateadvancements, contexts-and-predicates
LocatorBarhud-elements, hud
LodestoneTrackerpoints-of-interest
LoggedChatEventhud
LoggedChatMessagechat-and-signing
LoggedPrintStreamstarting-a-server
LoggingLevelLoadListenerplayers-and-sessions, starting-a-server
LoginProtocolspackets-and-stream-codecs, protocol-phases
LogTestReportergame-tests
LongArrayTagcodecs-nbt-json
LookAtPlayerGoalai-goals-and-brains
LookControlpathfinding
LoomMenucontainers-and-menus
LoopingAudioStreamsound-engine
LootCommandbrigadier-and-commands, contexts-and-predicates
LootContextglossary, naming-drift, advancements, contexts-and-predicates, enchantments, loot-tables
LootContextArgcontexts-and-predicates
LootContextParamsnaming-drift, block-breaking, damage-and-death, contexts-and-predicates, enchanting, loot-tables, environment-attributes-and-timelines
LootContextParamSetsblock-breaking, entity-selectors, contexts-and-predicates
LootContextSourcescontexts-and-predicates
LootContextUsercontexts-and-predicates
LootDataTypedata-driven-types, contexts-and-predicates
LootItemloot-tables
LootItemConditionglossary, naming-drift, advancements, data-driven-types, contexts-and-predicates, enchantments
LootItemConditionalFunctiondata-driven-types, loot-tables
LootItemConditionTypesnaming-drift, contexts-and-predicates
LootItemEntityPropertyConditioncontexts-and-predicates
LootItemFunctiondata-driven-types, contexts-and-predicates, enchanting, loot-tables
LootItemFunctionsdata-driven-types
LootItemRandomChanceConditioncontexts-and-predicates
LootItemRandomChanceWithEnchantedBonusConditionenchantments
LootParamscontexts-and-predicates, loot-tables
LootPoolloot-tables
LootPoolEntriesloot-tables
LootPoolEntryloot-tables
LootPoolEntryContainerdata-driven-types, loot-tables
LootTabledata-driven-types, contexts-and-predicates, loot-tables, what-the-client-is-told
LpVec3packets-and-stream-codecs
MaceItemdata-components
MacosUtilinput-and-keybinds
MacroFunctionnaming-drift, functions-and-macros
Mainthe-client-loop, identifiers-and-registries, how-a-server-dies, starting-a-server, creating-a-world
MainHandmodels-and-atlases
MainTargetblaze3d
ManagementServerthreads, what-this-book-skips, how-a-server-dies, server-tick, starting-a-server
MangroveRootPlacementtrees
MangroveRootPlacertrees
Mannequinglossary, naming-drift, attributes, authority, entity-anatomy, VIII · The player, player-anatomy
MapCodecfanin, naming-drift, dialogs, data-driven-types, recipes, structure-placement
MapExtendingReciperecipes
MapIdlevel-data-and-rules
MapIndexlevel-data-and-rules
MapItemitems-and-stacks
MapItemSavedDatalevel-data-and-rules
MappableRingBufferblaze3d, lightmap-fog-and-sky, post-processing
MappedRegistrydata-components, data-driven-types, identifiers-and-registries, tags
Markernon-living-damage, what-the-client-is-told
MarsagliaPolarGaussianmath-and-primitives
MatchBlockcontexts-and-predicates
Materialnaming-drift, models-and-atlases
MaterialBakermodels-and-atlases
MaterialConditionnaming-drift, data-driven-types, terrain
MaterialRulenaming-drift, data-driven-types, terrain
MaterialRuleContextterrain
MaterialSystemnaming-drift, density-functions, terrain
MegaJungleFoliagePlacertrees
MegaJungleTrunkPlacertrees
MegaPineFoliagePlacertrees
MemoryMapdata-driven-types
MemoryModuleTypeai-goals-and-brains, game-events-and-vibrations, points-of-interest
MemoryReservestarting-a-server
MemoryServerHandshakePacketListenerImplanatomy, protocol-phases
MemorySlotai-goals-and-brains
MenuProvidercontainers-and-menus
MenuScreensgui-and-screens, containers-and-menus
MenuTypehierarchy, gui-and-screens, identifiers-and-registries, containers-and-menus
MerchantMenucontainers-and-menus
MerchantOfferscontexts-and-predicates
MeshDatanaming-drift, blaze3d
MeshDefinitionentity-rendering
MessageArgumentglossary, brigadier-and-commands, entity-selectors, text-components, chat-and-signing
MessageBoxthe-window
MessageSignaturechat-and-signing
MessageSignatureCachechat-and-signing
MetricCategorywhat-this-book-skips
MetricsRecorderthe-client-loop, server-tick
MinecartCollisionContextmath-and-primitives
MinecartTNTnon-living-damage, damage-and-death
Minecraftbiggest, fanin, packages, glossary, hud-elements, naming-drift, threads, anatomy, what-this-book-skips, block-breaking, block-entities, block-interaction, debugging-the-running-game, gui-and-screens, hud, input-and-keybinds, options, prediction-and-acks, sound-engine, the-client-level, the-client-loop, what-makes-a-sound, resource-system, text-components, using-an-item, chat-and-signing, protocol-phases, the-connection, input-to-movement, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, XI · Rendering, blaze3d, entity-rendering, models-and-atlases, particles, post-processing, the-frame, the-window, visibility-and-the-frame-graph, how-a-server-dies, server-tick, starting-a-server, lighting, creating-a-world
MinecraftServerintroduction, biggest, packages, glossary, level-data-and-rules, naming-drift, threads, anatomy, what-this-book-skips, block-breaking, block-interaction, pistons-and-block-events, debugging-the-running-game, advancements, brigadier-and-commands, dialogs, entity-selectors, functions-and-macros, game-tests, permissions, scoreboard-and-data, synched-entity-data, data-driven-types, identifiers-and-registries, resource-system, tags, text-components, contexts-and-predicates, recipes, chat-and-signing, protocol-phases, the-connection, what-the-client-is-told, input-to-movement, player-anatomy, the-sword-swing, the-two-phase-tick, III · The server, how-a-server-dies, players-and-sessions, server-level-tick, server-tick, starting-a-server, chunk-generation-pipeline, chunk-storage, environment-attributes-and-timelines, lighting, points-of-interest, tickets-and-loading, biomes, creating-a-world
MinecraftServerGuithreads, how-a-server-dies
MinecraftServerStateServiceImplhow-a-server-dies
MineshaftPieceshand-built-structures
MineshaftStructurehand-built-structures, structure-placement
MinMaxBoundsadvancements, entity-selectors
MipmapGeneratormodels-and-atlases, the-window
MipmapStrategymodels-and-atlases
Mirrormath-and-primitives
MiscOverworldFeatureswhat-this-book-skips
MissingTextureAtlasSpritemodels-and-atlases
Mobbiggest, hierarchy, glossary, level-data-and-rules, non-living-damage, debugging-the-running-game, scoreboard-and-data, VI · Entities, ai-goals-and-brains, attributes, authority, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, synched-entity-data, contexts-and-predicates, enchanting, enchantments, what-the-client-is-told, the-sword-swing, environment-attributes-and-timelines
MobCategoryattributes, entity-anatomy, entity-lifecycle, server-level-tick
MobEffecthierarchy, glossary, naming-drift, attributes, identifiers-and-registries, status-effects
MobEffectCategorystatus-effects
MobEffectFogEnvironmentlightmap-fog-and-sky
MobEffectInstancehud-elements, status-effects
MobEffectsnaming-drift, attributes, damage-and-death, movement-and-collision, input-to-movement, status-effects, the-sword-swing
MobEffectsPredicateadvancements
MobEffectUtilblock-breaking, status-effects
MobRendererentity-rendering
MobSpawnSettingsentity-lifecycle, biomes
Modelsubmit-phases, entity-rendering
ModelBakerymodels-and-atlases
ModelBlockRenderernaming-drift, submit-phases, section-meshing
ModelDiscoverymodels-and-atlases
ModelFeatureRenderersubmit-phases, block-entity-rendering, entity-rendering
ModelGroupCollectormodels-and-atlases
ModelLayerLocationblock-entity-rendering, entity-rendering
ModelLayersentity-rendering
ModelManagerblocks-and-states, resource-system, block-entity-rendering, models-and-atlases, section-meshing
ModelPartsubmit-phases, entity-rendering
ModelProviderbiggest
ModelStatemodels-and-atlases
Monitornaming-drift, the-window
MonitoredLocalFrameDecoderthe-connection
MonitorManagernaming-drift, the-window
Monsterattributes, damage-and-death, entity-anatomy, entity-lifecycle
MonsterRoomFeatureloot-tables
MoonBrightnessCheckentity-lifecycle
MoonPhaselightmap-fog-and-sky, environment-attributes-and-timelines
MouseButtonEventnaming-drift, gui-and-screens, input-and-keybinds
MouseHandlerpackages, glossary, naming-drift, input-and-keybinds, the-client-loop, input-to-movement, the-window
MoveControlpathfinding
MoverTypeauthority, movement-and-collision, input-to-movement
MoveSimulationTypeauthority
MoveThroughVillageGoalai-goals-and-brains
MoveToTargetSinkai-goals-and-brains, pathfinding
MovingBlockFeatureRenderersubmit-phases
MovingBlockRenderStateblock-entity-rendering
MovingPistonBlockpistons-and-block-events
MsgCommandentity-selectors
Mthfanin, packages, math-and-primitives, damage-and-death, entity-anatomy, movement-and-collision, packets-and-stream-codecs, chunk-anatomy
MultiActionDialogdialogs
MultiButtonDialogScreendialogs
MultiLineLabelgui-and-screens
MultiNoiseBiomeSourcebiomes, creating-a-world
MultiNoiseBiomeSourceParameterListnaming-drift, biomes
MultiNoiseBiomeSourceParameterListsbiomes
MultiPackResourceManagerresource-system
MultiPartModelmodels-and-atlases
MultiPlayerGameModenaming-drift, anatomy, block-breaking, block-interaction, gui-and-screens, input-and-keybinds, prediction-and-acks, synched-entity-data, codecs-nbt-json, containers-and-menus, enchanting, enchantments, using-an-item, the-connection, VIII · The player, player-anatomy, the-spear, the-sword-swing, particles, section-meshing, fluids, lighting
MultipleTestTrackergame-tests
MultiplyValueenchantments
MultiVariantnaming-drift
MushroomCowsynched-entity-data
MusicManagersound-engine, what-makes-a-sound
MusicToastDisplayStatehud, what-makes-a-sound
MutableComponenttext-and-fonts, text-components
MyceliumBlockwhat-this-book-skips
Nameableentity-selectors, entity-anatomy
NameAndIdnaming-drift, players-and-sessions
NamedRulebrigadier-and-commands
NarratableEntrygui-and-screens
NarratorStatusgui-and-screens
NativeImageXI · Rendering, the-window
NativeLibrariesBootstrapanatomy, sound-engine, the-window
NaturalSpawnerentity-anatomy, entity-lifecycle, what-the-client-is-told, server-level-tick, biomes
NbtAccountercodecs-nbt-json, text-components, packets-and-stream-codecs
NbtContentsscoreboard-and-data, text-components
NbtExceptioncodecs-nbt-json
NbtIoglossary, codecs-nbt-json, how-a-server-dies, players-and-sessions, chunk-storage
NbtOpsglossary, codecs-nbt-json, text-components, packets-and-stream-codecs
NbtPathArgumentscoreboard-and-data, text-components
NbtProviderdata-driven-types, contexts-and-predicates
NbtProvidersdata-driven-types
NbtTagArgumentbrigadier-and-commands
NbtToSnbtwhat-this-book-skips
NbtUtilsentity-selectors, codecs-nbt-json
NearestAttackableTargetGoalai-goals-and-brains
NearestBedSensorpoints-of-interest
NeedleDirectionHelpermodels-and-atlases
NeighborUpdaterblock-update-flags, block-interaction, blocks-and-states, signal-and-dust
NestedLootTablenaming-drift, loot-tables
NetherFeatureswhat-this-book-skips
NetherFortressPieceshand-built-structures
NetherFortressStructureentity-lifecycle
NetherFossilPieceshand-built-structures
NetherFossilStructurehand-built-structures
NetworkRegistryLoadTaskidentifiers-and-registries
NeutralMobentity-anatomy
NewMinecartBehaviorinput-to-movement
NoDataSpecialModelRendererblock-entity-rendering
Nodepathfinding
NodeEvaluatorglossary, pathfinding
Noisedensity-functions
NoiseBasedChunkGeneratorchunk-anatomy, chunk-generation-pipeline, biomes, blending, creating-a-world, density-functions, structure-placement, terrain
NoiseBasedCountPlacementfeatures-and-placement
NoiseChunkdensity-function-nodes, contexts-and-predicates, blending, density-functions, structure-placement, terrain
NoiseFunctiondensity-function-nodes, density-functions
NoiseGeneratorSettingsmath-and-primitives, what-this-book-skips, terrain
NoiseRouterglossary, density-functions, terrain
NoiseRouterDatawhat-this-book-skips, blending, density-functions
Noisesdensity-functions
NoiseSettingsterrain
NoiseSpawnFindernaming-drift, biomes
NoiseStackdensity-functions
NoiseThresholdCountPlacementfeatures-and-placement
NoiseUtilsdensity-functions
NoOpFeaturefeatures-and-placement
NoRenderParticleparticles
NoRenderParticleGroupparticles
NormalCraftingReciperecipes
NormalNoisedensity-function-nodes, density-functions
NoteBlockpistons-and-block-events
NoteBlockInstrumentblocks-and-states
NoticeDialogdialogs
NotificationManagerhow-a-server-dies, players-and-sessions, server-tick, starting-a-server
NowPlayingToasthud, what-makes-a-sound
NullOpscodecs-nbt-json, packets-and-stream-codecs
NumberFormatscoreboard-and-data, data-driven-types
NumberFormatTypedata-driven-types
NumberFormatTypesscoreboard-and-data
NumberRangeInputdialogs
NumericTagcodecs-nbt-json
ObjectContentstext-components
ObjectInfotext-components
Objectivescoreboard-and-data, data-driven-types
ObjectiveArgumentscoreboard-and-data
ObjectiveCriteriaglossary, what-this-book-skips, scoreboard-and-data
ObjectSelectionListgui-and-screens
ObserverBlockdiodes-and-observers, signal-and-dust, scheduled-ticks
OceanMonumentPiecesbiggest, hand-built-structures
OceanMonumentStructurehand-built-structures
OceanRuinPieceshand-built-structures
OceanRuinStructurehand-built-structures
OctahedralGroupmath-and-primitives
Octreevisibility-and-the-frame-graph
OffsetPlacementnaming-drift, features-and-placement
OitPipelineSetblaze3d
OitStagenaming-drift, submit-phases, particles, visibility-and-the-frame-graph
OldMinecartBehaviorinput-to-movement
OldUsersConverterlevel-data-and-rules, starting-a-server
OminousBottleAmplifierhunger-and-experience
OminousItemSpawnernon-living-damage
OneShotai-goals-and-brains
OptionInstanceoptions, the-client-loop
Optionsbiggest, packages, hud-elements, anatomy, what-this-book-skips, debugging-the-running-game, input-and-keybinds, options, sound-engine, the-client-level, the-client-loop, resource-system, chat-and-signing, input-to-movement, the-sword-swing, lightmap-fog-and-sky, models-and-atlases, post-processing, section-meshing, the-window, chunk-storage
OptionsListgui-and-screens
OptionsRenderStatepost-processing
OptionsScreengui-and-screens
OptionsSubScreenoptions
OrderedSubmitNodeCollectorentity-rendering
OreFeaturechunk-anatomy, chunk-generation-pipeline, features-and-placement
OreVeinRulenaming-drift, terrain
Orientationsignal-and-dust, debugging-the-running-game
OutgoingChatMessagechat-and-signing
Overlaygui-and-screens
OverlayedPackResourcesnaming-drift, resource-system
OverlayFeaturefeatures-and-placement
OverlayMetadataSectionresource-system
OverlayTextureentity-rendering
OversizedItemRenderStatethe-gui-render-tree
OverworldBiomeBuilderbiomes
OwnableEntityentity-anatomy
Packresource-system, creating-a-world
PackCompatibilityresource-system
PackDetectorresource-system
PackedClockStatesenvironment-attributes-and-timelines
Packetfanin, hierarchy, threads, packets-and-stream-codecs, the-connection
PacketBundlePackerpackets-and-stream-codecs, the-connection
PacketBundleUnpackerpackets-and-stream-codecs, the-connection
PacketDecodercodecs-nbt-json, text-components, packets-and-stream-codecs, the-connection
PacketEncodercodecs-nbt-json, text-components, packets-and-stream-codecs, the-connection
PacketFlowpackets-and-stream-codecs
PacketListeneranatomy, protocol-phases, the-connection, server-tick
PacketProcessornaming-drift, anatomy, the-client-loop, dialogs, synched-entity-data, containers-and-menus, the-connection, input-to-movement, the-sword-swing, how-a-server-dies, server-tick, starting-a-server
PacketReportpackets-and-stream-codecs
PacketSendListenertext-components, the-connection
PacketTypefanin, glossary, packets-and-stream-codecs
PacketUtilsnaming-drift, threads, anatomy, synched-entity-data, containers-and-menus, protocol-phases, the-connection, server-tick
PackFormatresource-system
PackLocationInfoidentifiers-and-registries, resource-system
PackMetadataSectionresource-system
PackRepositoryresource-system
PackResourcesresource-system
PackSelectionConfigresource-system
PackSourceglossary, resource-system, creating-a-world
PackTyperesource-system
PaleMossDecoratortrees
Palettechunk-anatomy
PalettedContainernaming-drift, section-meshing, chunk-anatomy, biomes
PalettedContainerFactorychunk-anatomy
PalettedContainerROchunk-anatomy
PalettedPermutationsmodels-and-atlases
PalettedTextureManagerresource-system
PaletteResizechunk-anatomy
PandaVI · Entities
Panoramathe-gui-render-tree
PanoramaRenderStatethe-gui-render-tree
Parrottags
ParsedTemplatedialogs
ParserBasedArgumentnaming-drift
ParseResultsthe-execution-engine
PartDefinitionentity-rendering
Particleparticles
ParticleEngineglossary, math-and-primitives, the-client-level, the-client-loop, particles
ParticleGroupnaming-drift, particles
ParticleGroupRenderStateparticles
ParticleLimitnaming-drift, particles
ParticleOptionsdata-driven-types, status-effects, particles
ParticleProviderparticles
ParticleRenderTypeparticles
ParticleResourcesresource-system, models-and-atlases, particles
ParticlesRenderStateparticles
ParticleTypedata-driven-types, particles
ParticleTypesnon-living-damage, the-sword-swing
PartPoseentity-rendering
PatchedDataComponentMapnaming-drift, data-components, items-and-stacks
Pathdebugging-the-running-game, pathfinding, points-of-interest
PathComputationTypepathfinding
PathFinderglossary, pathfinding
PathfinderMobhierarchy, entity-anatomy, synched-entity-data
PathfindingContextpathfinding
PathNavigationattributes, entity-anatomy, pathfinding, points-of-interest
PathNavigationRegionpathfinding, chunk-anatomy
PathPackResourcesresource-system
PathTypenaming-drift, pathfinding
PathTypeCachepathfinding
PatrolSpawnerentity-lifecycle, starting-a-server, points-of-interest, XII · World generation
PauseScreengui-and-screens
PerfCommandwhat-this-book-skips
PeriodicNotificationManagerthe-client-loop, resource-system
PerlinNoisedensity-functions
Permissionglossary, permissions, data-driven-types
PermissionCheckglossary, naming-drift, permissions, data-driven-types
PermissionCheckTypespermissions
PermissionLevelglossary, naming-drift, permissions
PermissionProviderChecknaming-drift, permissions
Permissionslevel-data-and-rules, naming-drift, entity-selectors, permissions, text-components, player-anatomy
PermissionSetglossary, naming-drift, functions-and-macros, permissions, player-anatomy
PermissionSetSuppliernaming-drift, brigadier-and-commands, permissions
PermissionSetUnionpermissions
PermissionTypespermissions
PersistentEntitySectionManagerentity-anatomy, entity-lifecycle, how-a-server-dies, players-and-sessions, server-level-tick, chunk-storage, scheduled-ticks, tickets-and-loading
Phantomentity-anatomy
PhantomSpawnerentity-lifecycle, starting-a-server, XII · World generation
PictureInPictureRendererthe-gui-render-tree
PictureInPictureRenderStatethe-gui-render-tree
PiecesContainerglossary, structure-placement
PiercingWeapondata-components, the-spear, the-sword-swing
Pigentity-lifecycle
Piglinai-goals-and-brains, pathfinding, the-spear
PiglinAiai-goals-and-brains, contexts-and-predicates
PiglinSpecificSensorai-goals-and-brains
Pillagerentity-lifecycle
PineFoliagePlacertrees
PingDebugCharthud
PingDebugMonitorthreads, debugging-the-running-game
PipelineBuilderblaze3d
PipelineCachenaming-drift, blaze3d, post-processing
PistonBaseBlockpistons-and-block-events, signal-and-dust, server-level-tick
PistonHeadBlockpistons-and-block-events
PistonHeadRendererblock-entity-rendering
PistonHeadRenderStateblock-entity-rendering
PistonMathpistons-and-block-events
PistonMovingBlockEntityblock-entities, pistons-and-block-events
PistonStructureResolverpistons-and-block-events
PlaceCommandbrigadier-and-commands
PlacedFeaturedata-driven-types, features-and-placement, jigsaw-and-templates
PlacementContextXII · World generation, features-and-placement
PlacementFilterfeatures-and-placement
PlacementInfonaming-drift, recipes
PlacementModifiernaming-drift, data-driven-types, features-and-placement
PlaceOnGroundDecoratortrees
PlaceRecipeHelperrecipes
PlainMessagedialogs
PlainTextContentsnaming-drift, text-components
PlainTextFunctionfunctions-and-macros
Playerbiggest, fanin, glossary, hud-elements, naming-drift, non-living-damage, block-breaking, block-interaction, gui-and-screens, what-makes-a-sound, scoreboard-and-data, attributes, authority, damage-and-death, entity-anatomy, entity-lifecycle, movement-and-collision, synched-entity-data, text-components, containers-and-menus, enchanting, enchantments, items-and-stacks, loot-tables, recipes, using-an-item, VIII · The player, hunger-and-experience, input-to-movement, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, block-entity-rendering, server-level-tick, chunk-storage
PlayerAdvancementsnaming-drift, what-this-book-skips, advancements, how-a-server-dies, players-and-sessions
PlayerChatMessagetext-components, chat-and-signing
PlayerChunkSenderwhat-the-client-is-told, players-and-sessions, server-tick, lighting, tickets-and-loading
PlayerDataStoragelevel-data-and-rules, how-a-server-dies, players-and-sessions, starting-a-server
PlayerEnderChestContainerplayer-anatomy
PlayerEquipmentplayer-anatomy
PlayerGlyphProvidertext-and-fonts
PlayerInfochat-and-signing, player-anatomy
PlayerListlevel-data-and-rules, naming-drift, threads, what-makes-a-sound, advancements, brigadier-and-commands, entity-selectors, scoreboard-and-data, resource-system, tags, text-components, recipes, chat-and-signing, protocol-phases, what-the-client-is-told, particles, how-a-server-dies, players-and-sessions, server-tick, tickets-and-loading
PlayerMaptickets-and-loading
PlayerModelplayer-anatomy
PlayerModelPartplayer-anatomy, entity-rendering
PlayerModelTypeplayer-anatomy, entity-rendering
PlayerPredicateadvancements, data-driven-types, input-to-movement
PlayerRideableJumpingentity-anatomy
PlayerSafetyServiceTextFilterchat-and-signing
PlayerScoreEntryscoreboard-and-data
PlayerScoresscoreboard-and-data
PlayerSkinplayer-anatomy, entity-rendering
PlayerSkinRenderCacheentity-rendering
PlayerSpawnFinderplayers-and-sessions, starting-a-server, tickets-and-loading, biomes
PlayerSpritetext-components
PlayerTabOverlayhud-elements
PlayerTeamscoreboard-and-data, text-components
PlayerTriggeradvancements
PlaySoundConsumeEffectusing-an-item
PlaySoundEffectenchantments
PoiCompetitorScanai-goals-and-brains, points-of-interest
PoiManagerglossary, VI · Entities, ai-goals-and-brains, server-tick, chunk-storage, points-of-interest
PointGizmodebugging-the-running-game
PoiRecordpoints-of-interest
PoiSectionpoints-of-interest
PoisonMobEffectstatus-effects
PoiTypeai-goals-and-brains, points-of-interest
PoiTypespoints-of-interest
PoiTypeTagstags, points-of-interest
PolygonModeblaze3d
PoolAliasBindingdata-driven-types, jigsaw-and-templates
PoolAliasBindingsjigsaw-and-templates
PoolAliasLookupjigsaw-and-templates
PoolElementStructurePieceglossary, jigsaw-and-templates
Poolswhat-this-book-skips
PoplarFoliagePlacertrees
PoplarTrunkPlacertrees
PortalForcerpoints-of-interest
Posedamage-and-death, entity-anatomy, synched-entity-data
PoseStackthe-gui-render-tree, blaze3d, entity-rendering
Positionmath-and-primitives
PositionalRandomFactorymath-and-primitives
PositionCollisionContextmath-and-primitives
PositionMoveRotationthe-client-level, input-to-movement
PositionSourcedata-driven-types, game-events-and-vibrations
PositionSourceTypedata-driven-types
PosRuleTestdata-driven-types, jigsaw-and-templates, structure-placement
PosRuleTestTypedata-driven-types
PostChainpost-processing
PostChainConfigpost-processing
PostEffectCommandpost-processing
PostPasspost-processing
PostPlacementProcessorstructure-placement
PostSpawnProcessorentity-anatomy
PotDecorationswhat-this-book-skips
PotentialCalculatorentity-lifecycle
PotentSulfurBlockpistons-and-block-events
Potionhierarchy, identifiers-and-registries
PotionContentshunger-and-experience, status-effects
PowderedSnowFogEnvironmentlightmap-fog-and-sky
PowFunctiondensity-function-nodes
PredictiveActionprediction-and-acks
PreeditEventnaming-drift, input-and-keybinds
PreferredGraphicsApithe-window
PreparableReloadListenerglossary, resource-system, models-and-atlases, post-processing
PreparedRenderTypeblaze3d
PrepareSpawnTaskprotocol-phases, player-anatomy, players-and-sessions, starting-a-server, tickets-and-loading
PresetEditorcreating-a-world
PresetFlatWorldScreencreating-a-world
PrimaryLevelDatalevel-data-and-rules, naming-drift, starting-a-server, creating-a-world
PrimedTntnon-living-damage, the-client-level, movement-and-collision
PrimitiveTagcodecs-nbt-json
PrimitiveTopologynaming-drift, blaze3d
PrioritizeChunkUpdatessection-meshing
PriorityConsecutiveExecutorthreads, anatomy, chunk-storage
PriorityProviderentity-lifecycle
ProblemReportercodecs-nbt-json
ProcessorRulejigsaw-and-templates
ProfiledReloadInstanceresource-system
ProfilePublicKeychat-and-signing
Profilerwhat-this-book-skips
ProfilerFillerwhat-this-book-skips, server-tick
ProfilerPieCharthud
Projectilehierarchy, non-living-damage, entity-anatomy, using-an-item
ProjectileUtilthe-spear, the-sword-swing
ProjectileWeaponItemenchantments, items-and-stacks, using-an-item
Projectionpost-processing
ProjectionMatrixBufferblaze3d, post-processing
Propertyblocks-and-states
ProtectedBlockProcessorjigsaw-and-templates
ProtoChunkchunk-anatomy, chunk-generation-pipeline, chunk-storage, lighting, scheduled-ticks
ProtoChunkTickschunk-anatomy, scheduled-ticks
ProtocolCodecBuilderpackets-and-stream-codecs
ProtocolInfopackets-and-stream-codecs, protocol-phases, the-connection
ProtocolInfoBuildernaming-drift, packets-and-stream-codecs
ProtocolSwapHandlerthe-connection
ProviderReferenceDefinitiontext-and-fonts
PunchTreeTutorialStepInstancetags
PushReactionpistons-and-block-events
QuadCollectionmodels-and-atlases
QuadParticleFeatureRenderersubmit-phases, particles
QuadParticleGroupparticles
QuadParticleRenderStateparticles
QuartPosglossary, math-and-primitives
QueryThreadGs4threads, what-this-book-skips, how-a-server-dies, starting-a-server
Rabbitpathfinding
Raidscoreboard-and-data, entity-lifecycle, points-of-interest
RaidCommandbrigadier-and-commands
Raiderentity-lifecycle
Raidslevel-data-and-rules, server-level-tick, points-of-interest
RandomBooleanSelectorFeaturefeatures-and-placement
RandomChancePlacementfeatures-and-placement
RandomGroupPoolAliasjigsaw-and-templates
RandomizableContainerdata-driven-types, contexts-and-predicates, loot-tables
RandomizableContainerBlockEntityloot-tables
RandomLookAroundGoalai-goals-and-brains
RandomlySelectedPlacementfeatures-and-placement
RandomPoolAliasjigsaw-and-templates
RandomSelectorFeaturefeatures-and-placement
RandomSequencemath-and-primitives
RandomSequenceslevel-data-and-rules, math-and-primitives, contexts-and-predicates
RandomSourcefanin, packages, math-and-primitives, entity-selectors, contexts-and-predicates, enchanting, enchantments, loot-tables, creating-a-world
RandomSpreadFoliagePlacertrees
RandomSpreadStructurePlacementstructure-placement
RandomSpreadTypestructure-placement
RandomStatedensity-function-nodes, math-and-primitives, naming-drift, what-the-client-is-told, server-level-tick, biomes, density-functions, terrain
RandomSupportmath-and-primitives
RangeChoiceFunctiondensity-function-nodes
RangedAttributeattributes
RangedBowAttackGoalpathfinding
RangedCrossbowAttackGoalusing-an-item
RangeSelectItemModelnaming-drift, models-and-atlases
RangeSelectItemModelPropertiesmodels-and-atlases
RangeSlotSourcecontexts-and-predicates
RarityFilterfeatures-and-placement
RateKickingConnectionthe-connection
Ravagerdamage-and-death
RconClientthreads, what-this-book-skips
RconConsoleSourcewhat-this-book-skips, brigadier-and-commands
RconThreadthreads, what-this-book-skips, how-a-server-dies, starting-a-server
ReadOnlyScoreInfoscoreboard-and-data
RealmsConnectthreads, protocol-phases, the-connection
RealmsScreenhierarchy, what-this-book-skips
Recipeglossary, naming-drift, data-driven-types, recipes
RecipeAccessrecipes
RecipeBookwhat-this-book-skips
RecipeBookCategoriesrecipes
RecipeBookCategoryrecipes
RecipeBookComponentrecipes
RecipeBookMenurecipes
RecipeBookSettingswhat-this-book-skips
RecipeBookTyperecipes
RecipeCacherecipes
RecipeCollectionrecipes
RecipeCraftedTriggeradvancements
RecipeCraftingHolderrecipes
RecipeDisplayglossary, data-driven-types, recipes, packets-and-stream-codecs
RecipeDisplayEntrynaming-drift, recipes
RecipeDisplayIdglossary, recipes
RecipeHolderrecipes
RecipeInputrecipes
RecipeManagernaming-drift, block-entities, data-driven-types, resource-system, recipes
RecipeMapnaming-drift, data-driven-types, recipes
RecipePropertySetrecipes
RecipeSerializerdata-driven-types, recipes
RecipeSerializersrecipes
RecipeToasthud
RecipeTypedata-driven-types, recipes
RecipeUnlockedTriggeradvancements
RecordCodecBuilderfanin, codecs-nbt-json
RectGizmodebugging-the-running-game
RedStoneOreBlockprediction-and-acks
RedstoneSidesignal-and-dust
RedstoneTorchBlocksignal-and-dust, scheduled-ticks
RedstoneWallTorchBlocksignal-and-dust
RedstoneWireBlockintroduction, naming-drift, diodes-and-observers, signal-and-dust
RedstoneWireEvaluatorsignal-and-dust
ReentrantBlockableEventLoopthreads, anatomy, the-client-loop, server-tick, points-of-interest
RegenerationMobEffectstatus-effects
RegionBitmapchunk-storage
RegionFilecodecs-nbt-json, chunk-storage
RegionFileStoragecodecs-nbt-json, chunk-storage
RegionFileVersioncodecs-nbt-json, starting-a-server, chunk-storage
RegionStorageInfochunk-storage
RegionStorageUpgraderchunk-storage
RegistrationInfoidentifiers-and-registries
Registriesfanin, density-function-nodes, glossary, level-data-and-rules, naming-drift, what-this-book-skips, V · Blocks, debugging-the-running-game, what-makes-a-sound, game-tests, ai-goals-and-brains, attributes, entity-anatomy, data-driven-types, identifiers-and-registries, tags, contexts-and-predicates, enchanting, enchantments, recipes, hunger-and-experience, environment-attributes-and-timelines, creating-a-world, features-and-placement, hand-built-structures, jigsaw-and-templates, structure-placement, trees
Registryentity-anatomy, codecs-nbt-json, data-components, data-driven-types, identifiers-and-registries, tags
RegistryAccessidentifiers-and-registries, packets-and-stream-codecs
RegistryCodecsnaming-drift, identifiers-and-registries, tags
RegistryDataCollectordata-components, identifiers-and-registries, tags, protocol-phases
RegistryDataLoadernaming-drift, advancements, codecs-nbt-json, data-driven-types, identifiers-and-registries, resource-system, contexts-and-predicates, enchantments, recipes, starting-a-server, environment-attributes-and-timelines, creating-a-world
RegistryFileCodeccodecs-nbt-json, data-driven-types, identifiers-and-registries, contexts-and-predicates, density-functions
RegistryFixedCodeccodecs-nbt-json, identifiers-and-registries, game-events-and-vibrations
RegistryFriendlyByteBuffanin, synched-entity-data, codecs-nbt-json, identifiers-and-registries, packets-and-stream-codecs
RegistryLayernaming-drift, data-driven-types, identifiers-and-registries, resource-system, tags, contexts-and-predicates, enchantments
RegistryLoadTaskidentifiers-and-registries, tags, loot-tables
RegistryOpscodecs-nbt-json, data-driven-types, identifiers-and-registries, text-components
RegistrySetBuilderidentifiers-and-registries
RegistrySynchronizationdata-components, identifiers-and-registries, tags, contexts-and-predicates, enchantments
RegistryValidatoridentifiers-and-registries
Relativeinput-to-movement
ReloadableResourceManageranatomy, resource-system, models-and-atlases
ReloadableServerRegistriesnaming-drift, data-driven-types, identifiers-and-registries, resource-system, tags, contexts-and-predicates, loot-tables, starting-a-server
ReloadableServerResourcesbrigadier-and-commands, data-components, data-driven-types, resource-system, tags, starting-a-server, creating-a-world
ReloadCommandresource-system
ReloadInstanceanatomy, resource-system
RemoteChatSessionchat-and-signing, player-anatomy
RemoteFriendListUpdateHandlerthe-client-loop
RemotePlayerglossary, damage-and-death, entity-anatomy, player-anatomy, the-sword-swing, the-two-phase-tick
RemoteSampleLoggerdebugging-the-running-game
RemoteSlotcodecs-nbt-json, containers-and-menus
RemoveBinomialenchantments
Removeddata-components
RemoveStatusEffectsConsumeEffectusing-an-item
RenderBufferssection-meshing, the-frame
RenderLayerentity-rendering
RenderPassglossary, submit-phases, blaze3d, post-processing, visibility-and-the-frame-graph
RenderPassBackendblaze3d
RenderPassDescriptorblaze3d
RenderPipelineglossary, naming-drift, blaze3d, post-processing
RenderPipelinesnaming-drift, blaze3d, lightmap-fog-and-sky, particles, post-processing
RenderRegionCachesection-meshing
RenderSectionRegionthe-client-level, section-meshing
RenderSetupnaming-drift, blaze3d
RenderShapenaming-drift
RenderSystemintroduction, packages, glossary, naming-drift, threads, anatomy, what-this-book-skips, input-and-keybinds, the-client-loop, blaze3d, post-processing, the-frame, the-window
RenderTargetblaze3d
RenderTypeglossary, naming-drift, submit-phases, blaze3d, block-entity-rendering, entity-rendering, models-and-atlases
RenderTypeFeatureRenderersubmit-phases, entity-rendering
RenderTypesnaming-drift, blaze3d, entity-rendering
Repairabledata-components
RepairItemRecipeenchanting, recipes
RepeaterBlockdiodes-and-observers, signal-and-dust, scheduled-ticks
RepeatingPlacementfeatures-and-placement
ReportedExceptionthe-client-loop, containers-and-menus, the-connection, how-a-server-dies, server-tick
ReportGameListenergame-tests
ReportingContextwhat-this-book-skips
ReportTypehow-a-server-dies
RepositorySourceresource-system
ResetRaidStatusai-goals-and-brains
ResolutionContextnaming-drift, entity-selectors, text-components
ResolvableProfiletext-components, player-anatomy
ResolvedModelmodels-and-atlases
ResolvedServerAddressthe-connection
Resourceresource-system
ResourceArgumentbrigadier-and-commands
ResourceFilterSectionresource-system
ResourceKeyfanin, level-data-and-rules, blocks-and-states, advancements, brigadier-and-commands, game-tests, entity-anatomy, identifiers-and-registries, tags
ResourceKeyArgumentbrigadier-and-commands
ResourceLoadStateTrackerresource-system
ResourceManageridentifiers-and-registries, resource-system
ResourceManagerRegistryLoadTaskidentifiers-and-registries, tags, recipes
ResourceManagerReloadListenerresource-system
ResourceManagerTemplateSourcejigsaw-and-templates
ResourceMetadataresource-system
ResourceOrIdArgumentbrigadier-and-commands, contexts-and-predicates
ResourceOrTagArgumentbrigadier-and-commands, tags
ResourceOrTagKeyArgumentbrigadier-and-commands, tags
ResourceSelectorArgumentnaming-drift, brigadier-and-commands, game-tests, tags
RespawnAnchorBlockblock-interaction
ResultContainerrecipes
ResultSlotcontainers-and-menus, recipes
ReturnCommandthe-execution-engine
RootCommandNodebrigadier-and-commands
RootPlacerdata-driven-types, trees
RootPlacerTypedata-driven-types, trees
RotatingSectionStoragesection-meshing, visibility-and-the-frame-graph
Rotationmath-and-primitives
Rotationsmath-and-primitives
RotationSegmentblocks-and-states
RoundFunctiondensity-function-nodes, density-functions
RuinedPortalPiecehand-built-structures
RuinedPortalStructurehand-built-structures
RuleBlockEntityModifierdata-driven-types, jigsaw-and-templates
RuleBlockEntityModifierTypedata-driven-types
RuleProcessorjigsaw-and-templates
RuleTestdata-driven-types, jigsaw-and-templates
RuleTestTypedata-driven-types
RunFunctionfunctions-and-macros
RunningOnDifferentThreadExceptionthe-connection, server-tick
RunOneai-goals-and-brains
SampleLoggerdebugging-the-running-game, server-tick
SamplerCacheblaze3d
SamplerContextdensity-function-nodes, naming-drift, density-functions
SampleStoragedebugging-the-running-game
SaplingBlockfeatures-and-placement, trees
SavedDataglossary, level-data-and-rules, naming-drift, scoreboard-and-data, contexts-and-predicates, how-a-server-dies, server-tick, IV · The world, environment-attributes-and-timelines, tickets-and-loading, creating-a-world
SavedDataStoragelevel-data-and-rules, naming-drift, how-a-server-dies, starting-a-server, chunk-storage, creating-a-world
SavedDataTypelevel-data-and-rules, creating-a-world
SavedTickscheduled-ticks
ScaleExponentiallyenchantments
ScatteredFeaturePiecehand-built-structures
ScheduleCommandfunctions-and-macros
ScheduledTickscheduled-ticks
ScheduledTickAccessthe-client-level, scheduled-ticks
Schemafanin
Scopebrigadier-and-commands
Scorescoreboard-and-data, data-driven-types
ScoreAccessglossary, scoreboard-and-data
Scoreboardscoreboard-and-data
ScoreboardCommandbrigadier-and-commands, scoreboard-and-data
ScoreboardNameProviderdata-driven-types, contexts-and-predicates
ScoreboardNameProvidersdata-driven-types
ScoreboardSaveDatalevel-data-and-rules, scoreboard-and-data
ScoreboardValuedata-driven-types
ScoreContentsscoreboard-and-data, text-components
ScoreHolderentity-selectors, scoreboard-and-data, entity-anatomy, text-components
ScoreHolderArgumentbrigadier-and-commands, entity-selectors, scoreboard-and-data
Screenhierarchy, hud-elements, naming-drift, X · The client, gui-and-screens, input-and-keybinds, the-client-loop, the-gui-render-tree, permissions
ScreenAxisgui-and-screens
ScreenDirectiongui-and-screens
ScreenEffectRendererthe-frame
ScreenNarrationCollectorgui-and-screens
ScreenPositiongui-and-screens
ScreenRectanglegui-and-screens
Screenshottext-components, the-window
ScrollWheelHandlerinput-and-keybinds
SculkBehaviourV · Blocks
SculkBlockV · Blocks
SculkCatalystBlockEntitygame-events-and-vibrations
SculkSensorBlockgame-events-and-vibrations, scheduled-ticks
SculkSensorBlockEntitygame-events-and-vibrations
SculkSensorPhasegame-events-and-vibrations
SculkShriekerBlockgame-events-and-vibrations
SculkShriekerBlockEntitygame-events-and-vibrations
SculkSpreaderV · Blocks
SculkVeinBlockV · Blocks
SdlDebugthe-window
SDLEventHandlerglossary, naming-drift, input-and-keybinds, the-client-loop, the-window
SecondaryPoiSensorai-goals-and-brains
SectionBufferBuilderPacksection-meshing
SectionBufferBuilderPoolsection-meshing
SectionCompilerblock-entity-rendering, models-and-atlases, section-meshing
SectionCopysection-meshing, chunk-anatomy
SectionOcclusionGraphvisibility-and-the-frame-graph
SectionPosmath-and-primitives, entity-lifecycle, IV · The world, game-events-and-vibrations, lighting, tickets-and-loading
SectionRenderDispatchernaming-drift, threads, anatomy, the-client-loop, blaze3d, section-meshing, visibility-and-the-frame-graph
SectionStoragehow-a-server-dies, server-tick, chunk-generation-pipeline, chunk-storage, points-of-interest
SectionTaskDynamicQueuesection-meshing
SectionTrackerlighting, points-of-interest
SectionUpdateTrackersection-meshing
SeedCommandpermissions
SeededContainerLootloot-tables
SelectableReciperecipes
SelectBlockModelblock-entity-rendering
SelectItemModelnaming-drift, models-and-atlases
SelectItemModelPropertiesmodels-and-atlases
SelectorContentstext-components
SelectWorldScreencreating-a-world
Sensingai-goals-and-brains
Sensorai-goals-and-brains
SequenceFeaturefeatures-and-placement
SequenceFunctionnaming-drift, data-driven-types, loot-tables
SequentialEntryloot-tables
SerializableChunkDatanaming-drift, chunk-anatomy, chunk-generation-pipeline, chunk-storage, lighting, points-of-interest, scheduled-ticks, blending
SerializableTickContainerscheduled-ticks
ServerActivityMonitorserver-tick
ServerAddressthe-connection
ServerAdvancementManageradvancements, resource-system
ServerBossEventglossary, scoreboard-and-data
ServerboundAcceptCodeOfConductPacketprotocol-phases
ServerboundAcceptTeleportationPacketinput-to-movement
ServerboundAttackPacketnaming-drift, synched-entity-data, the-sword-swing
ServerboundBlockEntityTagQueryPacketdebugging-the-running-game
ServerboundChangeGameModePacketinput-and-keybinds
ServerboundChatAckPacketchat-and-signing
ServerboundChatCommandPacketbrigadier-and-commands, chat-and-signing
ServerboundChatCommandSignedPacketchat-and-signing
ServerboundChatPackettext-components, chat-and-signing
ServerboundChatSessionUpdatePacketchat-and-signing
ServerboundChunkBatchReceivedPacketthreads, what-the-client-is-told
ServerboundClientCommandPacketlevel-data-and-rules
ServerboundClientInformationPacketoptions, synched-entity-data, protocol-phases
ServerboundClientTickEndPacketthe-client-loop, input-to-movement
ServerboundCommandSuggestionPacketbrigadier-and-commands, packets-and-stream-codecs
ServerboundConfigurationAcknowledgedPacketprotocol-phases
ServerboundContainerButtonClickPacketenchanting
ServerboundContainerClickPacketcodecs-nbt-json, containers-and-menus, packets-and-stream-codecs
ServerboundContainerClosePacketgui-and-screens
ServerboundCookieResponsePacketprotocol-phases
ServerboundCustomClickActionPacketdialogs, packets-and-stream-codecs
ServerboundCustomPayloadPacketpackets-and-stream-codecs
ServerboundCustomQueryAnswerPacketprotocol-phases
ServerboundDebugSubscriptionRequestPacketdebugging-the-running-game
ServerboundEntityTagQueryPacketdebugging-the-running-game
ServerboundFinishConfigurationPacketidentifiers-and-registries, packets-and-stream-codecs, protocol-phases
ServerboundHelloPacketnaming-drift, protocol-phases
ServerboundInteractPacketnaming-drift, synched-entity-data
ServerboundJigsawGeneratePacketjigsaw-and-templates
ServerboundKeepAlivePacketthreads
ServerboundKeyPacketnaming-drift, protocol-phases
ServerboundLoginAcknowledgedPacketprotocol-phases
ServerboundMovePlayerPacketinput-to-movement
ServerboundMoveVehiclePacketthe-client-level, authority, input-to-movement
ServerboundPacketListenerpackets-and-stream-codecs
ServerboundPingRequestPacketprotocol-phases
ServerboundPlaceRecipePacketrecipes
ServerboundPlayerAbilitiesPacketplayer-anatomy
ServerboundPlayerActionPacketblock-breaking, block-interaction, prediction-and-acks, using-an-item, the-spear
ServerboundPlayerCommandPacketinput-to-movement
ServerboundPlayerInputPacketnaming-drift, input-to-movement
ServerboundPlayerLoadedPacketpackets-and-stream-codecs, players-and-sessions
ServerboundPunchPacketnaming-drift, block-breaking, input-and-keybinds, the-connection, the-sword-swing
ServerboundResourcePackPacketresource-system, packets-and-stream-codecs
ServerboundSeenAdvancementsPacketadvancements
ServerboundSelectKnownPacksidentifiers-and-registries
ServerboundSetCarriedItemPacketblock-breaking, block-interaction, containers-and-menus, player-anatomy
ServerboundSetCreativeModeSlotPacketcodecs-nbt-json, containers-and-menus, packets-and-stream-codecs
ServerboundSetGameRulePacketlevel-data-and-rules
ServerboundSetJigsawBlockPacketjigsaw-and-templates
ServerboundSetStructureBlockPacketjigsaw-and-templates
ServerboundStatusRequestPacketprotocol-phases
ServerboundUseItemOnPacketblock-interaction, prediction-and-acks
ServerboundUseItemPacketblock-interaction, prediction-and-acks, using-an-item
ServerChunkCachelevel-data-and-rules, anatomy, block-entities, entity-lifecycle, what-the-client-is-told, input-to-movement, how-a-server-dies, players-and-sessions, server-level-tick, server-tick, chunk-generation-pipeline, chunk-storage, game-events-and-vibrations, lighting, tickets-and-loading
ServerClockManagerglossary, level-data-and-rules, naming-drift, server-level-tick, server-tick, starting-a-server, environment-attributes-and-timelines
ServerCodeOfConductConfigurationTaskprotocol-phases
ServerCommandSuggestionsProviderthreads, brigadier-and-commands, server-tick
ServerCommonPacketListenerpackets-and-stream-codecs
ServerCommonPacketListenerImplbiggest, anatomy, synched-entity-data, resource-system, protocol-phases, the-connection, how-a-server-dies, players-and-sessions, server-tick
ServerConfigurationPacketListenerImplthreads, identifiers-and-registries, protocol-phases, players-and-sessions
ServerConnectionListeneranatomy, protocol-phases, the-connection, how-a-server-dies, players-and-sessions, server-tick, starting-a-server
ServerDatathe-connection
ServerDebugSubscribersdebugging-the-running-game, pathfinding, server-tick
ServerEntityattributes, entity-anatomy, movement-and-collision, synched-entity-data, packets-and-stream-codecs, what-the-client-is-told, server-level-tick
ServerEntityGetterIII · The server
ServerFunctionLibraryfunctions-and-macros, resource-system, tags
ServerFunctionManagerfunctions-and-macros, the-execution-engine, resource-system, server-tick
ServerGamePacketListenerpackets-and-stream-codecs
ServerGamePacketListenerImplbiggest, level-data-and-rules, naming-drift, threads, block-breaking, block-interaction, debugging-the-running-game, prediction-and-acks, advancements, brigadier-and-commands, permissions, authority, damage-and-death, synched-entity-data, containers-and-menus, enchantments, recipes, using-an-item, chat-and-signing, packets-and-stream-codecs, protocol-phases, the-connection, input-to-movement, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, players-and-sessions, server-tick, tickets-and-loading
ServerHandshakePacketListenerImplthreads, protocol-phases, the-connection
ServerItemCooldownsusing-an-item
ServerLevelintroduction, biggest, fanin, packages, glossary, level-data-and-rules, math-and-primitives, naming-drift, threads, anatomy, block-breaking, block-entities, block-interaction, blocks-and-states, pistons-and-block-events, signal-and-dust, what-makes-a-sound, entity-selectors, game-tests, the-execution-engine, ai-goals-and-brains, attributes, authority, damage-and-death, entity-anatomy, entity-lifecycle, movement-and-collision, pathfinding, synched-entity-data, identifiers-and-registries, contexts-and-predicates, enchantments, items-and-stacks, loot-tables, recipes, using-an-item, what-the-client-is-told, player-anatomy, the-spear, the-sword-swing, the-two-phase-tick, particles, how-a-server-dies, players-and-sessions, server-level-tick, server-tick, starting-a-server, chunk-anatomy, chunk-generation-pipeline, chunk-storage, environment-attributes-and-timelines, fluids, game-events-and-vibrations, points-of-interest, scheduled-ticks, tickets-and-loading, features-and-placement, jigsaw-and-templates, structure-placement
ServerLevelAccessorentity-anatomy, entity-lifecycle
ServerLevelDatalevel-data-and-rules, starting-a-server
ServerLinksstarting-a-server
ServerLinksDialogdialogs
ServerLinksDialogScreendialogs
ServerListthe-connection
ServerLoginPacketListenerImplthreads, anatomy, protocol-phases, the-connection, players-and-sessions
ServerNameResolverthe-connection
ServerOpListplayers-and-sessions
ServerOpListEntrynaming-drift, permissions, players-and-sessions
ServerPackCommandbrigadier-and-commands, resource-system
ServerPacketListenerpackets-and-stream-codecs, the-connection, server-tick
ServerPackManagerresource-system
ServerPacksSourceresource-system, starting-a-server
ServerPlaceReciperecipes
ServerPlayerbiggest, packages, glossary, naming-drift, anatomy, block-breaking, block-entities, block-interaction, debugging-the-running-game, options, advancements, brigadier-and-commands, dialogs, entity-selectors, permissions, scoreboard-and-data, attributes, authority, damage-and-death, entity-lifecycle, synched-entity-data, codecs-nbt-json, text-components, containers-and-menus, enchanting, enchantments, items-and-stacks, loot-tables, recipes, using-an-item, IX · Networking, chat-and-signing, protocol-phases, what-the-client-is-told, VIII · The player, hunger-and-experience, input-to-movement, player-anatomy, status-effects, the-spear, the-sword-swing, the-two-phase-tick, post-processing, how-a-server-dies, players-and-sessions, server-level-tick, server-tick, chunk-storage, tickets-and-loading
ServerPlayerGameModeblock-breaking, block-interaction, prediction-and-acks, items-and-stacks, loot-tables, using-an-item, VIII · The player, player-anatomy, the-two-phase-tick, particles
ServerRecipeBookwhat-this-book-skips, recipes, player-anatomy
ServerReconfigScreenthe-connection
ServerRedirectHandlerthe-connection
ServerResourcePackConfigurationTaskresource-system, protocol-phases
ServerScoreboardlevel-data-and-rules, scoreboard-and-data
ServerSelectionListthe-connection
ServerStatsCounterwhat-this-book-skips, how-a-server-dies, players-and-sessions
ServerStatusserver-tick, starting-a-server
ServerStatusPacketListenerImplprotocol-phases, the-connection
ServerStatusPingerentity-selectors, text-components, protocol-phases, the-connection
ServerTextFilterchat-and-signing, starting-a-server
ServerTickRateManageranatomy, server-tick, scheduled-ticks
ServerWatchdogglossary, threads, the-window, how-a-server-dies
ServerWaypointManagerlevel-data-and-rules, scoreboard-and-data, attributes
Servicesstarting-a-server
SetAttributesFunctionattributes
SetContainerContentsloot-tables
SetContainerLootTableloot-tables
SetEnchantmentsFunctionenchanting
SetHiddenStateai-goals-and-brains
SetItemCountFunctionnaming-drift, data-driven-types
SetItemFunctionloot-tables
SetOnceOptionStateentity-selectors
SetRandomDyesFunctionloot-tables
Settingsstarting-a-server
SetValueenchantments
SetWalkTargetFromBlockMemorypoints-of-interest
ShaderDefinesblaze3d
ShaderManagernaming-drift, resource-system, blaze3d, post-processing
ShaderSourceblaze3d
ShadowFeatureRenderersubmit-phases
ShapedRecipenaming-drift, recipes
ShapedRecipePatternrecipes
ShapelessReciperecipes
ShapeOutlineFeatureRenderersubmit-phases
Shapesmath-and-primitives, movement-and-collision, pathfinding, fluids
SharedConstantsthreads, anatomy, block-breaking, debugging-the-running-game, what-makes-a-sound, brigadier-and-commands, entity-lifecycle, chat-and-signing, the-connection, models-and-atlases, starting-a-server, blending, creating-a-world, jigsaw-and-templates, terrain
SharedSuggestionProviderbrigadier-and-commands
Shearableentity-anatomy, synched-entity-data
ShearsItemblock-breaking
SheepVI · Entities, synched-entity-data, data-components
SheepRenderersynched-entity-data
SheepRenderStatesynched-entity-data
SheepWoolLayersynched-entity-data
SheepWoolUndercoatLayersynched-entity-data
Sheetsmodels-and-atlases
ShelfMushroomDecoratortrees
ShelfRenderStateblock-entity-rendering
ShieldDecorationReciperecipes
ShieldItemdamage-and-death
ShiftNoiseFunctiondensity-function-nodes
ShipwreckPieceshand-built-structures
ShipwreckStructurehand-built-structures
Shulkerthe-client-level, movement-and-collision
ShulkerBoxBlockloot-tables
ShulkerBoxBlockEntityblock-entities, pistons-and-block-events
ShulkerBoxMenucontainers-and-menus
ShulkerBoxSpecialRendererblock-entity-rendering
ShulkerBulletnon-living-damage, entity-lifecycle
SignableCommandbrigadier-and-commands, permissions, chat-and-signing
SignalGetterdiodes-and-observers, pistons-and-block-events, signal-and-dust
SignBlockEntityblock-entities, dialogs, permissions
SignedArgumentbrigadier-and-commands, chat-and-signing
SignedMessageBodychat-and-signing
SignedMessageChainnaming-drift, chat-and-signing
SignedMessageLinknaming-drift, chat-and-signing
SignedMessageValidatorchat-and-signing
SignRenderStateblock-entity-rendering
SignTextblock-entity-rendering
SilentInitExceptionI · Anatomy, the-client-loop
SimpleBitStoragechunk-anatomy
SimpleBlockFeaturenaming-drift
SimpleContainercontainers-and-menus
SimpleCriterionTriggeradvancements, contexts-and-predicates
SimpleDensityFunctiondensity-function-nodes, naming-drift, blending, density-functions
SimpleDialogdialogs
SimpleDialogScreendialogs
SimpleFeatureRenderPhasesubmit-phases, entity-rendering
SimpleJsonResourceReloadListenerresource-system
SimpleModelWrappermodels-and-atlases
SimplePreparableReloadListenerresource-system
SimpleRandomSelectorFeaturefeatures-and-placement
SimpleRegionStoragehow-a-server-dies, chunk-storage, blending
SimpleReloadInstanceresource-system
SimpleSoundInstancesound-engine
SimpleStateProviderdata-driven-types
SimpleUnboundProtocolpackets-and-stream-codecs
SimpleWaterloggedBlockfluids
SimplexNoisedensity-functions
SimulationChunkTrackertickets-and-loading
SingleEnchantmentenchanting
SingleEntryContainerBaseloot-tables
SingleFilemodels-and-atlases
SingleItemReciperecipes
SingleOptionInputdialogs
SinglePieceStructurehand-built-structures
SinglePoolElementjigsaw-and-templates
SingleQuadParticlenaming-drift, submit-phases, particles
SingleRecipeInputrecipes
SingleSpriteSourcetext-and-fonts
SingleThreadedRandomSourcemath-and-primitives
SingleTickProfilerthe-client-loop, server-tick
SingletonArgumentInfobrigadier-and-commands
SingleValuePalettechunk-anatomy
SingleVariantmodels-and-atlases
SkinManagerentity-rendering
SkinTextureDownloaderentity-rendering, the-window
SkipFieldscodecs-nbt-json
SkipPacketDecoderExceptionpackets-and-stream-codecs, the-connection
SkipPacketEncoderExceptionpackets-and-stream-codecs, the-connection
SkipPacketExceptionpackets-and-stream-codecs, the-connection
SkullBlockRendererblock-entity-rendering
SkyLightEnginelighting
SkyLightSectionStoragelighting
SkyRendererlightmap-fog-and-sky, environment-attributes-and-timelines
SkyRenderStatelightmap-fog-and-sky
SleepInBedai-goals-and-brains, points-of-interest
SleepStatusserver-level-tick
SliceFunctiondensity-function-nodes, density-functions
SliceShapemath-and-primitives
Slotcontainers-and-menus
SlotCollectioncontexts-and-predicates
SlotDisplaydata-driven-types, recipes, packets-and-stream-codecs
SlotDisplayContextcontexts-and-predicates, recipes
SlotLootdata-driven-types, contexts-and-predicates, loot-tables
SlotProviderentity-anatomy
SlotRangescontainers-and-menus
SlotSourcedata-driven-types, contexts-and-predicates
SlotSourceArgumentcontexts-and-predicates
SlotSourcesdata-driven-types
SmearedPerlinNoisedensity-functions
SmeltItemFunctionenchantments, loot-tables
SmithingMenurecipes
SmithingRecipeInputrecipes
SmithingTransformReciperecipes
SmithingTrimReciperecipes
SmokerMenurecipes
SmoothSwimmingMoveControlpathfinding
SnbtDatafixeranatomy
SnbtGrammarnaming-drift, brigadier-and-commands, codecs-nbt-json
SnbtOperationsbrigadier-and-commands, codecs-nbt-json
SnowGolemsynched-entity-data
Soundsound-engine, what-makes-a-sound
SoundBuffersound-engine
SoundBufferLibrarysound-engine
SoundEnginethreads, sound-engine, what-makes-a-sound
SoundEngineExecutorthreads, sound-engine
SoundEventfanin, what-makes-a-sound
SoundEventListenersound-engine
SoundEventRegistrationwhat-makes-a-sound
SoundEventRegistrationSerializerwhat-makes-a-sound
SoundEventsbiggest, fanin, non-living-damage, what-makes-a-sound, synched-entity-data
SoundInstancesound-engine
SoundManagersound-engine, the-client-loop, what-makes-a-sound, resource-system
SoundPreviewHandlerwhat-makes-a-sound
SoundSourcesound-engine, what-makes-a-sound
SoundTypewhat-makes-a-sound
SourceFiltermodels-and-atlases
SpaceProvidertext-and-fonts
SpacerElementgui-and-screens
SpatialAttributeInterpolatorenvironment-attributes-and-timelines
SpatialLongSetlighting
SpawnArmorTrimsCommandbrigadier-and-commands
SpawnConditiondata-driven-types
SpawnConditionsentity-lifecycle
SpawnContextentity-lifecycle
SpawnDatablock-entities
SpawnEggItementity-lifecycle, VII · Items and inventories
SpawnerBlockEntityblock-entities, pistons-and-block-events, entity-lifecycle
SpawnerRenderStateblock-entity-rendering
SpawnPlacementsglossary, entity-lifecycle
SpawnPlacementTypenaming-drift
SpawnPlacementTypesnaming-drift
SpawnPrioritySelectorsentity-lifecycle, data-driven-types
SpawnTargetPointbiomes
SpearAnimationsthe-spear
SpearApproachthe-spear
SpearAttackthe-spear
SpearRetreatthe-spear
SpearUseGoalai-goals-and-brains, the-spear
SpecialBlockModelWrapperblock-entity-rendering
SpecialDatesblock-entity-rendering
SpecialGlyphstext-and-fonts
SpecialModelRendererblock-entity-rendering
SpecialModelRenderersblock-entity-rendering, models-and-atlases
SpecialModelWrapperblock-entity-rendering, models-and-atlases
SpectatorGuihud-elements
SpeleothemBlockscheduled-ticks
SpeleothemClusterFeaturenaming-drift
SpeleothemFeaturenaming-drift
Spiderpost-processing
SPIRVModulewhat-this-book-skips
SplashManagerresource-system
SplineFunctiondensity-function-nodes
SpreadPlayersCommandXIII · Commands and data packs
SpriteContentsmodels-and-atlases
SpriteIdmodels-and-atlases
SpriteLoadermodels-and-atlases
SpriteSetparticles
SpriteSourceListmodels-and-atlases
SpriteSourcesmodels-and-atlases
SpruceFoliagePlacertrees
SpvModuleblaze3d
SpyglassItemusing-an-item
StackedContentsrecipes, player-anatomy
StackedItemContentsrecipes, player-anatomy
StagedVertexBuffersubmit-phases, the-frame
StagingBufferblaze3d, section-meshing
StairBlockblocks-and-states
StairsShapeblocks-and-states
Statglossary, scoreboard-and-data
StateDefinitionglossary, blocks-and-states, fluids
StateHolderV · Blocks, block-interaction, blocks-and-states, fluids
StatePropertiesPredicateadvancements
StaticActiondialogs
StaticCache2Dchunk-generation-pipeline
Statswhat-this-book-skips, block-breaking, block-interaction, damage-and-death, enchanting, items-and-stacks, recipes, using-an-item
StatTypescoreboard-and-data, data-driven-types
StatusProtocolspackets-and-stream-codecs, protocol-phases
Std140Builderblaze3d, post-processing
Std140SizeCalculatorblaze3d
SteppedInterpolationTrackermovement-and-collision, synched-entity-data, what-the-client-is-told
Stitchermodels-and-atlases
StitcherExceptionmodels-and-atlases
StonecutterMenurecipes
StonecutterReciperecipes
StopCommandhow-a-server-dies
Stopwatcheslevel-data-and-rules, starting-a-server
StorageDataAccessorscoreboard-and-data
StorageDataSourcetext-components
StoredUserEntryplayers-and-sessions
StoredUserListplayers-and-sessions
StraightTrunkPlacertrees
Strategynaming-drift, chunk-anatomy
StreamCodecfanin, glossary, naming-drift, synched-entity-data, codecs-nbt-json, data-driven-types, recipes, packets-and-stream-codecs
StreamDecoderpackets-and-stream-codecs
StreamEncoderpackets-and-stream-codecs
StreamMemberEncoderpackets-and-stream-codecs
StreamTagVisitorcodecs-nbt-json
StrictJsonParsercodecs-nbt-json
Striderpathfinding
StringArgumentSerializerbrigadier-and-commands
StringDecomposertext-and-fonts, text-components
StringReaderadvancements, brigadier-and-commands, identifiers-and-registries
StringRepresentableblocks-and-states
StringSplittertext-and-fonts
StringTagcodecs-nbt-json
StringTemplatedialogs, functions-and-macros
StringUtilchat-and-signing
StrollAroundPoiai-goals-and-brains
StrollToPoiai-goals-and-brains
StrollToPoiListai-goals-and-brains
StrongholdPiecesbiggest, hand-built-structures
StrongholdStructurehand-built-structures
Structureglossary, naming-drift, data-driven-types, hand-built-structures, jigsaw-and-templates, structure-placement
StructureAccesschunk-anatomy
StructureBlockEntityjigsaw-and-templates
StructureCheckentity-lifecycle, codecs-nbt-json, chunk-generation-pipeline, chunk-storage, XII · World generation, density-functions, hand-built-structures, structure-placement
StructureCheckResultstructure-placement
StructureGridSpawnergame-tests
StructureManagerstructure-placement
StructurePieceloot-tables, hand-built-structures, jigsaw-and-templates, structure-placement
StructurePiecesBuilderhand-built-structures
StructurePieceSerializationContextdata-driven-types
StructurePieceTypeglossary, data-driven-types, hand-built-structures
StructurePlacementdata-driven-types, structure-placement
StructurePlaceSettingsjigsaw-and-templates
StructurePoolElementdata-driven-types, jigsaw-and-templates
StructurePoolElementTypedata-driven-types
StructureProcessordata-driven-types, hand-built-structures, jigsaw-and-templates
StructureProcessorListjigsaw-and-templates
StructureSetstructure-placement
StructureSpawnOverrideentity-lifecycle
StructureStartwhat-the-client-is-told, hand-built-structures, jigsaw-and-templates, structure-placement
StructureTagstags
StructureTemplateblock-update-flags, hand-built-structures, jigsaw-and-templates, trees
StructureTemplateManagerwhat-this-book-skips, resource-system, starting-a-server, jigsaw-and-templates
StructureTemplatePooljigsaw-and-templates
StructureTypedata-driven-types, structure-placement
StructureUtilsgame-tests, scheduled-ticks
Stylenaming-drift, text-and-fonts, text-components
StyleArgumentbrigadier-and-commands
SubmitNodeCollectionsubmit-phases, text-and-fonts, entity-rendering, post-processing
SubmitNodeCollectornaming-drift, submit-phases, block-entity-rendering, entity-rendering, the-frame
SubmitNodeStorageglossary, naming-drift, submit-phases, entity-rendering, the-frame
SubStringSourcetext-and-fonts
SubtitleOverlayhud-elements, sound-engine
SuggestionProvidersbrigadier-and-commands
SuggestionSupplierbrigadier-and-commands
SulfurCubesynched-entity-data
SulfurCubeInnerLayerentity-rendering
SummonCommandentity-anatomy, entity-lifecycle
SummonEntityEffectenchantments
SuppressedExceptionCollectorhow-a-server-dies, server-tick
SurfaceRelativeThresholdFilterfeatures-and-placement
SurfaceWaterDepthFilterfeatures-and-placement
SuspiciousStewEffectshunger-and-experience, status-effects
SwampHutPiecehand-built-structures
SweetBerryBushBlockauthority
SwimNodeEvaluatorpathfinding
SwingAnimationdata-components, the-sword-swing
SwingAnimationTypethe-spear
SwizzleArgumentbrigadier-and-commands
SyncedDataHolderentity-anatomy, synched-entity-data
SynchedEntityDataentity-anatomy, synched-entity-data, enchantments
SynchronizeRegistriesTaskidentifiers-and-registries, tags, protocol-phases, players-and-sessions
SystemToasthud
TabOrderedElementgui-and-screens
Tagglossary, II · Foundations, codecs-nbt-json, text-components
TagBuildertags
TagEntrytags, loot-tables
TagFiletags
TagKeyidentifiers-and-registries, tags
TagLoaderidentifiers-and-registries, resource-system, tags, starting-a-server
TagNetworkSerializationtags
TagParserbrigadier-and-commands, codecs-nbt-json
TagsProvidertags
TagTypecodecs-nbt-json
TagValueInputcodecs-nbt-json
TagValueOutputcodecs-nbt-json
TamableAnimalentity-anatomy
Targetpathfinding
TargetBlockscheduled-ticks
TargetedConditionalEffectenchantments
Targetingentity-anatomy
TargetingConditionsai-goals-and-brains
TaskSchedulertickets-and-loading
Teamscoreboard-and-data, text-components
TeamColornaming-drift, scoreboard-and-data
TeamColorArgumentnaming-drift
TeamCommandbrigadier-and-commands, scoreboard-and-data
TeamMsgCommandentity-selectors
TelemetryEventTypewhat-this-book-skips
TelemetryPropertywhat-this-book-skips
TeleportCommandXIII · Commands and data packs
TeleportRandomlyConsumeEffectusing-an-item
TeleportTransitionplayers-and-sessions
TemplatePathFactoryjigsaw-and-templates
TemplateSourcejigsaw-and-templates
TemplateStructurePiecehand-built-structures
TemptGoalpathfinding
TemptingSensorai-goals-and-brains
Termbrigadier-and-commands
TerrainAdjustmentstructure-placement, terrain
TerrainParticlemodels-and-atlases, particles
TerrainProviderwhat-this-book-skips
TestBlockgame-tests
TestBlockEditScreengame-tests
TestBlockModegame-tests
TestCommandgame-tests
TestDatanaming-drift, game-tests
TestEnvironmentDefinitionnaming-drift, functions-and-macros, game-tests, data-driven-types, environment-attributes-and-timelines
TestFindernaming-drift, game-tests
TestFunctionLoadernaming-drift
TestInstanceBlocknaming-drift, game-tests
TestInstanceBlockEditScreengame-tests
TestInstanceBlockEntitynaming-drift, game-tests
TestInstanceRenderergame-tests, block-entity-rendering
TextColornaming-drift, text-components
TextComponentTagVisitortext-components
TextFeatureRenderernaming-drift, submit-phases, text-and-fonts
TextFilterchat-and-signing
TextGizmodebugging-the-running-game
TextInputdialogs
TextRenderabletext-and-fonts
TextureAtlasblaze3d, models-and-atlases
TextureManagerthe-client-loop, resource-system, models-and-atlases, the-window
TextureSlotsmodels-and-atlases
TextureTargetblaze3d, post-processing
TextureTransformblaze3d
TextureUtilthe-window
TheEndBiomeSourcebiomes, creating-a-world
TheEndGatewayBlockEntitypistons-and-block-events
TheEndGatewayRendererblock-entity-rendering
ThreadedLevelLightEnginehow-a-server-dies, chunk-generation-pipeline, chunk-storage, lighting
ThreadingDetectormath-and-primitives, chunk-anatomy
ThreadSafeLegacyRandomSourcemath-and-primitives
ThreeLayersFeatureSizetrees
ThrottlingChunkTaskDispatcherchunk-generation-pipeline, tickets-and-loading
ThrownTridentenchantments
TickablePacketListenerthreads, anatomy, the-connection, server-tick
TickableSoundInstancesound-engine
TickableTexturemodels-and-atlases
TickAccessscheduled-ticks
TickCommandserver-tick
TickContainerAccessscheduled-ticks
Tickettickets-and-loading
TicketStorageglossary, level-data-and-rules, naming-drift, what-the-client-is-told, how-a-server-dies, server-level-tick, starting-a-server, tickets-and-loading
TicketTypeglossary, how-a-server-dies, players-and-sessions, starting-a-server, tickets-and-loading
TickingBlockEntityblock-entities
TickPrioritydiodes-and-observers, scheduled-ticks
TickRateManagerthreads, anatomy, block-entities, the-client-loop, entity-lifecycle, input-to-movement, block-entity-rendering, the-frame, server-level-tick, server-tick, scheduled-ticks
TickTaskserver-tick
TickThrottlerchat-and-signing, input-to-movement
TiledBlitRenderStatethe-gui-render-tree
TimeCommandenvironment-attributes-and-timelines
Timelinenaming-drift, VI · Entities, ai-goals-and-brains, identifiers-and-registries, environment-attributes-and-timelines
Timelinesnaming-drift, ai-goals-and-brains, lightmap-fog-and-sky, environment-attributes-and-timelines, points-of-interest
TimelineTagstags, environment-attributes-and-timelines
TimerQuerythe-frame
TimerQueuelevel-data-and-rules, naming-drift, functions-and-macros, server-level-tick, server-tick
TitleScreengui-and-screens
TlsfAllocatorsection-meshing
Toasthud
ToastManagerhud-elements, hud, what-makes-a-sound
ToggleKeyMappinginput-and-keybinds
Toolblock-breaking, data-components, items-and-stacks
ToolMaterialblock-breaking, the-spear
Tooltipgui-and-screens
TooltipDisplaydata-components
TpsDebugCharthud
TpsDebugDimensionsserver-tick
TraceableEntityentity-anatomy
TraceCallbacksthe-execution-engine
TrackedWaypointpackets-and-stream-codecs
TrackingDebugSynchronizerdebugging-the-running-game
TrackingEmitterparticles
TracyGpuProfilerwhat-this-book-skips
TracyZoneFillerwhat-this-book-skips
TradeSetcontexts-and-predicates
Transformationmath-and-primitives
TransientBlockserver-level-tick
TransientBlockAllocatorblaze3d
TransientCraftingContainerrecipes
TransientEntitySectionManagerthe-client-level, entity-lifecycle
TransientMemoryblaze3d
TranslatableContentstext-and-fonts, text-components
TranslatableFormatExceptiontext-components
TranslucencyPointOfViewvisibility-and-the-frame-graph
TranslucentFeatureRenderPhasesubmit-phases, entity-rendering
TranslucentSubmitsubmit-phases
TransmuteReciperecipes
Transparencymodels-and-atlases, the-window
TreeDecoratordata-driven-types, trees
TreeDecoratorTypedata-driven-types, trees
TreeFeaturenaming-drift, features-and-placement, trees
TreeFeatureswhat-this-book-skips, trees
TreeGrowernaming-drift, trees
TreeNodePositionadvancements
TrialSpawnerblock-entities, entity-lifecycle, contexts-and-predicates
TrialSpawnerBlockEntityblock-entities
TrialSpawnerStateblock-entities
TrialSpawnerStateDatablock-entities, contexts-and-predicates
TridentItemdata-components, enchantments, using-an-item
TridentSpecialRendererblock-entity-rendering
TriggerCommandbrigadier-and-commands, scoreboard-and-data
TrimMaterialPropertymodels-and-atlases
TripWireBlockscheduled-ticks
TripWireHookBlocksignal-and-dust
TrueTypeGlyphProviderDefinitiontext-and-fonts
TrunkPlacerdata-driven-types, tags, trees
TrunkPlacerTypedata-driven-types, trees
TrunkVineDecoratortrees
TryFindLiquidGoalnaming-drift, pathfinding
Tutorialthe-client-loop
TutorialToasthud
TwoLayersFeatureSizetrees
TypedDataComponentcodecs-nbt-json, data-components
TypedEntityDatacodecs-nbt-json, data-components
TypedInstanceentity-anatomy, tags, items-and-stacks
UberGpuBuffernaming-drift, blaze3d, section-meshing
UiLightmaplightmap-fog-and-sky
UnaryFunctiondensity-function-nodes, naming-drift, density-functions
UnbakedCuboidGeometrymodels-and-atlases
UnbakedGeometrymodels-and-atlases
UnbakedGlyphnaming-drift, text-and-fonts
UnboundEntryActionfunctions-and-macros
UnboundProtocolpackets-and-stream-codecs
UnconfiguredPipelineHandlerthe-connection
UndeadRenderStateentity-rendering
UnderwaterAmbientSoundHandlerwhat-makes-a-sound
UniformContainerBasenaming-drift, loot-tables
UniformGeneratorcontexts-and-predicates
UniformValuepost-processing
UnihexProvidertext-and-fonts
Unitmath-and-primitives, codecs-nbt-json, enchantments
Unstitchermodels-and-atlases, the-window
UpdateActivityFromScheduleai-goals-and-brains, points-of-interest
UpdateIntervalentity-anatomy, synched-entity-data
UpgradeDatachunk-anatomy
UpgradeProgresschunk-storage
UpwardsBranchingTrunkPlacertrees
UseBonemealai-goals-and-brains
UseCooldownusing-an-item
UseDurationusing-an-item, models-and-atlases
UseEffectsusing-an-item, hunger-and-experience, the-spear
UseOnContextblocks-and-states
UserBanListplayers-and-sessions
UseRemainderitems-and-stacks
UserNameToIdResolverplayers-and-sessions
UserWhiteListplayers-and-sessions
Utf8Stringpackets-and-stream-codecs
Utilfanin, packages, level-data-and-rules, math-and-primitives, threads, anatomy, sound-engine, the-client-loop, entity-selectors, identifiers-and-registries, resource-system, tags, recipes, chat-and-signing, visibility-and-the-frame-graph, how-a-server-dies, starting-a-server, chunk-storage, scheduled-ticks
UUIDUtilpackets-and-stream-codecs, protocol-phases
UvMappingsubmit-phases
Validatablecontexts-and-predicates, enchantments
ValidateNearbyPoiai-goals-and-brains, points-of-interest
ValidationContextcontexts-and-predicates
ValidationContextSourcecontexts-and-predicates
ValueInputblock-entities, codecs-nbt-json
ValueInputContextHelpercodecs-nbt-json
ValueOutputnaming-drift, codecs-nbt-json
VanillaEnchantmentProvidersenchanting
VanillaPackResourcesnaming-drift, resource-system
VanillaRegistrieswhat-this-book-skips, identifiers-and-registries
VariantSelectormodels-and-atlases
VariantUtilsentity-lifecycle
VarIntpackets-and-stream-codecs, the-connection
Varint21FrameDecoderpackets-and-stream-codecs, the-connection
Varint21LengthFieldPrependerthe-connection
VarLongpackets-and-stream-codecs
VaultBlockEntitycontexts-and-predicates
VaultRenderStateblock-entity-rendering
Vec2math-and-primitives, input-to-movement
Vec3fanin, math-and-primitives, contexts-and-predicates, packets-and-stream-codecs
Vec3Argumentglossary, brigadier-and-commands
Vec3imath-and-primitives
VecDeltaCodecwhat-the-client-is-told
VegetationFeatureswhat-this-book-skips
VehicleEntityhierarchy, non-living-damage, damage-and-death, entity-anatomy
VersionCommandpermissions
VertexConsumerlightmap-fog-and-sky
VertexFormatblaze3d
VertexFormatElementblaze3d
VerticalAnchorfeatures-and-placement
Vexpathfinding
VibrationInfogame-events-and-vibrations
VibrationParticleOptiondata-driven-types, game-events-and-vibrations
VibrationSelectorgame-events-and-vibrations
VibrationSystemgame-events-and-vibrations
VideoModenaming-drift, the-window
VideoSettingsScreenthe-window
ViewAreavisibility-and-the-frame-graph
Villagerai-goals-and-brains, entity-lifecycle, pathfinding, data-components, environment-attributes-and-timelines, points-of-interest
VillagerCalmDownai-goals-and-brains
VillagerGoalPackagesai-goals-and-brains, points-of-interest
VillagerMakeLovepoints-of-interest
VillagerProfessionai-goals-and-brains
VillagerTradecontexts-and-predicates, enchanting, enchantments
VillagerTradesVII · Items and inventories
VillageSiegeentity-lifecycle, server-level-tick, starting-a-server, environment-attributes-and-timelines, points-of-interest
VisGraphvisibility-and-the-frame-graph
Visibilityentity-lifecycle, server-level-tick, tickets-and-loading
VisibilitySetvisibility-and-the-frame-graph
VoxelShapemath-and-primitives, submit-phases, VI · Entities, movement-and-collision, jigsaw-and-templates
VulkanBackendanatomy, blaze3d, the-window
VulkanCommandEncoderblaze3d
VulkanGpuSurfaceblaze3d
VulkanRenderPassblaze3d
VulkanTransientMemoryblaze3d
WaitingForResponseScreendialogs
WakeUppoints-of-interest
WalkNodeEvaluatorpathfinding
WallAndGroundTransformationsblock-entity-rendering
WallClimberNavigationpathfinding
WanderingTraderDatalevel-data-and-rules
WanderingTraderSpawnerentity-lifecycle, starting-a-server, points-of-interest
Wardenai-goals-and-brains, damage-and-death, game-events-and-vibrations
WardenAiai-goals-and-brains, game-events-and-vibrations
WardenSpawnTrackerCommandbrigadier-and-commands
WaterAvoidingRandomStrollGoalai-goals-and-brains
WaterBoundPathNavigationpathfinding
WaterFluidfluids
WaterFogEnvironmentlightmap-fog-and-sky
WaterloggedTransparentBlockfluids
WaypointStyleManagerhud, resource-system
Weapondamage-and-death, data-components, the-sword-swing
WeatherAttributesenvironment-attributes-and-timelines
WeatherCommandXIII · Commands and data packs
WeatherDatalevel-data-and-rules, naming-drift, server-level-tick, starting-a-server
WeatherEffectRendererlightmap-fog-and-sky
WeatherRenderStatelightmap-fog-and-sky
WeighedSoundEventssound-engine, what-makes-a-sound
WeightedListparticles
WeightedRandomSelectorFeaturefeatures-and-placement
WeightedVariantsmodels-and-atlases
WidgetTooltipHoldergui-and-screens
WindChargenon-living-damage, the-sword-swing
Windowglossary, naming-drift, anatomy, the-client-loop, the-frame, the-window, how-a-server-dies
WindowEventHandlerthe-window
WingsLayerentity-rendering
WinScreengui-and-screens
WitherBossscoreboard-and-data, entity-lifecycle
WitherMobEffectstatus-effects
Wolfdamage-and-death, entity-lifecycle, data-components
WoodlandMansionPieceshand-built-structures
WoodlandMansionStructurehand-built-structures
WoodTypeblocks-and-states
WorkAtComposterai-goals-and-brains
WorkAtPoiai-goals-and-brains
WorldBorderlevel-data-and-rules, server-level-tick
WorldBorderCommandXIII · Commands and data packs
WorldBorderRendererlightmap-fog-and-sky
WorldCarvernaming-drift, data-driven-types, blending, terrain
WorldCarverTypesterrain
WorldClocklevel-data-and-rules, naming-drift, server-level-tick, environment-attributes-and-timelines
WorldClockslevel-data-and-rules, environment-attributes-and-timelines
WorldCoordinatebrigadier-and-commands
WorldCoordinatesbrigadier-and-commands
WorldCreationContextcreating-a-world
WorldCreationGameRulesScreenlevel-data-and-rules, creating-a-world
WorldCreationUiStatelevel-data-and-rules, creating-a-world
WorldDatalevel-data-and-rules, starting-a-server
WorldDataConfigurationlevel-data-and-rules, identifiers-and-registries, resource-system
WorldDimensionslevel-data-and-rules, creating-a-world
WorldGenContextchunk-generation-pipeline, chunk-storage
WorldGenerationContextfeatures-and-placement, terrain
WorldGenLevelfeatures-and-placement, trees
WorldgenRandommath-and-primitives, features-and-placement, structure-placement
WorldGenRegionblock-update-flags, blocks-and-states, entity-lifecycle, chunk-generation-pipeline, environment-attributes-and-timelines, points-of-interest, scheduled-ticks, blending, features-and-placement
WorldGenSettingsglossary, level-data-and-rules, naming-drift, starting-a-server, creating-a-world
WorldGenTickAccessscheduled-ticks
WorldLoaderglossary, threads, data-driven-types, identifiers-and-registries, resource-system, tags, starting-a-server, XII · World generation, creating-a-world
WorldOpenFlowscreating-a-world, features-and-placement
WorldOptionsglossary, level-data-and-rules, chunk-generation-pipeline, creating-a-world
WorldOptionsScreenanatomy, permissions
WorldPresetglossary, creating-a-world
WorldPresetscreating-a-world
WorldSelectionListcreating-a-world
WorldSessionTelemetryManageradvancements
WorldStemglossary, starting-a-server, creating-a-world
WorldUpgraderstarting-a-server, chunk-storage
WrappedGoalglossary, ai-goals-and-brains
WritableLevelDatalevel-data-and-rules
WritableRegistryidentifiers-and-registries
WrittenBookContenttext-components
XoroshiroRandomSourcemath-and-primitives, density-functions
YieldJobSiteai-goals-and-brains, points-of-interest
ZeroBitStoragechunk-anatomy
Zoglinai-goals-and-brains
Zombieai-goals-and-brains, attributes, authority, entity-lifecycle
ZombieAttackGoalai-goals-and-brains
ZombieModelentity-rendering
ZombieNautilusentity-lifecycle
ZombieRendererentity-rendering
ZombieRenderStateentity-rendering
ZombifiedPiglinpathfinding

The lecture map

The video series is an ordering over the system pages, which are the stable content. Each part’s landing page lists its lectures in watching order and says in a line what each one is; this page assembles the thirteen orders and states the dependencies between them — where a later part is assumed by an earlier one, and what to watch first because of it. It was drafted one part per session, and the owner confirms it — reading each part with the decompile open — before anything is recorded. Nothing in Reference is watched, and nothing in the maps is: the maps are looked at once, the reference pages are looked up, and a lecture links into them wherever a viewer would pause the video to read a table.

Because the subject here is the order, nothing below describes a lecture. What each one is about is said once, on its part’s landing page; what is here is what the order needs — how each part is ordered inside itself, the lectures that must be watched together or in a fixed sequence, and the pages worth taking out of turn. There is exactly one departure from the sidebar order, environment attributes and timelines before Part III’s second lecture; and there are three pages a viewer coming for one part alone has to fetch from elsewhere in the book, which the end of this page names.

The one rule that holds everywhere: each part assumes only the parts before it, and where that is not true the section says so and names the page to watch early. The dependencies between parts, at the end, draws the whole graph.

I · Anatomy

  1. Anatomy — watched first, because every later diagram’s lanes assume it.
  2. What this book skips — second and not last, because a boundary is worth drawing before you invest in thirteen parts rather than after.

II · Foundations

Watch 1 and 2 in that order and before everything else in the part. The five below them lean on those two far more than on each other, so 3 to 7 have little order among themselves: tags come after the resource system, whose stack they are read from, and 7 is the pattern the other six exist to make possible and reads best last.

  1. Codecs, NBT and JSON
  2. Identifiers and registries
  3. The resource system
  4. Tags
  5. Data components
  6. Text components
  7. The data-driven type pattern

III · The server

Part III is a line into a loop and out again: the loop first, because it is what the rest of the book lives inside, and the beginning and the end last, because they are only interesting once you know what they start and stop.

  1. The server tick
  2. The level tick — watched immediately after the first, and never apart from it.
  3. Players and sessions
  4. Starting a server
  5. How a server dies

Watch environment attributes and timelines (Part IV) before the level tick if you want its opening to mean anything. It costs nothing to take out of order — it depends on nothing but registries and codecs — and Part III is the earliest of the six other parts that lean on it.

IV · The world

Lectures 2 to 6 are the vocabulary page and the conveyor after it, and must be watched in that order — nothing later in the chain can be watched first. Lecture 1 is off the line on purpose. Of the last four, 7 comes before 8; the other two need only the vocabulary page and can go anywhere after it.

  1. Environment attributes and timelines — first, for the reason Part III already gave: it depends on nothing but registries and codecs, and nine pages in six other parts cite it, though only four of those parts’ landing pages list it (the table at the foot).
  2. Chunk anatomy — the vocabulary page. Everything below assumes it, and so does Part V.
  3. Tickets and loading
  4. The chunk generation pipeline — Part XII is the cargo on this conveyor and cannot be watched before it.
  5. Lighting — inside the chain, but nothing later in this part needs it first (chunk storage points at it only for the layers an unload drops); one page each in Parts IX and X cites it, and neither part’s landing page lists it.
  6. Chunk storage
  7. Scheduled ticks — two of Part V’s redstone lectures assume this one, and so does the next.
  8. Fluids
  9. Game events and vibrations
  10. Points of interest — Part VI’s AI lecture reads this index, so watch this one first if you are taking that part next.

V · Blocks

Part V is a hub and six spokes, and the hub is watched first because the other six all reach back into the same pair of figures in it: what Level.setBlock and LevelChunk.setBlockState do with a write. Those figures are also where the two update channels are defined, which is the vocabulary every later lecture in the part uses. Two of the six are one lecture in two halves.

  1. Blocks and states
  2. Block interaction
  3. Block breaking — the same lecture’s other half. Watch it immediately after, and never apart from it.
  4. Block entities — reaches forward into Parts VI, VII and XI, most of all for the menu a furnace’s progress travels in.
  5. Signal and dust — assumes scheduled ticks only lightly.
  6. Pistons and block events
  7. Diodes and the observer — assumes scheduled ticks properly: a repeater’s delay is an entry in that queue.

Part V’s two click lectures are the applications of prediction and acknowledgement in Part X, and that page’s own scenario needs this part’s vocabulary. The dependency is circular and this book cuts it here.

VI · Entities

Part VI is a ladder, and nothing in it can be reordered without breaking something.

  1. Entity anatomy
  2. Authority — short, and the rung the rest of the ladder leans on hardest.
  3. Entity lifecycle — assumes tickets and loading for what entity-ticking means.
  4. Synched entity data
  5. Attributes — watched immediately after synched entity data, the two being a pair.
  6. Movement and collision — needs authority in front of it, and blocks and states for what a collision shape is.
  7. AI: goals and brains — assumes environment attributes and timelines for the schedule and points of interest for the bed.
  8. Pathfinding — the other half of the same lecture, watchable on its own once goals and brains has run.
  9. Damage and death

Part VI must precede Part VIII, which is the player half of nearly every page here, and it should precede Parts IX and X, both of which lean on authority. It assumes Part III first — two of this part’s surprises are claims about which phase of the tick something ran in — then Part IV for what makes an entity tick at all and Part V for what it collides with.

VII · Items and inventories

Part VII is two tiers. The first three lectures are the vocabulary and are watched in order; the last five are three engines that touch each other only at their boundaries, so they can be watched in any order — though the two pairs below want to stay together, and contexts and predicates leans on no stack at all and could come first.

  1. Items and stacks — assumes data components completely.
  2. Using an item
  3. Containers and menus — needs both of Part III’s tick pages for when a broadcast happens: the packet drain at the top of the tick, the level’s entity phase, and a pickup in the connection phase after the levels.
  4. Recipes
  5. Enchantments
  6. Enchanting — watch it directly after enchantments.
  7. Contexts and predicates — the page in this part that Part XIII’s landing page names.
  8. Loot tables — watch it directly after contexts and predicates, which it is the worked example of.

Part VII assumes Part II for components, codecs and the reload, Part III for where in a tick a packet is drained and a broadcast lands, and Part V for how a chest gets opened. It must precede Part VIII, which is the player’s own inventory and the swing that spends an item’s durability. Contexts and predicates is a prerequisite of Part XIII’s /execute if predicate and of the advancement system, and is the one lecture here a viewer coming for commands should watch out of order.

VIII · The player

Lectures 1 and 2 are the trunk and are watched in that order; the five below them fall into four groups that need nothing from each other. Only one of the four has an internal order — the spear is the sword swing’s sequel — so 3, 6 and 7 can go anywhere after the trunk.

  1. Player anatomy — the vocabulary lecture.
  2. The two-phase tick — watch it immediately after player anatomy.
  3. Input to movement
  4. The sword swing
  5. The spear
  6. Hunger and experience
  7. Status effects

Part VIII assumes Part VI above everything — authority in particular, which is where the whole part’s premise is stated — and Part III for the tick phases the two-phase tick lives between. It assumes Part VII for using an item, which is the machinery both the spear’s charge and the meal run on; the inventory itself is Part VIII’s, and Part VII stops at the slot. Nothing later in the book is needed to watch it, but Parts X, XI and XIII come back to it.

IX · Networking

Part IX is the wire three times, and two things it carries. The first two lectures are one lecture in two halves and should be watched together; the third finishes the wire’s own story by showing the connection changing language, and the last two both sit inside the play language and need neither it nor each other.

  1. The connection
  2. Packets and stream codecs — the second half of the same lecture.
  3. Protocol phases
  4. What the client is told
  5. Chat and signing

Part IX assumes Part III for the tick phases its traffic is timed against, and Part I’s anatomy for the two loops, which is the fact most of what looks like network jitter comes back to. It assumes Part II for codecs and for Component, and Part VI’s authority for the premise under lecture four. It is a prerequisite of Part X, which is the same wire watched from the receiving end.

X · The client

Nothing here hands off to anything except inside the GUI stack, six to nine, and between two and three, which share a ledger; every other page after the first answers “when in the client’s one loop does this happen” and needs only the hub. Watch the hub first and then take the rest in any order that suits, except the three groups noted below.

  1. The client loop
  2. The client level
  3. Prediction and acknowledgement
  4. Input and keybinds
  5. Options
  6. GUI and screens
  7. The GUI render tree
  8. Text and fonts
  9. The HUD
  10. Sound: the engine
  11. What makes a sound happen
  12. Debugging the running game

Six to nine are the part’s one internal pipeline and are watched consecutively, tree before text. Two and three are a pair. Ten and eleven are the two halves of sound, in either order.

Part X assumes Part IX for the wire it is the far end of, and Part I’s anatomy for the two loops. It assumes Part VI’s authority as the premise under lectures two and three — which split its question between them, what the client may simulate and what it may show before it is told — and Part V before lecture three, because Part V’s landing page rules that its two click lectures come first and they are the ledger’s two applications. Lecture six assumes Part VII’s containers and menus, which is the thing behind the screen it opens on, and lecture eight assumes Part II’s text components. Lecture one is a prerequisite of Part XI, whose frame is one span inside the loop’s turn.

XI · Rendering

The part opens on the frame itself rather than on the substrate underneath it — an inversion made for the viewer rather than for the dependencies, like the late place of Part XII’s structure lectures — so the watching order below is not the order a frame runs in.

  1. The frame
  2. The window
  3. Blaze3D
  4. Visibility and the frame graph
  5. Section meshing
  6. Models and atlases
  7. Entity rendering
  8. Block-entity rendering
  9. Lightmap, fog and sky
  10. Particles
  11. Post-processing

Four and five are a pair — two pages that were one, and still one journey seen from its two ends — and so are seven and eight, the second written as the differences from the first. One to three can also be watched one, three, two.

Part XI assumes two pages of Part X: the client loop, which is what says when a frame happens, and the client level, for what the thing being drawn is. Lecture six assumes Part II’s resource system; lecture nine assumes Part IV’s environment attributes and timelines, which owns the system this part only consumes.

XII · World generation

The three structure lectures run first in the game and are watched last here, bar lecture ten: a structure is decided at ChunkStatus.STRUCTURE_STARTS, two statuses before the biomes it will stand in exist, and writes no block until ChunkStatus.FEATURES, one status after the noise fill. Keeping them together at the end costs two of the earlier pages a forward topic and buys a whole arc in one place.

  1. Density functions
  2. Biomes
  3. Terrain
  4. Blending at the old-chunk border
  5. Features and placement
  6. Trees
  7. Structure placement
  8. Jigsaw and templates
  9. Hand-built structures
  10. Creating a world

One to six are the chunk being made, in the order it is made, and each from two on leans on one before it. Seven, eight and nine are one arc, and eight and nine are alternatives to each other rather than a sequence. Ten is the object the other nine read, told last because it is a tree of everything they explain — but it is the one lecture that moves, and a viewer who wants the origin before the machinery can watch it first.

Part XII assumes Part IV’s chunk generation pipeline, and hard: it is the only page that says when any of this runs and what the ten statuses are, and eight of the ten lectures here name one. It also assumes Part IV’s chunk anatomy for what is being written into and Part IV’s environment attributes and timelines for lecture two, where the biome is one layer of a modifier stack; and three Part II lectures — codecs, registries and the data-driven type pattern, whose fifty-six instances this part owns twenty-six of — because worldgen is the most thoroughly data-driven system in the game.

XIII · Commands and data packs

The dependency runs strictly one way: lectures 1 to 3, then 4 and 5, then the last four. Those last four are peers rather than a sequence — watch them in any order, or only the ones you care about — but none of the five before them is optional for any of them.

  1. Brigadier and commands
  2. Permissions
  3. Entity selectors
  4. The execution engine
  5. Functions and macros
  6. Advancements
  7. Scores, teams and stored data
  8. Dialogs
  9. Game tests

Two and four are the pair that most changes how a viewer reads everything else, and two is the one an existing mod author most needs; three sits between them because it needs the atom two defines and hands its fan-out to four. Six, seven, eight and nine each assume one through five and nothing else in this part.

Part XIII assumes Part III’s server tick twice over, because two of its pages turn on where in that order a thing sits. It assumes Part II’s codecs and the data-driven type pattern, of which dialogs and game tests are the two clearest instances; Part IX’s connection for the boundary between the Netty threads and the Server thread, which the command packets and the suggestion request each cross their own way; and Part VII’s contexts and predicates, which advancements need for every trigger that tests a player and selectors for one option.

The dependencies between parts

Every arc below is a before you start entry on a landing page: the part at the tail is one the part at the head assumes. The two dependencies the whole book leans on, whether or not a landing page lists them, keep their boxes and the one arc each along the spine, and have the rest of their arcs left off, because those would reach almost every node — Part I’s anatomy, for the threads every diagram’s lanes are on, and Part II’s codecs and registries, assumed wherever something is written to disk, sent on the wire or looked up by name. Two arcs out of Part II rest on neither — Part X’s on text components and Part XI’s on the resource system — and are left off with the rest. Read a solid arc as watch before: the parts run down the page in the sidebar’s order, and every solid arc runs down with them.

The thirteen parts in watch order; an arc on the right is a part another part assumes, an arc on the left is one of the two places a part assumes a later one Part I · AnatomyI · Anatomy Part II · FoundationsII · Foundations Part III · The serverIII · The server Part IV · The worldIV · The world Part V · BlocksV · Blocks Part VI · EntitiesVI · Entities Part VII · Items and inventoriesVII · Items and inventories Part VIII · The playerVIII · The player Part IX · NetworkingIX · Networking Part X · The clientX · The client Part XI · RenderingXI · Rendering Part XII · World generationXII · World generation Part XIII · Commands and data packsXIII · Commands and data packs I → II: Part II assumes Part I II → III: Part III assumes Part II III → IV: Part IV assumes Part III IV → V: Part V assumes Part IV V → VI: Part VI assumes Part V VII → VIII: Part VIII assumes Part VII IX → X: Part X assumes Part IX X → XI: Part XI assumes Part X III → V: Part V assumes Part III IV → VI: Part VI assumes Part IV V → VII: Part VII assumes Part V VI → VIII: Part VIII assumes Part VI III → VI: Part VI assumes Part III VI → IX: Part IX assumes Part VI VII → X: Part X assumes Part VII III → VII: Part VII assumes Part III VI → X: Part X assumes Part VI IX → XIII: Part XIII assumes Part IX III → VIII: Part VIII assumes Part III V → X: Part X assumes Part V III → IX: Part IX assumes Part III VII → XIII: Part XIII assumes Part VII IV → XI: Part XI assumes Part IV IV → XII: Part XII assumes Part IV III → XIII: Part XIII assumes Part III IV ⇢ III: Part III assumes Tickets and loading, Environment attributes and timelines, cut on purpose Tickets and loading Environment attributes and timelines X ⇢ V: Part V assumes Prediction and acknowledgement, cut on purpose Prediction and acknowledgement

The thirteen parts top to bottom in watch order; an arc on the right runs from a part to one that assumes it, always downward, and the two dashed arcs on the left are the only places a part assumes a later one, each labelled with the pages it needs.

The graph is a line with two knots in it, and the sidebar order is a valid walk through it: no solid arc points at an earlier part. The two dashed arcs are the places where it does not hold, and each is cut on purpose rather than solved by reordering.

Part III assumes two pages of Part IV. The level tick uses entity-ticking and block-ticking range, which tickets and loading owns, and its first step throws away a cache belonging to environment attributes and timelines. The first is cut by definition — the level tick defines both ranges before it uses them — and the second by order: the environment page depends on nothing but registries and codecs, so it is the first page of Part IV in the sidebar, in that part’s watch order and in the list above, and it is the one lecture worth watching before its part.

Part V and Part X assume each other. The two click lectures in Part V are the applications of prediction and acknowledgement, and that page’s own scenario is a block placed against a wall, which needs Part V’s vocabulary. The cut is at Part V: both click pages state the ledger’s contract in the same four sentences near their top, which is all either needs, and the machinery waits for Part X, so the whole of Parts V and VI is watched before that one Part X lecture. Part VI hands something forward to two Part X pages: the client level opens by saying it is not an authority either, and prediction and acknowledgement answers the same question a second way, and both need authority behind them.

Ten pages carry most of the graph — nine rows below, because the two server ticks are one dependency in two lectures. The membership rule is mechanical: a page that two or more landing pages name under before you start, less anatomy, codecs and registries, the three pages the whole book leans on. A viewer who has watched these ten can take the parts they belong to in almost any order; a viewer who skips one of them will find a later part’s first surprise unexplained.

the pageits partthe parts whose landing pages assume it
The server tick and the level tickIIIIV, V, VI, VII, VIII, IX, XIII — seven of the eight later parts on the server’s side of the game, for which phase something ran in
Environment attributes and timelinesIVIII, VI, XI, XII — the clock, the schedule, the colour of the sky, and the biome as one layer of the stack
Chunk anatomyIVV, VI, XII — a block state’s home, the heightmaps that decide where a mob may spawn, and what terrain is written into
AuthorityVIVIII, IX, X — the premise under every page about a player, and under what the client is told
The resource systemIIIII, VII, XI — the staged load and its barrier: a server’s own data at startup, where recipes and loot tables come from, and the reload the atlases are built by
The connectionIXX, XIII — the thread boundary between the network and the game, and the hop most packets make across it
Tickets and loadingIVIII, VI — what entity-ticking means
The data-driven type patternIIXII, XIII — the type field in a data-pack file and the registry it dispatches on; these two parts own most of its instances
Text componentsIIIX, X — what a chat line and a screen’s label are before anything draws them

Nineteen other system pages are named by exactly one landing page, and each belongs in that part’s before you start rather than in this table. Three are worth saying out loud anyway, because a viewer coming for one part alone most often has to fetch them from elsewhere in the book: blocks and states before Part VI, contexts and predicates before Part XIII’s advancements, and the client loop before Part XI.

Watched straight through, the sidebar order still needs one departure from itself, and it is now as small as it can be: environment attributes and timelines is the first lecture of Part IV and wants watching before Part III’s second. A viewer coming for one part rather than the whole book takes that part’s before you start list as the order.

The book’s two ends are chosen rather than inherited. What this book skips is the second lecture for the reason Part I’s entry gives; Part XIII’s game tests are the closing lecture because they are the game’s own answer to the question the whole book has been asking — how do you know what it does — and because nothing later depends on them.