Chapter 3, Pixels & Words

Where your textures, names, and sounds come from, and the folder layout that makes it all automatic.

Code registers what exists; assets decide what it looks, reads, and sounds like. The good news in this chapter is how little wiring there is: drop files in the right folders with the right names and RetroAPI finds them. The folder names are the API.

The assets folder, top to bottom

Everything lives under one root, and that root's name must equal your mod id, the same example_mod from fabric.mod.json. Here is the showcase's tree:

src/main/resources/assets/example_mod/
assets/example_mod/
├── icon.png                          # the mod's icon (fabric.mod.json points here)
├── lang/
│   └── en_US.lang                    # display names & achievement text
├── textures/
│   ├── block/
│   │   ├── example_block.png
│   │   ├── sided_block_top.png
│   │   ├── sided_block_bottom.png
│   │   ├── sided_block_side.png
│   │   ├── counter_block.png
│   │   ├── crate_block.png
│   │   ├── freezer_front.png
│   │   ├── freezer_side.png
│   │   └── freezer_top.png
│   ├── item/
│   │   └── suspicious_substance.png
│   └── gui/
│       ├── crategui.png
│       └── freezer.png
└── sounds/
    └── sound/
        └── counter/
            └── click.ogg

If textures don't show up, the first thing to check is that the folder is named exactly your mod id. assets/ExampleMod/ or assets/example-mod/ will silently load nothing.

Block & item textures: atlased sprites

The simplest case is the one from the elevator pitch, a single texture chained onto the builder:

in ExampleMod.init()
EXAMPLE_BLOCK = RetroBlockAccess.create(Material.STONE)
	.texture(id("example_block"))   // → textures/block/example_block.png on all 6 faces
	.register(id("example_block"));

That .texture(id("example_block")) loads assets/example_mod/textures/block/example_block.png onto all six faces of the block. Items follow the parallel path: .texture(id("suspicious_substance")) loads textures/item/suspicious_substance.png. The convention is just folder-by-kind: blocks read from textures/block/, items from textures/item/.

These textures are atlased. Beta's renderer doesn't bind a texture per block, it draws from one big sheet (terrain.png for blocks, gui/items.png for items) and addresses each tile by a sprite index. Vanilla fills the low indices; RetroAPI assigns your sprites slots ≥ 256 and composites your PNGs onto the sheet client-side at load. Under StationAPI (if present) it re-resolves your sprites into StationAPI's atlas instead, via RetroTextures.resolveStationAPITextures(), you write the same code either way.

The handle these calls track is a RetroTexture, and its public int id field is the live sprite index, the number the renderer hands the tessellator. You'll use it directly in the next section and in Chapter 4's sided block.

Remember from Chapter 2: reserving these slots is pure bookkeeping, so it runs on a dedicated server too. Only the client ever decodes the PNG and paints the atlas.

example_block
example_block.png
download
counter_block
counter_block.png
download
crate_block
crate_block.png
download
freezer_front
freezer_front.png
download
suspicious_substance
suspicious_substance.png
download

Multi-texture blocks

A block with a different face per side calls RetroTextures.addBlockTexture(id) once per texture and overrides getTexture(face) to return the right sprite index. That's a whole topic, the sided block, with its three textures and the RetroTexture.id trick, and it gets a full walkthrough in Chapter 4. Pointing you there rather than repeating it.

Reusing vanilla sprites, no PNG needed

Sometimes the texture you want already ships with the game. You can borrow a vanilla sprite index two ways:

  • On a block builder, .sprite(Block.GLASS.getTexture(0)), the portal block in Chapter 4 does this to look like glass, shipping no texture of its own.
  • On an item subclass, override getTextureId(damage) to return another item's index. The Jump Stick in Chapter 5 returns Item.STICK.getTextureId(0) and so needs no art at all.

Animated textures

Textures animate now. Drop a {name}.png.mcmeta next to the PNG and stack the frames as square tiles in a vertical strip (a 16×64 image is four 16×16 frames), and the ordinary .texture() call picks the animation up with no other change. If you'd rather drive it from code, RetroTextures.addAnimatedBlockTexture(id, frameCount, ticksPerFrame) and addAnimatedItemTexture(id, frameCount, ticksPerFrame) do the same in one line. The full schema, interpolation, and the showcase's ember block live in Chapter 16.

The same path works for items: the showcase's Spectre Staff is a plain RetroItemAccess item whose texture is a 16×208 strip (thirteen 16×16 frames) with a spectre_staff.png.mcmeta beside it. Its mcmeta also shows the per-frame timing form, a frames list of indices with one entry written as {"index": 8, "time": 9} to hold that frame longer, so an animated item is nothing more than the strip plus its mcmeta and a normal .texture(id("spectre_staff")) call:

{
    "animation": {
        "frametime": 3,
        "frames": [0, 1, 2, 3, 4, 5, 6, 7, {"index": 8, "time": 9}, 9, 10, 11, 12]
    }
}

GUI textures: bound directly, not atlased

Window art is different. A GUI background is too big to live in the terrain atlas, so it isn't atlased at all, you bind it straight from its classpath path through the vanilla texture manager. Here's the crate screen's background draw:

