How registration works
One shape for every kind of content, the two ways to declare it, and what RetroAPI does with your ids once you hand them over.
Everything in this section, blocks, items, mobs, dimensions, sounds, particles, world features, follows the same three-part shape. Learn it once here and the individual pages are mostly a list of what each builder can do.
The shape
- Describe it. A builder (
RetroBlockAccess.create(...),RetroItemAccess.create(),RetroFeatures.ore(...)) or a plain constructor for your own subclass. - Name it. A namespaced identifier:
example_mod:ruby_ore. This name is the permanent one, everything else about your content, including its number, is allowed to change. - Register it from your
retroapientrypoint (Entrypoints & sides), so it lands at a point where the platform is ready and before any world assigns ids.
// a block
RUBY_ORE = RetroBlockAccess.create(Material.STONE)
.strength(3.0F).texture(id("ruby_ore")).register(id("ruby_ore"));
// an item
RUBY = RetroItemAccess.create()
.maxStackSize(64).texture(id("ruby")).register(id("ruby"));
// something that generates in the world
RetroFeatures.ore(RUBY_ORE).size(6).count(8).heightRange(4, 32).register();Ids move. Names do not.
Beta identifies a block by a number, and a modded block has to be given one. RetroAPI hands out a provisional number at registration and then, when a world is opened, replaces it with whatever that world's saved map says the block should be, so that a world you built last week still has your ore where you left it, even if you have since added or removed content.
The consequence is the single most important rule in this wiki: never keep a raw numeric id, and never keep an ItemStack, in a field that outlives registration, unless you are prepared to fix it up. An ItemStack stores the number, not the name. If you must cache one, subscribe to the remap:
IdRemapCallback.EVENT.register(remap -> {
remap.fix(ExampleMod.STARTER_KIT); // an ItemStack field
ExampleMod.magicId = remap.map(ExampleMod.magicId); // a raw id
});RetroAPI already does this for everything it owns: crafting and smelting recipes, fuels, achievement icons. This callback is for the ones only you know about.
Code-driven and data-driven
Most content can be declared as a builder call or as a JSON file under assets/ or data/. Wherever the two are genuine alternatives, a switch sits directly above them and flips the whole wiki between them. They are not alternatives everywhere, though, so here is the honest map:
| Topic | Code | Data files | Which wins |
|---|---|---|---|
| Block properties (hardness, light, material) | ✔ builder | - | code only |
| Block textures | ✔ .texture, .sided, .column, .textures | ✔ model JSON | a model JSON overrides .texture |
| Block state definitions | ✔ .states(...) | ✔ blockstates/*.json "properties" | code wins on conflict, but see below |
| What each state looks like | state-aware textures, tint, overlays | ✔ blockstates/*.json "variants" | only data can pick a model per variant |
| Item sprites | ✔ .texture, .layers, .overlay | ✔ models/item/*.json | a model JSON overrides code |
| Tags (mineable, tiers, your own) | ✔ .mineable, .needsTool, .tag, RetroTags | ✔ data/<ns>/tags/**.json | union, both apply |
| Display names | ✔ auto-generated fallback | ✔ lang/*.lang | lang file wins |
| Texture animation | ✔ addAnimated*Texture | ✔ .png.mcmeta | either; mcmeta is detected automatically |
| Sounds | ✔ RetroSounds | ✔ drop .ogg files in place | files are auto-loaded |
| Recipes, entities, dimensions, features, particles, components | ✔ | - | code only |
RUBY_ORE = RetroBlockAccess.create(Material.STONE)
.register(id("ruby_ore")); // displays as "Ruby Ore"No file at all: with no translation present RetroAPI generates a readable name from the identifier. Fine while prototyping.
tile.example_mod.ruby_ore.name=Ruby OreOne line per name, per language, and a translation pack can add languages you never wrote. What you ship.
Where the two combine, not compete
The table above has one row that is easy to misread, so it gets its own section. Block states are not an either/or. The two sides do different jobs and a block with visual variants normally needs both.
| You want… | Comes from |
|---|---|
| the list of properties to exist at all | either side, .states(LIT) or "properties" in the JSON |
a typed handle to read and write with (state.get(LIT)) | code only |
| a different model per variant | the blockstate JSON only |
| a different texture/tint/overlay per state, with no model | code (.sided, .tint, .overlay) |
Declare the property in JSON only and it does exist, but in Java you have to find it by name and cast, which throws away the type safety that made the property object worth having:
@SuppressWarnings("unchecked")
RetroProperty<Boolean> lit =
(RetroProperty<Boolean>) RetroStates.property(LAMP, "lit");
RetroStates.set(world, x, y, z, state.with(lit, true));So the usual shape of a lamp that both does something and looks different is: the property in code, the variants in the blockstate file. They agree because they are the same property name; if they disagree, the code declaration wins and the mismatch is logged.
// code: the property you will actually use in game logic
static final RetroBoolProperty LIT = RetroBoolProperty.of("lit");
LAMP = RetroBlockAccess.create(Material.STONE)
.states(LIT)
.register(id("lamp"));{
// no "properties" needed: the code already declared it
"variants": {
"lit=false": { "model": "example_mod:block/lamp_off" },
"lit=true": { "model": "example_mod:block/lamp_on" }
}
}A block whose states are pure gameplay, a machine that tracks progress but never changes its face, needs no JSON at all. A block that only changes color with its state can stay in code too, using .tint(...) or .overlay(...). It is specifically different models per variant that requires the file.
What each page in this section covers
| Page | Covers |
|---|---|
| Blocks | the builder, per-face textures, tint and overlays, tool gating, custom block classes |
| Block states | properties, flattened indices, reading/writing state, storage past 16 states |
| Items | stacks, custom item classes, layered sprites, handheld poses |
| Tools, food & armor | tool kinds and tiers, tools with no ToolMaterial, edible items, armor sets |
| Tags | mineable/*, needs_*_tool, item tags, and your own groupings |
| Recipes & fuel | shaped, shapeless, smelting, fuel, the wildcard rule |
| Block entities & GUIs | tickers, inventories, containers, screens, synced fields |
| Entities | mobs, renderers, and how spawns reach the client |
| Achievements | pages, icons, granting |
| Sounds & music | effects, streaming music, records, the multiplayer bridge |
| Particles | the particle registry, sprite particles, the multiplayer bridge |
| Item components | typed per-stack data that survives saving and networking |
| Dimensions & portals | registering a world, walking into it, serial ids |
| Worldgen | chunk generators, biomes, and features that generate in existing worlds |
Under StationAPI, registration is handed to StationAPI and the calls above still work unchanged, that is the point of routing everything through one entrypoint. What differs is listed on StationAPI interop.