Positions & multiblocks

A position type, six directions with opinions, multiblock patterns drawn as ASCII, and the load pass that runs when the world is actually there.

Beta passes positions around as three loose ints. That's fine for one block. The moment a block cares about what is next to it, or is part of something larger, mod code fills up with world.setBlock(x + dx, y + dy, z + dz, …) and hand-written direction switches that are wrong in exactly one rotation. This page is the toolkit for that.

RetroVec3i, the block position b1.7.3 never had

An immutable integer vector. Every operation returns a new one, so it's safe to share, and equal coordinates are equals and hash alike, so it works as a HashMap key for machine networks and multiblock parts.

position maths
RetroVec3i pos   = RetroVec3i.of(x, y, z);
RetroVec3i above = pos.up();
RetroVec3i far   = pos.add(2, 0, -3).multiply(2).subtract(RetroVec3i.UP);

// and it can read and write itself
if (above.isAir(world)) above.setBlock(world, Block.TORCH);
int id = pos.blockId(world);
RetroBlockState state = pos.state(world);
BlockEntity be = pos.blockEntity(world);

// distances, without writing the arithmetic again
if (pos.distanceSquared(other) <= 64) { … }        // within 8 blocks
if (pos.isWithin(other, 3)) { … }                    // inside a 7×7×7 cube
int steps = pos.manhattanDistance(other);

RetroDirection, all six of them

The four-value RetroFacing from Directional blocks is the horizontal one a furnace uses. RetroDirection is the full six, with the offsets, rotations and face indices spelled out:

directions with opinions
RetroDirection facing = RetroDirection.fromPlacer(player);  // includes up/down
RetroDirection back   = facing.opposite();
RetroDirection right  = facing.rotateRight();               // clockwise from above

RetroVec3i inFront = pos.offset(facing);                   // one block along it
RetroVec3i reach   = pos.offset(facing, 3);                // three blocks along it

int face = facing.face();      // 0 down, 1 up, 2 north, 3 south, 4 west, 5 east
boolean flat = facing.isHorizontal();
char axis = facing.axis();      // 'X', 'Y' or 'Z'

// which face did that hit come from?
RetroDirection hit = RetroDirection.nearest(dx, dy, dz);

RetroFacing gained the same helpers, so horizontal-only blocks don't have to convert: opposite(), rotateLeft(), rotateRight(), offsetX(), offsetZ(), vector(), offset(x, y, z) and toDirection().

The face indices are the same numbers getTexture(side, meta) and the render context are asked about, so facing.face() is directly usable when you're deciding what a face looks like.

Multiblocks: draw the shape, don't code it

A multiblock is a shape plus a test. Written by hand, the test is a page of neighbor checks; written twice (once per rotation) it's a page of bugs. RetroMultiblock takes the shape as ASCII layers and a legend, the way you'd sketch it on paper:

src/main/java/com/example/example_mod/ExampleMultiblocks.java
public static final RetroMultiblock FURNACE_TOWER = RetroMultiblock.builder()
    .layer("BBB",             // y = 0, bottom layer, rows run north → south
           "BCB",
           "BBB")
    .layer(" F ",             // y = 1
           "F F",
           " F ")
    .where('B', Block.BRICKS)
    .where('C', ExampleMod.CORE_BLOCK)
    .where('F', Block.IRON_BLOCK)
    .anchor('C')               // the cell you match FROM (usually the controller)
    .build();

Then ask whether it's standing. matchAnyRotation tries all four horizontal facings, so the player can build it whichever way they like:

in the core block entity
RetroMultiblock.Match match = ExampleMultiblocks.FURNACE_TOWER.matchAnyRotation(world, x, y, z);
if (match != null) {
    RetroFacing facing = match.facing();              // which way it was built
    for (RetroVec3i part : match.positions()) { … }    // every cell it covers
    for (RetroVec3i frame : match.positions('F')) { … } // just the iron frame
}
Builder callMeaning
.layer("…", "…")one horizontal layer, bottom first; rows north→south, columns west→east
.where('B', block)this character means that block, any metadata
.where('B', state)…that exact block state
.whereAny('B', a, b, c)…any of these blocks
.where('B', predicate)…whatever you can express as a test
' ' (space)don't care, matched by default and never included in positions()
RetroMultiblock.AIRmust be empty (an interior a player can stand in)
.anchor('C')which cell match(world, x, y, z) is called on

A match can also form the structure for you, match.fill(world, block, meta) replaces every matched cell, which is the usual "the structure snaps together and becomes active" step.

The load pass: doing things when the world is actually there

Multiblocks want to rebuild themselves after a world loads, and that's where beta trips you. A block entity's readNbt runs while the chunk is still being deserialized: there is no world on it yet, the neighbors may not exist, and touching the world from there either throws or reads a half-built chunk. The usual workaround is a hand-rolled "have I initialized yet?" boolean in every block entity.

RetroAPI ships that boolean once, for everyone:

src/main/java/com/example/example_mod/ExampleMod.java, initRetro()
BlockEntityLoadedCallback.EVENT.register(blockEntity -> {
    if (blockEntity instanceof ExampleCoreBlockEntity core) {
        core.rebuildStructure();   // safe: world and neighbors exist
    }
});

It fires once per block entity, the first time it ticks in a world, on both sides. That's the earliest moment at which "all the NBT is read and the world is real" is true.

Block entities that never tick never fire it, by design, an inert container has nothing to rebuild. If yours needs the pass, give it a tick method (an empty one is fine).

Next: proving it all works, on a client and on a real server.