src/main/java/com/example/example_mod/ExampleCrateScreen.java, trimmed
private static final String TEXTURE = "/assets/example_mod/textures/gui/crategui.png";

@Override
protected void drawBackground(float tickDelta) {
	int texture = this.minecraft.textureManager.getTextureId(TEXTURE);
	GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
	this.minecraft.textureManager.bindTexture(texture);
	int left = (this.width - this.backgroundWidth) / 2;
	int top = (this.height - this.backgroundHeight) / 2;
	this.drawTexture(left, top, 0, 0, this.backgroundWidth, this.backgroundHeight);
}

textureManager.getTextureId(path) loads the file (cached after the first call) and returns an OpenGL texture handle; bindTexture makes it current; drawTexture blits a region of it to the screen. Note the leading slash and the full /assets/<modid>/... path, this is a raw classpath lookup, so it must be exact.

Both GUI sheets are 256×256. The crate window uses the top-left 176×222 region; the rest of the sheet is free space. The freezer reuses the vanilla furnace layout, which means it keeps its progress-bar sprites at u ≥ 176, the same column past the 176-pixel-wide window where vanilla's furnace.png stores the flame and arrow sprites it blits over the GUI as fuel burns. The freezer's animated bars are Chapter 6's story.

crategui
crategui.png
download
freezer
freezer.png
download

Lang files: names without code

RetroAPI loads assets/<modid>/lang/en_US.lang automatically (and, if StationAPI is present, also assets/<modid>/stationapi/lang/en_US.lang). Here is the showcase's, in full:

src/main/resources/assets/example_mod/lang/en_US.lang
# Display names for the mod's content. RetroAPI loads this file automatically
# (assets/<modid>/lang/en_US.lang). The @ is replaced with your mod id at load
# time, so these lines work even if you rename the mod.
#
# Anything you don't translate gets an auto-generated default from its
# identifier (example_block -> "Example Block"), so missing lines never crash.

# Blocks: tile.<modid>.<identifier>.name
tile.@.example_block.name=Example Block
tile.@.sided_block.name=Sided Block
tile.@.example_pipe.name=Example Pipe
tile.@.counter_block.name=Counter Block
tile.@.crate_block.name=Crate
tile.@.freezer_block.name=Freezer
tile.@.example_portal.name=Example Portal

# Items: item.<modid>.<identifier>.name
item.@.suspicious_substance.name=Suspicious Substance
item.@.jump_stick.name=Jump Stick

# Achievements: achievement.<name> is the title, achievement.<name>.desc the
# description, where <name> is the string passed to RetroAchievements.register.
achievement.example_mod.getting_started=Getting Started
achievement.example_mod.getting_started.desc=Join a world with the example mod installed.
achievement.example_mod.jump_for_joy=Jump for Joy
achievement.example_mod.jump_for_joy.desc=Jump!
achievement.example_mod.dimensional_traveler=Dimensional Traveler
achievement.example_mod.dimensional_traveler.desc=Step through the example portal.

# Title of the mod's own achievement page (the < > buttons on the achievements screen).
gui.retroapi.achievementPage.example_mod.example_page=Example Mod

A few things worth pinning down:

  • The @ is replaced with your mod id at load time. Write tile.@.example_block.name and it becomes tile.example_mod.example_block.name. Rename the mod and the lang file follows along untouched.
  • Key formats: tile.<modid>.<identifier>.name for blocks, item.<modid>.<identifier>.name for items, achievement.<name> and achievement.<name>.desc for achievements, and gui.retroapi.achievementPage.<namespace>.<identifier> for a custom achievement page title.
  • Missing lines never break anything. Anything you don't translate gets an auto-generated default derived from its identifier, example_block becomes "Example Block", so a half-finished lang file shows readable placeholders, not raw keys.

Sounds: the autoloader

Custom sounds need no registration call at all. RetroAPI scans assets/<modid>/sounds/<channel>/<path>.ogg and derives an event id from the path. The channels are sound/ (the usual effects), music/, and streaming/, plus a stationapi/ root scanned the same way when StationAPI is present.

The id derivation, step by step:

  1. Strip the .ogg extension.
  2. Turn directory slashes into dots.
  3. Lowercase the whole thing.
  4. For the sound/ channel only, collapse a trailing digit so numbered variants share one event, call1.ogg and call2.ogg both register the event call and play a random one.
  5. Prefix <modid>:.

The showcase ships one sound: sounds/sound/counter/click.ogg. Walk it through, strip the extension (counter/click), slashes to dots (counter.click), lowercase (unchanged), prefix the mod id, and you get the event id example_mod:counter.click. The counter block plays it on every right-click:

src/main/java/com/example/example_mod/ExampleCounterBlock.java, the play line
world.playSound(x + 0.5, y + 0.5, z + 0.5, "example_mod:counter.click", 1.0F, 1.0F);

The two floats are volume and pitch. Offset by +0.5 on each axis to play from the block's center.

🔊 click.ogg
→ example_mod:counter.click
download

That's pixels, names, and sounds handled, the quiet half of every feature. Now the loud half: let's build some blocks.