Chapter 21, Item Memory
Beta ItemStacks remember nothing. This chapter gives them typed, persistent data the modern way: declare a component once, then get and set it on any stack.
A beta ItemStack is three numbers: id, count, damage. There is no NBT field, so two stacks of the same item are interchangeable and an item cannot carry anything beyond which item it is and how worn it is. Modern Minecraft moved past that years ago: items hold rich, typed data called data components, and stack.get(TYPE) / stack.set(TYPE, value) read and write it. RetroAPI ports that whole idea backwards. You declare a component type once, then read and write it on any stack, and the value persists, copies, and (on a dedicated server) syncs. This is the closest neighbor to Chapter 5: components extend items, so if you have not met the item builder yet, start there.
Registering a component
A component is a RetroComponentType<T>, the beta port of modern's DataComponentType. You make one with RetroComponents.register(id, defaultValue, serializer): an id, the value returned when the component is unset, and a serializer that says how the value rides in NBT for saving (and, on a dedicated server, on the wire). RetroAPI ships serializers for every primitive you are likely to want:
| Serializer | Holds |
|---|---|
RetroComponentType.INT | an Integer |
RetroComponentType.FLOAT | a Float |
RetroComponentType.BOOL | a Boolean |
RetroComponentType.STRING | a String |
RetroComponentType.LONG | a Long |
RetroComponentType.DOUBLE | a Double |
RetroComponentType.ITEM | an Item reference (stored as its id) |
RetroComponentType.ITEM_STACK | a whole ItemStack: id, count, damage, and its own components |
RetroComponentType.NBT | a raw NbtCompound, the free-form escape hatch |
Here are the showcase's three components, registered in init(). Two are plain ints, the third holds a whole record (more on that below):
// Register two typed components (modern DataComponentType equivalent). They are
// just ints here; RetroComponentType.compound(...) handles records.
CLICK_COUNT = com.periut.retroapi.component.RetroComponents.register(
id("click_count"), 0, com.periut.retroapi.component.RetroComponentType.INT);
MOOD = com.periut.retroapi.component.RetroComponents.register(
id("mood"), 0, com.periut.retroapi.component.RetroComponentType.INT);
ATTUNEMENT = com.periut.retroapi.component.RetroComponents.register(
id("attunement"), GemAttunement.EMPTY, GemAttunement.CODEC);Keep the returned RetroComponentType in a static field, exactly as you keep a Block or Item reference. The default (0, or GemAttunement.EMPTY) is what a stack hands back before anyone has written to it.
Reading and writing
Modern Minecraft can add methods to ItemStack; beta cannot, so the get/set API lives on RetroComponents as statics taking the stack as the first argument. That is the only real difference from stack.get(TYPE):
| Call | What it does |
|---|---|
RetroComponents.get(stack, type) | the value, or the type's default if unset |
RetroComponents.getOrDefault(stack, type, fallback) | the value, or your fallback if unset |
RetroComponents.set(stack, type, value) | write the value onto the stack |
RetroComponents.has(stack, type) | whether the stack explicitly has it set |
RetroComponents.remove(stack, type) | clear it from the stack |
The Counter item is the fabric-docs example ported straight across: every right-click bumps a CLICK_COUNT component on the stack. The read-modify-write is one tiny helper:
private static void bump(ItemStack stack) {
int count = RetroComponents.getOrDefault(stack, ExampleMod.CLICK_COUNT, 0);
RetroComponents.set(stack, ExampleMod.CLICK_COUNT, count + 1);
}That stack now remembers how many times it was clicked, even though a beta ItemStack has nowhere of its own to keep that number. The how lives in the Persistence and Multiplayer sync sections below; for the moment, trust that it sticks.
The Counter item is registered with .texture(id("counter")), so it needs an item texture named counter.png:
src/main/resources/assets/example_mod/textures/item/counter.png
Tooltips
A clicked count is no good if you cannot see it. The catch: beta has no tooltip system at all. Hovering an item in the inventory shows its name and nothing more. So RetroAPI adds the multi-line tooltip itself and reads the extra lines from your item. Implement RetroTooltipProvider and override appendTooltip(stack, lines), adding as many strings as you like; they render under the name. Prefix a line with a section sign for color, the same codes vanilla chat uses (§6 is gold, §b aqua, and so on):
@Override
public void appendTooltip(ItemStack stack, List<String> lines) {
// section sign + 6 = gold, matching the fabric example's ChatFormatting.GOLD.
lines.add("§6Clicks: " + RetroComponents.get(stack, ExampleMod.CLICK_COUNT));
}That covers items you wrote. For an item you do not own (a vanilla item, or one from another mod), implement the same interface and hand it to RetroTooltips.register(item, provider) instead. RetroAPI calls both paths, the item's own provider and any registered one, for every hovered stack:
/** Adds a tooltip provider for an item (works for vanilla items too). */
public static void register(Item item, RetroTooltipProvider provider) {
PROVIDERS.put(item, provider);
}This is also the only general-purpose multi-line tooltip in beta, so even an item with no components can use it to print a description line under its name.
Records and lists
Components are not limited to single primitives. The compound(writer, reader) serializer packs a whole structured value into its own sub-compound and back, the beta-era stand-in for a RecordCodecBuilder. The showcase's GemAttunement is a two-field record (a level and an awakened flag) carried as one component:
public record GemAttunement(int level, boolean awakened) {
public static final GemAttunement EMPTY = new GemAttunement(0, false);
/** The serializer: writes the two fields into a sub-compound and reads them back. */
public static final RetroComponentType.Serializer<GemAttunement> CODEC =
RetroComponentType.compound(
(nbt, value) -> {
nbt.putInt("level", value.level);
nbt.putBoolean("awakened", value.awakened);
},
(NbtCompound nbt) -> new GemAttunement(nbt.getInt("level"), nbt.getBoolean("awakened")));
}The writer is a BiConsumer<NbtCompound, T>, the reader a Function<NbtCompound, T>. Register it like any other serializer (the ATTUNEMENT line above passes GemAttunement.CODEC), then get and set GemAttunement values just as you would an int.
For many-of-something, wrap an element serializer with listOf. It produces a Serializer<List<E>> that stores the list as an NBT list of compounds, so RetroComponentType.listOf(RetroComponentType.STRING) gives you a component holding a List<String>, and listOf(GemAttunement.CODEC) a list of records.
Inventories and raw NBT
Yes, a component can hold a whole inventory or arbitrary NBT. Two built-in serializers cover it. ITEM_STACK stores a full ItemStack, id, count, damage, and the stack's own components, and combined with listOf it stores an entire inventory in a single component:
// A component that holds a list of ItemStacks, e.g. a bag or a backpack's contents.
static final RetroComponentType<List<ItemStack>> CONTENTS =
RetroComponents.register(id("contents"), new ArrayList<>(),
RetroComponentType.listOf(RetroComponentType.ITEM_STACK));
// then read/write it like any other component
List<ItemStack> held = RetroComponents.getOrDefault(stack, CONTENTS, new ArrayList<>());
held.add(new ItemStack(Item.DIAMOND, 3));
RetroComponents.set(stack, CONTENTS, held);When even that is too rigid, RetroComponentType.NBT stores a raw NbtCompound, the free-form escape hatch for data you already have as nbt, or shapes a typed serializer would be fussy for. All of it rides the same persistence and sync as a plain int, so an inventory stored in a component saves with the world and (on a dedicated server) syncs down like anything else.
Component-driven textures
Modern Minecraft repaints an item from its components, stacking and tinting model layers at render time. RetroAPI does the same: an item implements RetroLayeredTexture and hands back a list of RetroTextureLayer (an atlas sprite id plus a tint) that RetroAPI draws back to front everywhere the item appears, hotbar, inventory, hand, and dropped on the ground. The base is just layer 0; there is no separate base texture. It is drawn live, nothing is baked, so the number of distinct looks is unbounded: a tool with thousands of component-driven colors, or a smooth gradient, costs nothing to store because only the layers of the stacks actually on screen are ever drawn.
For the simple case of swapping ONE whole sprite by a component (no tint, no stacking), there is also RetroDynamicTexture with a single getDynamicTextureId(stack). RetroLayeredTexture is the richer one and is what the Mood Gem uses now.
The simplest layered item: the Sticked Apple
Before the component-driven version, the plainest possible layered item: two untinted layers, a vanilla stick under a vanilla apple, with no texture or components of its own. Item.getTextureId(0) pulls a vanilla item's atlas slot, so this ships no PNG:
@Override
public List<RetroTextureLayer> getTextureLayers(ItemStack stack) {
return List.of(
RetroTextureLayer.plain(Item.STICK.getTextureId(0)), // bottom layer: a stick
RetroTextureLayer.plain(Item.APPLE.getTextureId(0))); // top layer: an apple
}The component-driven version: the Mood Gem
The Mood Gem ships ONE grayscale gem sprite and TINTS it live from its MOOD component, exactly how modern recolors a layer, instead of baking a PNG per color. A second, untinted sparkle layer appears only once the ATTUNEMENT record says the gem is awakened, so BOTH a primitive component (the tint) and a structured one (whether the overlay exists) drive the picture. It registers the two sprites up front:
MOOD_GEM_BASE = com.periut.retroapi.register.block.RetroTextures.addItemTexture(id("mood_gem_base")).id;
MOOD_GEM_SPARKLE = com.periut.retroapi.register.block.RetroTextures.addItemTexture(id("mood_gem_sparkle")).id;
// The 0xRRGGBB tint per mood (red, green, blue, gold), multiplied onto the gray gem.
public static final int[] MOOD_TINTS = {0xFF4D4D, 0x57E06A, 0x4D8AFF, 0xFFC233};and builds its layer list from the components each render:
@Override
public List<RetroTextureLayer> getTextureLayers(ItemStack stack) {
int mood = RetroComponents.getOrDefault(stack, ExampleMod.MOOD, 0);
GemAttunement att = RetroComponents.getOrDefault(stack, ExampleMod.ATTUNEMENT, GemAttunement.EMPTY);
List<RetroTextureLayer> layers = new ArrayList<>(2);
layers.add(RetroTextureLayer.tinted(ExampleMod.MOOD_GEM_BASE, ExampleMod.MOOD_TINTS[mood]));
if (att.awakened()) {
layers.add(RetroTextureLayer.plain(ExampleMod.MOOD_GEM_SPARKLE));
}
return layers;
}Right-clicking cycles MOOD through 0..3, and the gem recolors live the instant the component changes; loop all the way around and the ATTUNEMENT record awakens, adding the sparkle. Registering the two sprites is Chapter 3.
Persistence
Here is the trick that makes all of the above work despite beta stacks having no NBT field. RetroAPI injects a component map onto every ItemStack through a mixin (the RetroComponentHolder duck interface), and the get/set calls read and write that map. The map is then:
- deep-copied when a stack is copied or split, so a half-stack keeps the parent's components and the two halves drift apart independently;
- written to and read from NBT under a single
retroapi:componentstag, so it rides along through every save path; - saved to the sidecar and restored on load, the same vanilla-safe sidecar machinery that hides modded blocks and inventories from vanilla saves (the mechanism is Chapter 6).
The upshot: components survive slot-moves, dropping the item, picking it back up, quitting and reloading the world. The Counter's number is still there tomorrow. Because beta has no key-iteration API on NbtCompound, the serializer writes the whole component compound to the binary NBT format and re-reads it to recover the keys; that detail lives in ComponentNbt and you never touch it.
Multiplayer sync
Singleplayer needs no sync at all. Beta's singleplayer is client-only, the client is the world, so it already holds the real stacks with their components and everything simply works.
A dedicated server is the hard case. Beta serializes an ItemStack inline as id/count/damage with no NBT, so a client never learns about components the server set, and the held item's texture and tooltip would never update. RetroAPI's ComponentWire fixes this: packet mixins append a tiny component blob after each stack in the slot, inventory, and dropped-item spawn packets (the stack is always the last field, so trailing data is safe) and read it back on the far side. A dropped or unreadable blob is non-fatal, the item still works, just unsynced for that frame.
One rule matters more than the wire format, and it is the bug that bites everyone. Do not guard use() with the usual !world.isRemote server-only pattern. Bump the component on both sides:
@Override
public ItemStack use(ItemStack stack, World world, PlayerEntity player) {
// Bump on BOTH sides: client predicts so the tooltip updates instantly,
// server is the authority that persists and reconciles. Guarding with
// !world.isRemote is what made the count never move on a dedicated server.
bump(stack);
return stack;
}
@Override
public boolean useOnBlock(ItemStack stack, PlayerEntity player, World world, int x, int y, int z, int side) {
// Right-clicking a BLOCK: beta always processes these on the server, so this
// is the reliable path. Returning true consumes the interaction.
bump(stack);
return true;
}The client bump is a prediction so the tooltip and sprite move the instant you click; the server bump is the authority that persists and, via ComponentWire, reconciles the client. They land on the same number, so there is no flicker. Guard it server-only and the client does nothing, sees no change, and so never tells the server to do anything either, the count just sits still forever. Right-clicking a block routes to useOnBlock, which the server always processes, so it is the rock-solid path when air-clicks feel unreliable. The same logic drives the Mood Gem's color cycle. Setting up and probing a dedicated server is Chapter 12 and Chapter 22.
The Counter and the GemAttunement record are ports of the custom-data-components walkthrough in FabricMC's docs (fabric-docs), used under CC BY-NC-SA. RetroAPI carries the same shapes back to beta where ItemStack has no NBT to lean on.
Components give items a memory. Next we make sure all of it actually holds up: running the client and a real dedicated server, offline mode, and the sided gotchas that this chapter's sync rule hints at, in Chapter 22, Proving Grounds.


