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

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.