Chapter 4, Blocks

Three blocks, graded easy to interesting: one plain cube, one with a face per side, and a pipe that draws itself.

Blocks are where most mods start, and RetroAPI makes the easy ones genuinely easy without hiding the hard ones. The whole surface is a small builder you chain options onto, plus a couple of static entry points. We'll meet all of it across three blocks, each a little more ambitious than the last.

Block #1, the plainest cube

Here is EXAMPLE_BLOCK exactly as it appears in init(), including the commented menu of other options you could have chained:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// The simplest possible block: stone-like, with its own texture.
EXAMPLE_BLOCK = RetroBlockAccess.create(Material.STONE)
	.sounds(Block.STONE_SOUND_GROUP)   // mine/place/step sounds
	.strength(1.5f, 10.0f)             // hardness, blast resistance
	.texture(id("example_block"))      // one texture used on all six faces
	.register(id("example_block"));
// Other builder options you can chain before .register(...):
//   .light(1.0f), glow like glowstone
//   .opacity(0), let light pass through
//   .nonOpaque(), neighbours draw their touching faces
//   .bounds(minX,minY,minZ,maxX,maxY,maxZ), non-full-cube hitbox
//   .sprite(Block.GLASS.getTexture(0)), reuse a vanilla texture instead
//   .alwaysDrops() / .alwaysEffectiveTool() / .effectiveTool(PickaxeItem.class)

Read it left to right: create a stone-material block, give it stone sounds, set its hardness and blast resistance, point it at a texture, and register it under an identifier. That's a complete, mineable, craftable block. (The recipe that crafts it is Chapter 11; its lang display name is Chapter 3.)

That .texture(id("example_block")) needs an actual image. RetroAPI loads it by name from your mod's block-texture folder, so save a 16x16 PNG at this exact path (example_mod is your mod id, and the file name matches the id(...) you passed to .texture):

src/main/resources/assets/example_mod/textures/block/example_block.png

example_block
example_block.png
download

The whole builder, in one table

These are all the builder methods. Chain any of them before register; each returns the builder so they fit on one line. This is the complete list, there are no others.

MethodWhat it does
sounds(BlockSoundGroup)mine / place / step sounds (e.g. Block.STONE_SOUND_GROUP)
strength(float)hardness (mining time); resistance derived from it
strength(float, float)hardness and blast resistance together
resistance(float)blast resistance on its own
light(float)emitted light, 0 to 1 (1.0 = full glowstone)
opacity(int)how much light it blocks (0 = transparent to light)
nonOpaque()not a solid cube, neighbours render the faces they share with it
bounds(minX,minY,minZ,maxX,maxY,maxZ)a non-full-cube hitbox/shape, in 0 to 1 block units
renderType(NamespacedIdentifier)use a render type (vanilla or your own) instead of a plain cube
sprite(int)set the default sprite index (particles + item form), often a vanilla one
texture(NamespacedIdentifier)load textures/block/<name>.png onto all six faces
alwaysDrops()always drop as an item when broken, regardless of tool
alwaysEffectiveTool()any tool mines it at full speed
effectiveTool(Class<? extends Item>)which tool class mines it effectively (e.g. PickaxeItem.class)
register(NamespacedIdentifier)finalize and register; returns the Block, auto-creates its BlockItem
register(NamespacedIdentifier, IntFunction<BlockItem>)same, but you supply the BlockItem (custom item form)

And the three static entry points that start (or feed) a builder:

