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
| class | what it decides | thread |
|---|---|---|
Identifier | the name — namespace and path — and which strings are legal ones | any |
ResourceKey | the name paired with the registry it belongs to, interned so equal keys are one object | any |
Registry · MappedRegistry | the table: key to object to integer, the frozen flag, the two tag tables | elements written on the launching thread or a load task’s worker, tags by whichever thread applies them; read from anywhere |
Holder | a 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 · RegistryAccess | the read-only view a codec resolves against: all the registries it may name | any |
BuiltInRegistries | the static registries — created empty at class init, filled and frozen by Bootstrap.bootStrap | the launching thread, before any server or client object exists |
RegistryDataLoader | the 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 know | Util.backgroundExecutor |
LayeredRegistryAccess | which layer may see which: four RegistryLayers on the server, two ClientRegistryLayers on the client | the 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.
Identifierwas ResourceLocation, and the vanilla data you remember being registered in code is now read as JSON like anyone else’s:RegistrySetBuilder,BootstrapContextandVanillaRegistriesare 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). AndBlock.BLOCK_STATE_REGISTRYis not a registry:IdMapperis the standaloneIdMapbehind block-state ids and similar palettes, sharing an interface withRegistryand 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.