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

Text components

Verified against Minecraft 26.2 · 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 click action. 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 anyone knows what it says. The 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 the 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, during command execution
Languagea key to a template; the client swaps the instance on every resource reloadRender thread 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

flowchart TD
    Comp["Component: an interface with four abstract members"] --> MComp["MutableComponent: the only implementation"]
    MComp --> Contents["one ComponentContents"]
    MComp --> Style["one Style: eleven nullable fields, null means inherit"]
    MComp --> Siblings["an ordered list of siblings, each a Component"]
    Siblings -. "recursion" .-> MComp
    subgraph kinds ["the seven kinds"]
        Text["PlainTextContents"]
        Trans["TranslatableContents: key, fallback, arguments"]
        Key["KeybindContents"]
        Score["ScoreContents"]
        Sel["SelectorContents"]
        Nbt["NbtContents"]
        Obj["ObjectContents: a sprite in the text"]
    end
    Contents --> kinds
    MComp --> CS["ComponentSerialization.CODEC, and STREAM_CODEC over NBT"]

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 a fresh Style each time. 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, which is how a death message too long to send is cut to 256 for its replacement. 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 ComponentTranslatableContents.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 that say nothing until a server resolves them. 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.obfuscatedStyle.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, Style.NO_SHADOW is zero, and Style.withoutShadow 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, three hovers, one refusal

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, 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 budget, and every clientbound chat packet uses them (packets and stream codecs). So does everything else 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 the texts that must decode before a registry exists: 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: ResolutionContext.validate swaps a rejected sprite 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
    participant SP as ServerPlayer
    participant CT as CombatTracker
    participant CS as ComponentSerialization
    participant CPL as ClientPacketListener
    participant DScr as DeathScreen
    participant TrC as TranslatableContents
    participant Language as Language

    Note over SP,CT: the server tick in which the player dies
    SP->>CT: die: getDeathMessage, if show_death_messages
    CT->>CT: the last CombatEntry's DamageType.deathMessageType, then DamageSource.getLocalizedDeathMessage
    CT-->>SP: translatable death.attack.arrow, arguments: the victim's display name, the killer's
    SP->>CS: ClientboundPlayerCombatKillPacket to the victim, TRUSTED_STREAM_CODEC
    SP->>CS: the same component in ClientboundSystemChatPacket to everyone, and getString for the log
    Note over CS: the Netty thread, PacketEncoder
    CS->>CS: encode through NbtOps: translate, with, and each argument a compound with hover_event and insertion
    Note over CS,CPL: the client's Netty thread decodes, then ensureRunningOnSameThread
    CS->>CPL: handlePlayerCombatKill, a MutableComponent nobody has read
    CPL->>DScr: new DeathScreen with the packet's message, Render thread
    Note over DScr: the next frame
    DScr->>TrC: visitText, then Component.visit reaches the contents
    TrC->>Language: decompose: Language.getInstance, getOrDefault(death.attack.arrow)
    Language-->>TrC: the template from the client's language stack, en_us then the selected code
    TrC-->>DScr: the victim's name, " was shot by ", the killer's name, each argument visited in turn
    TrC->>Language: the killer's name, if it is a mob, is entity.minecraft.zombie, worded the same way

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.msgIddeath.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 actually found one, so a fall-typed source with no recorded fall drops back to the ordinary message. 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: if the send fails, a replacement carries death.attack.even_more_magic with the first 256 characters of the real message in a hover, so the death screen never goes 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.

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 the message reaches nobody and is never read at all. On a 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 named mob’s death is logged the same way from LivingEntity.die. The 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 crash the decomposer. 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 took the same path through ChatListener.handleSystemMessage.

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.visitStringDecomposer, 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 on the first frame that drew it.

Why does /tellraw with a selector name people when chat with an @ does not? Chat is a literal, always: nothing in the chat path calls ComponentUtils.resolve. A command’s message argument parses @ into MessageArgument.Parts and expands them behind Permissions.COMMANDS_ENTITY_SELECTORS; a /tellraw component is resolved whole by ComponentArgument.getResolvedComponent.

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.

What does an object component actually put in the text? One U+FFFC placeholder whose style names a sprite font — an atlas sprite or a player head — so the font system draws the picture where a glyph would go. Anything that reads the text as a string sees [sprite] or [name head] instead.

Why does the server console show death messages in English? The server’s Language is Language.DEFAULT_INSTANCE, the bundled en_us.json; Language.inject is called only by the client’s LanguageManager.

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.