Static methodUse it when
RetroBlockAccess.create(Material)a plain Block of that material is enough (block #1)
RetroBlockAccess.of(Block)you wrote a Block subclass and hand in an instance (blocks #2, #3)
RetroBlockAccess.allocateId()get a fresh numeric id to pass into your subclass's constructor

Every block gets a BlockItem automatically on register, that's the item form you hold in your hand and place. The two-argument register(id, IntFunction<BlockItem>) overload is there only when you want a custom item form (its own name, stack behavior, or sub-items).

About those ids

Beta 1.7.3 stores blocks by a numeric id in a fixed-size table, and the low slots are all taken by vanilla. RetroAPI's job is to make that invisible to you:

  • It allocates numeric ids ≥ 256, above vanilla's range, so your blocks never collide with the game's.
  • It remaps them per-world for stability. The actual number a block gets can differ between worlds; RetroAPI keeps a per-world mapping so a save always reads back the right block even if the raw number shifted.
  • It hides them from vanilla saves using sidecar files alongside the world data, so a world opened without your mod doesn't choke on an id it doesn't recognise, and your content reappears intact when the mod returns.

The upshot: you never type a numeric id. You deal only in NamespacedIdentifiers, id("example_block"), and RetroAPI handles the bookkeeping that used to be the most error-prone part of beta modding.

Block #2, a face on every side

When one texture on all six faces isn't enough, you write a Block subclass. The sided block has a top, a bottom, and a shared side texture, grass-style. Here's the entire class:

src/main/java/com/example/example_mod/ExampleSidedBlock.java
package com.example.example_mod;

import com.periut.retroapi.register.block.RetroTexture;
import com.periut.retroapi.register.block.RetroTextures;

import net.minecraft.block.Block;
import net.minecraft.block.material.Material;

/**
 * A block with a different texture on its top, bottom and sides (think grass or
 * a crafting table). For anything beyond "one texture on all faces" you make a
 * Block subclass, register each texture with RetroTextures.addBlockTexture(...),
 * and override getTexture(face).
 *
 * Faces: 0 = bottom, 1 = top, 2 = north, 3 = south, 4 = west, 5 = east.
 */
public class ExampleSidedBlock extends Block {

	private final RetroTexture bottom;
	private final RetroTexture top;
	private final RetroTexture side;

	public ExampleSidedBlock(int id, Material material) {
		super(id, material);
		// Each call loads assets/example_mod/textures/block/<name>.png into the
		// block atlas and returns a handle whose .id is the sprite index.
		this.bottom = RetroTextures.addBlockTexture(ExampleMod.id("sided_block_bottom"));
		this.top = RetroTextures.addBlockTexture(ExampleMod.id("sided_block_top"));
		this.side = RetroTextures.addBlockTexture(ExampleMod.id("sided_block_side"));
		// The default sprite (used for particles etc.) + keep the block/texture pair
		// tracked so the atlas can re-resolve it (e.g. when StationAPI is present).
		this.textureId = this.side.id;
		RetroTextures.trackBlock(this, this.side);
	}

	@Override
	public int getTexture(int face) {
		switch (face) {
			case 0: return this.bottom.id;
			case 1: return this.top.id;
			default: return this.side.id;
		}
	}
}

And its registration, note RetroBlockAccess.of(...) wrapping a freshly-constructed instance, with allocateId() supplying the constructor's id:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
SIDED_BLOCK = RetroBlockAccess.of(new ExampleSidedBlock(RetroBlockAccess.allocateId(), Material.WOOD))
	.sounds(Block.WOOD_SOUND_GROUP)
	.strength(1.0f)
	.alwaysEffectiveTool()
	.alwaysDrops()
	.register(id("sided_block"));

Three things make this work:

  • RetroTextures.addBlockTexture(id) loads one PNG into the atlas and returns a RetroTexture whose public .id is its sprite index (the atlas mechanics are Chapter 3).
  • getTexture(face) is vanilla's per-face hook. The face indices are 0 = bottom, 1 = top, 2 = north, 3 = south, 4 = west, 5 = east; returning a sprite index per face is all the renderer needs.
  • this.textureId = this.side.id sets the default sprite, used for break particles and the held-item icon, and RetroTextures.trackBlock(this, this.side) registers the block/texture pairing so the atlas can re-resolve it later (for instance, into StationAPI's atlas).
sided_block_top
sided_block_top.png
download
sided_block_bottom
sided_block_bottom.png
download
sided_block_side
sided_block_side.png
download

Block #3, a pipe that draws itself

The last block isn't a cube at all. It's a small box in the center of its space, drawn by a custom render type rather than the default cube renderer. Two pieces: register the render type once, then point a block at it.

The render type is registered as a static field at the top of ExampleMod, so it exists before init() runs:

src/main/java/com/example/example_mod/ExampleMod.java, static field
public static final NamespacedIdentifier PIPE_RENDER_TYPE = RenderType.register(
	id("pipe"),
	ctx -> {
		// ctx is a BlockRenderContext: it knows the block, position, world, lighting,
		// and exposes helpers like renderFullCube(), renderLitFace(face, sprite), ...
		ctx.renderAllLitFaces(Block.COBBLESTONE.getTexture(0));
		return true;
	}
);

And the block that uses it, from init():

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// A non-full-cube block using the custom render type registered above.
PIPE_BLOCK = RetroBlockAccess.create(Material.STONE)
	.sounds(Block.METAL_SOUND_GROUP)
	.strength(2.0f)
	.nonOpaque()
	.bounds(4 / 16.0F, 4 / 16.0F, 4 / 16.0F, 12 / 16.0F, 12 / 16.0F, 12 / 16.0F)
	.renderType(PIPE_RENDER_TYPE)
	.sprite(Block.COBBLESTONE.getTexture(0)) // used for particles + item form
	.register(id("example_pipe"));

The block is nonOpaque() (so it doesn't hide neighbours), shrunk to the middle 8×8×8 with bounds(...) (the 4/16 to 12/16 range), and pointed at PIPE_RENDER_TYPE with renderType(...). Because it ships no texture of its own, .sprite(Block.COBBLESTONE.getTexture(0)) borrows cobblestone for its particles and held-item form, and the render lambda borrows the same sprite for its faces.

The render context

A render type is a function from a BlockRenderContext to "did I draw it?" (true means you handled rendering; the engine won't fall back to the default cube). The context knows the block, its position, the world, and lighting, and hands you a kit of helpers:

HelperWhat it draws / gives you
renderFullCube()a standard six-sided cube using the block's own sprites
renderAllLitFaces(sprite)all six faces with one sprite, lit per face (what the pipe uses)
renderLitFace(face, sprite)a single face, with that face's lighting
shouldRenderFace(face)whether a face is visible (neighbour isn't covering it)
getSprite(face)the block's sprite index for a face
tesselator()the raw Tessellator for hand-rolled vertices when you need full control

Custom render type ids are numbered starting at 18, the vanilla render types occupy 0 to 17. You never write that number; RenderType.register hands back a NamespacedIdentifier and resolves the int internally. If you only need a built-in look, the RenderTypes class exposes the vanilla ones as constants, RenderTypes.BLOCK, RenderTypes.CROSS, RenderTypes.TORCH, RenderTypes.PLANT, RenderTypes.FENCE, and the rest, to pass straight into .renderType(...).

Mineable: modern tool gating

Beta's rule for "which tool drops this block" is crude. RetroAPI swaps in the modern mineable tag system, the same one a 1.21 data pack uses, and you opt a block in with one builder call:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// Mineable tags, modern semantics: ruby ore drops ONLY when mined with a
// pickaxe, at pickaxe speed. The same tag also loads from data files; see
// data/example_mod/tags/block/mineable/pickaxe.json, which re-adds this block
// (a harmless union) and tags vanilla glass as pickaxe-mineable too.
RUBY_ORE = RetroBlockAccess.of(new RubyOreBlock(RetroBlockAccess.allocateId()))
	.sounds(Block.STONE_SOUND_GROUP)
	.strength(3.0f)
	.texture(id("ruby_ore"))
	.mineable(com.periut.retroapi.tag.RetroTool.PICKAXE)
	.register(id("ruby_ore"));

The ore is now a Block subclass, handed in through RetroBlockAccess.of(...) with allocateId() supplying its constructor id, because it drops something other than itself. Custom drops work the plain vanilla way: override getDroppedItemId(int meta, Random) to return the item id (and, optionally, getDroppedItemCount(Random) for the count). Combined with the mineable tag, the ruby only drops when a pickaxe did the breaking; the wrong tool breaks the block and yields nothing.

src/main/java/com/example/example_mod/RubyOreBlock.java
package com.example.example_mod;

import net.minecraft.block.Block;
import net.minecraft.block.material.Material;

import java.util.Random;

/**
 * Ruby ore drops rubies, not itself, the same way diamond ore drops diamonds:
 * override getDroppedItemId (and optionally getDroppedItemCount). Because the
 * block is in the mineable/pickaxe tag, the drop only happens when a pickaxe
 * does the breaking; the wrong tool breaks the block and yields nothing.
 */
public class RubyOreBlock extends Block {

	public RubyOreBlock(int id) {
		super(id, Material.STONE);
	}

	@Override
	public int getDroppedItemId(int meta, Random random) {
		return ExampleMod.RUBY.id;
	}

	@Override
	public int getDroppedItemCount(Random random) {
		return 1 + random.nextInt(2); // 1 or 2 rubies
	}
}

That .mineable(RetroTool.PICKAXE) is varargs, pass as many tool kinds as you like. RetroTool lives in com.periut.retroapi.tag and has five constants: PICKAXE, AXE, SHOVEL, HOE, SWORD. A block in any mineable tag follows the modern rule: it drops only when a matching tool is held (unless you also chain .alwaysDrops()), and it's mined at that tool's speed.

Custom tool items that don't subclass the vanilla tool classes still work, declare their kind on the item builder with RetroItemAccess's .tool(RetroTool.PICKAXE), and the gating recognizes them.

ruby_ore
ruby_ore.png
download

The data side, copied from a modern pack

The same tag also loads from a data file, in the modern 1.21 format, no translation needed:

src/main/resources/data/example_mod/tags/block/mineable/pickaxe.json
{
  "values": [
    "example_mod:ruby_ore",
    "minecraft:glass"
  ]
}

The full format is supported: values, the replace flag, #refs to other tags, and optional entries written {"id": ..., "required": false}. A few things make these files safe to copy straight out of a modern pack:

  • Vanilla names resolve. "minecraft:glass" works because RetroAPI ships a built-in name table mapping modern block names to beta's blocks.
  • Unknown modern-only names don't crash, but they do warn. A texture name beta has no sprite for, or a #variable a model never resolves, is not fatal: the model still bakes and that face just shows the missing texture, so a copied file from a newer pack still loads. But RetroAPI now logs a warning naming the model and the unresolved value, so a typo or a genuinely-missing texture is visible in the log instead of a silent blank face.
  • Tags union. The code call and the data file both feed the same mineable/pickaxe tag, so re-adding ruby_ore in JSON is harmless; every mod's contributions merge.

Under the hood this is one generic tag engine you can use for anything, not just mining:

CallWhat it does
RetroTags.isIn(block, RetroTagKey.block("mineable/pickaxe"))is this block in that tag?
RetroTags.all(tag)every block currently in the tag
RetroTags.addToTag(tag, blocks...)add blocks to a tag from code

RetroAPI tags are namespace-blind by design: every namespace's file for the same tag path feeds one tag. A tags/block/mineable/pickaxe.json shipped by any mod, under any namespace, contributes to the single mineable/pickaxe tag. That is what makes vanilla-style data packs and cross-mod tagging line up without coordination.

Tool tiers: needs_iron_tool

Mineable tags gate which kind of tool; the needs_<tier>_tool family gates how good it has to be, exactly like modern Minecraft. A block in needs_iron_tool drops only when the breaking tool is iron tier or better, AND its kind still has to match the mineable tag. Both gates are enforced in the same harvest hook. The ladder is wood < stone < iron < diamond, and vanilla gold tools mine at wood tier, also like modern.

The block side has two equivalent ways to put a block in a needs_<tier>_tool tag, exactly like the mineable tags above: a data file or one call from code. They feed the same tag and union, so reach for whichever fits, no need for both.

The data way, a modern-format tag file. The showcase puts its ruby ore behind iron:

src/main/resources/data/example_mod/tags/block/needs_iron_tool.json
{
  "values": [
    "example_mod:ruby_ore"
  ]
}

The code way, one call in init() after the block is registered, no file at all. RetroToolTier.IRON.needsTag() hands back the very same needs_iron_tool tag key the file feeds, and RetroTags.addToTag drops the block into it:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// The CODE way to set a tool tier on a block: identical effect to listing ruby_ore in
// data/example_mod/tags/block/needs_iron_tool.json above, and the two union harmlessly.
// RetroToolTier.IRON.needsTag() is the needs_iron_tool tag key; addToTag puts the block
// in it, so the ore now drops only for an iron-or-better pickaxe. Runs after RUBY_ORE
// is registered above, exactly like .mineable(...) is the code twin of mineable/pickaxe.json.
com.periut.retroapi.tag.RetroTags.addToTag(
	com.periut.retroapi.tag.RetroToolTier.IRON.needsTag(), RUBY_ORE);

Pick one. The data file is the move when you are copying a modern pack or want the gating editable without recompiling; the code call is the move when the tier is a fixed property of a block you are already registering in init(), keeping the rule next to the block. The kind gate has the same pair: .mineable(RetroTool.PICKAXE) is the code twin of mineable/pickaxe.json, and it too sugars to a RetroTags.addToTag(RetroTool.PICKAXE.mineableTag(), block) you could write by hand. Now the item side: where does a tool's tier come from? Vanilla tools need nothing, any ToolItem subclass infers its tier from its material's mining level (ToolMaterial.getMiningLevel()), so an iron pickaxe is iron tier the moment the file loads. Custom tools that are plain Items declare a tier with one chainable builder call, and the kind right next to it:

ExampleMod.java, init(), the Ruby Pick
// Tool KIND and TIER, declared in one chain: this plain Item (no ToolItem
// subclass anywhere) counts as an iron-tier pickaxe for the tag system, so it
// harvests anything in mineable/pickaxe up through needs_iron_tool, including
// the ruby ore above. Vanilla tools never need this, they infer kind from
// their class and tier from their material. The handheld item model
// (retroapi/models/item/ruby_pick.json) gives it the diagonal in-hand pose.
RUBY_PICK = (Item) RetroItemAccess.create()
	.maxStackSize(1)
	.tool(com.periut.retroapi.tag.RetroTool.PICKAXE)
	.tier(com.periut.retroapi.tag.RetroToolTier.IRON)
	.register(id("ruby_pick"));

Like every builder method it returns the access object, so it slots anywhere in the chain, and it works from a constructor too: RetroItemAccess.of(this).tier(RetroToolTier.IRON). Both halves, the block's required tier and the tool's own tier, are introspectable: ask RetroTags.requiredTier(block) or RetroToolTier.of(item) yourself for custom harvest logic.

The showcase's starter kit hands you both halves of the demonstration: the stone pickaxe breaks ruby ore but drops nothing, while the ruby pick (or any iron-or-better pickaxe) drops 1 to 2 rubies.

Condensed dirt: a shovel block

The ruby ore is a pickaxe block; the same two gates work for any tool. Condensed dirt, the "your first block" example ported from fabric-docs, is the shovel counterpart: a compressed-dirt cube that mines with a shovel and, like the ore, only drops for an iron-tier tool. It is the plainest builder in the mod plus those two tag calls:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
CONDENSED_DIRT = RetroBlockAccess.create(Material.SOIL)
	.sounds(Block.GRAVEL_SOUND_GROUP)                       // crunchy, like gravel
	.strength(0.6f)
	.mineable(com.periut.retroapi.tag.RetroTool.SHOVEL)    // shovel is the matching tool
	.register(id("condensed_dirt"));

The texture is a single all-sides cube face, saved at the path below.

src/main/resources/assets/example_mod/textures/block/condensed_dirt.png

condensed_dirt
condensed_dirt.png
download

A block built with .texture(...) still ships three resource files: a blockstate that picks a model, a block model that wires the texture onto a cube, and an item-form model so the inventory icon renders. These are condensed dirt's three:

src/main/resources/assets/example_mod/retroapi/blockstates/condensed_dirt.json
{
  "variants": {
    "": { "model": "example_mod:block/condensed_dirt" }
  }
}
src/main/resources/assets/example_mod/retroapi/models/block/condensed_dirt.json
{
  "parent": "minecraft:block/cube_all",
  "textures": {
    "all": "example_mod:block/condensed_dirt"
  }
}
src/main/resources/assets/example_mod/items/condensed_dirt.json
{
  "model": {
    "type": "minecraft:model",
    "model": "example_mod:block/condensed_dirt"
  }
}

The shovel kind comes from the .mineable(SHOVEL) call right in the builder; the iron tier comes from the same needs_iron_tool tag the ruby ore uses, and either way of joining it works. The data way just adds a second line to the file the ore already shares:

src/main/resources/data/example_mod/tags/block/needs_iron_tool.json
{
  "values": [
    "example_mod:ruby_ore",
    "example_mod:condensed_dirt"
  ]
}

The code way is the same one-liner the ore could have used, swapping the block in:

src/main/java/com/example/example_mod/ExampleMod.java, in init()
// Same effect as adding "example_mod:condensed_dirt" to needs_iron_tool.json; pick whichever.
com.periut.retroapi.tag.RetroTags.addToTag(
	com.periut.retroapi.tag.RetroToolTier.IRON.needsTag(), CONDENSED_DIRT);

So a wooden or stone shovel breaks condensed dirt but drops nothing; an iron (or diamond) shovel drops the block. A pickaxe, even a diamond one, is the wrong kind and never drops it. Its inventory icon comes from a 26.1.2 item definition rather than a model, which is Chapter 16's story. Condensed dirt is also in the starter kit, with both a stone and an iron-tier tool, so you can feel both gates.

What we built

Three blocks: a plain textured cube, a class-backed cube with a face per side, and a sub-cube pipe with its own renderer. Between them they exercise the whole block builder, custom Block subclasses, the per-face texture API, bounds, non-opacity, and custom render types, and every one of them got a free BlockItem on register, persisted with stable ids, safe in vanilla saves. The ruby ore added one more modern convenience: tag-driven tool gating, copied straight from a 1.21 data pack.

Speaking of item forms: next we make items that aren't blocks at all, including one that makes you jump.