Skip to main content

Tutorial 6 Multiblocks and color coded teleportation

This chapter adds two multiblocks to the Minecraft 1.21.1 project. A multiblock is a group of ordinary blocks that together act as one machine. Our teleporter forms when the player right-clicks a completed pattern. Our receiver forms automatically when its last construction block is placed. Both use three colors to identify a destination, with all interaction happening in the world.

The examples deliberately use fixed patterns and small helper classes. Start with Multiblocks, follow the two block callbacks into it, and then follow the teleporter ticker into ReceiverSavedData.

Build and try the two structures​

Get 17 Teleporter Bases and 9 Receiver Bases from the Tutorial Mod creative tab, and 9 vanilla glass blocks from Minecraft's creative inventory. The mod tab also contains the Tutorial Block, Generator, and Pig Spawner from the earlier chapters. Survival recipes produce eight base blocks from eight iron ingots around an ender pearl or redstone. The construction roof uses ordinary glass blocks.

Build the teleporter in four layers. Each diagram is a top view, and each cell is one block position. B is a Teleporter Base, G is a vanilla glass block, and . is air.

Layer y+3 Layers y+1 and y+2 Layer y
G G G B . B B B B
G G G . . . B B B
G G G B . B B B B

The platform contains nine blocks. The four corner pillars contain another eight blocks, two above each corner. Use vanilla glass blocks for the nine roof positions and leave both interior layers empty. Right-click any of the 17 Teleporter Bases with an empty main hand to form the machine. Holding a block item lets you place it normally against a base without sneaking.

An invalid pattern produces a chat message and leaves the blocks intact. A valid pattern changes every construction position to a state with formed=true, removes the nine glass blocks, replaces the center with a Teleporter, and replaces one of the four directly adjacent platform blocks with a Destination Controller. The controller occupies the position closest to the player and faces outward. Stand on the side where you want its controls before forming.

Build a separate 3×3 platform of Receiver Bases. On the next server tick after the last placement, its center becomes a Receiver and its other eight blocks become formed platform pieces. Leave a gap between separate platforms so each 3×3 pattern is unambiguous. A larger solid area can contain several possible patterns; the first complete unformed square found wins.

The controller has three small color squares on its outward face. The receiver has three squares on top, where they are accessible even though it is the center of a low platform. Aim at a square and right-click with a dye or a piece of vanilla colored wool. The corresponding color changes without consuming the dye or wool, in both survival and creative. These are color indicators, not inventory slots.

The first color square has a gold frame on both blocks; the other two frames are dark. The controller's code reads left to right when facing its controls. Read the code from the gold-framed square toward the opposite end, even when viewing the receiver from the other side. The gold frame stays the same when the square's dye color changes.

All three squares initially show white. The order matters: red / blue / white and white / blue / red are different destinations. Set the same ordered colors on the controller and receiver, then walk onto the center of the teleporter. Players, mobs, and other entities can travel. A receiver in another dimension works too; arrival uses its actual coordinates without Nether coordinate scaling. Several receivers with the same code provide random destinations.

Breaking any of the teleporter's 17 structural blocks restores all surviving members to unformed Teleporter Bases. Breaking any of the receiver's nine blocks similarly restores Receiver Bases and removes that receiver from the saved directory. The removed block stays removed. Special blocks drop their original base item, so players never need to craft the internal blocks. The nine glass roof blocks are restored when a teleporter is dismantled. If a roof position is occupied, its glass is dropped as an item instead, preserving the block there. To rebuild a teleporter, replace the missing block, clear any roof obstructions and replace their glass, then right-click again. Forming a new structure creates new color data, starting at white / white / white.

Register construction blocks and internal blocks​

ModBlocks registers the two construction bases and three internal blocks: Teleporter, Destination Controller, and Receiver. Only the construction bases get block items and creative-tab entries. The roof uses Blocks.GLASS. ModBlockEntities registers a Teleporter BE type and one shared color-controller BE type, valid for both Destination Controller and Receiver.

Every structural block extends MultiblockBlock and shares these properties:

PropertyMeaning
formedWhether this position belongs to a formed structure
partPlatform, beveled corner, lower pillar, or upper pillar
facingRotation of the local model; on the teleporter, the direction of its controller

The boolean is the important gameplay state. The other properties let a normal block model draw a different piece at each position. A construction base starts as a full cube with formed=false. Formation retains the original base block at ordinary member positions and assigns its visual role. The center and controller positions become different block types because they need block entities and additional behavior.

Put all tutorial items in one creative tab​

ModCreativeTabs registers a CreativeModeTab through a deferred register on the mod event bus. Its builder supplies the translated title Tutorial Mod, a Teleporter Base icon, and a displayItems callback listing the five mod block items. The callback includes the earlier Tutorial Block, Generator, and Pig Spawner alongside both multiblock construction bases. Internal formed blocks have no item form and stay out of the tab.

TutorialMod registers this helper with the other deferred registers. The old listener that populated Minecraft's Building Blocks tab is replaced by the custom tab's contents callback. The title translation comes from ModLanguageProvider. NeoForge's creative tab guide describes the builder and registration hooks.

Compare manual and automatic formation​

The manual path begins in MultiblockBlock.useWithoutItem. The server calls Multiblocks.formTeleporter only for an unformed Teleporter Base when the player's main hand is empty. Despite its name, Minecraft can call useWithoutItem while the player holds an item, so the empty-hand check is explicit. useItemOn returns SKIP_DEFAULT_BLOCK_INTERACTION for a held item, allowing its normal use or placement to proceed. An empty stack returns PASS_TO_DEFAULT_BLOCK_INTERACTION to reach the formation callback. Building against the bases therefore requires no sneaking.

A clicked base may be on the platform or either pillar level. The helper tries candidate centers within one block horizontally and zero to two blocks below the clicked position. Each candidate must have exactly the required 17 unformed base blocks, nine vanilla glass blocks, and a clear interior. Validation completes before any block changes. This avoids partially forming a machine when the final checked position is wrong.

The controller direction is the closest of north, east, south, and west, measured from the player's position to each adjacent block's center. Candidate order provides a consistent tie-break. Once a candidate passes validation, form assigns states, consumes the roof, places the controller, and places the teleporter center.

The automatic path starts in MultiblockBlock.onPlace. An unformed Receiver Base schedules a block tick one tick later. Its tick callback calls Multiblocks.formReceiver, which tries nearby centers for a full 3×3 square of unformed Receiver Bases. It uses the same form helper with the receiver flag. The delay keeps pattern changes out of the block-placement callback and lets the last placement finish first. No player interaction or failure message is needed; an incomplete receiver simply waits for another placement.

These are two different triggers for the same idea: find a candidate, validate all members, then change the structure together. There is no need for a general pattern language to explain a pair of fixed tutorial structures.

Dismantle without recursive removal​

MultiblockBlock.onRemove watches formed members. Replacing one formed member with another formed member is permitted because formation itself replaces the center and controller. Removing a formed member, or restoring it to an unformed state, calls Multiblocks.memberRemoved.

The helper looks for the relevant center and checks that the removed position belongs to its pattern. If the removed block was the center itself, its old state identifies which structure to dismantle. The receiver's GlobalPos is removed from the directory first, and the surviving center is reset before the other members are restored.

Restoring blocks also calls onRemove. A small set of centers currently being dismantled prevents those callbacks from starting the same operation again; try/finally always clears the guard. The removed position is skipped so teardown does not recreate the broken block. This works for block destruction, explosions, and ordinary replacement through the world's block-change API.

After restoring the teleporter's members, teardown restores its nine glass roof blocks. Only air is replaced; occupied roof positions refund their glass as items. Receivers have no roof to restore.

The pattern searches and teardown read a small fixed area, including nearby chunks when necessary. That keeps structures crossing a chunk boundary consistent. A large multiblock system might instead store explicit ownership links for each member; here the bounded search keeps the example easier to follow.

Set three colors through world interactions​

ColorControllerBlockEntity stores three fields of type DyeColor: color1, color2, and color3. They all start at DyeColor.WHITE. The three getters expose the fields directly, while getColor(slot) selects a field for the renderer and setColor(slot, color) updates the chosen field. The slot indices are 0, 1, and 2, corresponding to fields 1, 2, and 3.

ColorControllerBlock.useItemOn accepts a DyeItem or a vanilla wool block item. Dyes expose their color directly. For wool, colorFromItem checks ItemTags.WOOL, reads the block's registry name, and extracts the color from the vanilla <color>_wool naming pattern. DyeColor.byName resolves that name; the minecraft namespace check keeps this limited to vanilla wool.

ColorControllerBlock.hitSlot converts the world hit point into coordinates relative to the clicked block and undoes its facing rotation. For each clockwise quarter turn of the model, it transforms (x, z) to (z, 1 - x) to return the click to the default north-facing coordinates. The renderer rotates the wool cubes by -90 * turns degrees around Y to match the baked model. The same three horizontal centers are used for every orientation: 12, 8, and 4 pixels across the default model's 16-pixel x axis, so the controls read left to right when viewed from north. Front controls also check the clicked face and vertical band; receiver controls check the top face and their depth band. Clicking elsewhere does not paint a slot.

ColorControllerBlockEntity.setColor ignores invalid slot indices and colors that already match the chosen slot. On the server, a change updates one field, calls setChanged, updates the saved directory if this is a receiver, and sends a block update to nearby clients. The held item stays unchanged. The BE saves each color's numeric ID under color1, color2, and color3 in saveAdditional. DyeColor.getId() supplies a value from 0 to 15, so each field fits in one NBT byte. loadAdditional reads the bytes with DyeColor.byId, defaulting to white for missing or invalid IDs. getUpdateTag supplies colors when a client loads the chunk, while getUpdatePacket supplies later changes.

ColorControllerRenderer draws three tiny wool cubes using the existing block renderer. Their positions match the hit-test coordinates and the dark frames in the baked models. The receiver cubes sit on top of the platform; controller cubes face outward. The renderer is registered in the existing client-only subscriber, keeping rendering classes away from a dedicated server. This is world BE synchronization, as in the pig preview from tutorial 5; there is no menu or screen.

Save a directory shared by all dimensions​

ReceiverSavedData holds a map from GlobalPos to a ReceiverColors record. The record stores three DyeColor values as an immutable snapshot, so it stays independent of the receiver block entity. Record equality compares all three colors in order. GlobalPos combines a dimension key with a block position. We keep one directory in the Overworld's data storage so every dimension uses the same object. Minecraft loads or creates it through computeIfAbsent; changes call setDirty so they will be written to disk. This follows the NeoForge SavedData API.

Each saved entry contains the dimension identifier, packed block position, and the three color IDs as bytes. The full GlobalPos is the map key, so the same coordinates in different dimensions identify separate receivers. Recoloring replaces a receiver's color snapshot instead of leaving its old destination behind. Different positions may store the same three colors. Formation registers three white colors, and onLoad registers the BE's saved colors when its chunk loads again. Unloading a chunk does not remove the entry. Dismantling does.

randomReceiver collects matching positions and randomly selects a candidate. It loads the selected receiver's chunk and checks that the receiver BE and its three colors still match. A stale location is removed; a mismatched color snapshot is repaired. If a candidate is stale, the method tries another matching position. Valid receivers therefore remain usable even when their chunks were unloaded before the request, without keeping all receiver chunks permanently loaded.

Detect arrivals and teleport on the server​

TeleporterBlock supplies a BE ticker only on the logical server. TeleporterBlockEntity.serverTick finds its controller through the teleporter's facing, reads its three colors, and looks for entities intersecting the small region just above the center platform. Checking the entity's feet excludes entities high above the pad. Passengers travel through their root vehicle's dimension transition rather than independently starting another teleport.

The platform is half a block high, so arrival is the receiver position plus (0.5, 0.5, 0.5). Before moving an entity, the ticker checks the destination world border, collision, and liquid. A blocked receiver gives players a message and leaves the entity at the source. Selection chooses a receiver before this space check; a later retry may choose another matching receiver.

Entity.changeDimension with a DimensionTransition handles both local moves and transfers between dimensions, including the special server-player path. The transition preserves look direction, resets movement, and adds the vanilla portal chunk ticket. After success, fall distance is cleared and the receiver plays SoundEvents.PORTAL_TRAVEL, the same sound event used for Nether travel. A cross-dimension mob may be recreated as a new Java object, so code after the transition uses the returned entity.

If there is no matching receiver, a player sees a message containing the three colors; other entities remain silently at the source. Attempts are limited to once per second per entity while it stays on the pad. The retry map discards entities that leave, preventing unbounded memory growth and messages every tick.

Generate the platform and roof models​

ModMultiblockModelProvider creates the new blockstates and models. It has a unique provider name so it can run alongside the earlier model provider. Construction bases are full cubes with different top textures. Formed bases select a model from their part and rotate it with facing.

The full structure follows the supplied sketch: a low platform with beveled corners, a colored center pad, slim corner pillars, a roof, and three controls on one side. Four small cuboid steps approximate each diagonal corner of the octagonal footprint. MultiblockShapes uses the same dimensions for selection and collision, so the platform is half height and the pillars are narrow. The teleporter uses a separate corner model with a short 4×4-pixel support from platform height to the top of the corner block. This bridges the gap between the half-height base and the pillar above; its collision shape includes the same support. Receiver corners use the plain beveled model. Existing formed teleporter corner states also select the new support model, so they connect correctly after reloading the client.

Each upper pillar model extends above its block and inward to draw one quarter of the roof. Four quarters make a complete 3×3 roof after the glass construction blocks disappear. Each quarter is split into pieces no wider than 16 pixels, with explicit UV coordinates between 0 and 16. This keeps the iron texture at its normal scale and prevents the overhanging geometry from sampling neighboring textures in the atlas. This roof is decorative and has no separate collision or block position to break. Its supporting pillars remain real, breakable member blocks. Models use vanilla textures, so this chapter needs no new bitmap art.

Loot generation maps each internal block back to its construction item. Recipe, translation, and pickaxe-tag providers are extended alongside the new model provider. Run data generation after changing these providers; generated JSON files are included in the mod resources.

Run the checks and record the demonstration​

From this project, run:

./gradlew runData
./gradlew build
./gradlew runGameTestServer -Pexclude_optional_mods
./gradlew runClient

On Windows, use gradlew.bat. The headless suite includes the five existing machine tests and fourteen multiblock tests. The new tests cover invalid patterns, closest controller selection, roof removal and restoration, occupied roof refunds, pillar support collision, held-item placement without sneaking, empty-hand formation, all 17 teleporter removal positions, automatic receiver formation, coloring with dyes and wool, rotated hit coordinates, BE synchronization and serialization, duplicate receiver selection, directory serialization, recoloring and removal, blocked arrivals, player messages, server-player teleportation, and mob travel between dimensions.

The cross-dimension mob test observes EntityJoinLevelEvent to capture the recreated entity, then checks its type, UUID, destination position, and removal of the original. A newly loaded destination chunk may not yet expose entities through getEntity, so an immediate lookup would make the check depend on chunk timing. The listener is always unregistered in finally. Both versions use this approach.

The larger multiblocks.snbt arena lives in src/gameTest/resources and stays out of the published jar.

For an in-game demonstration, build an incomplete teleporter first and show its failure message. Add the missing roof glass block, stand on your preferred control side, and form it. Show the changed platform, pillars, roof, and controller. Build a receiver by placing its ninth block and point out that it forms without a click. Paint matching codes and teleport a player, a pig, and a dropped item. Then demonstrate two receivers with the same code, a code with no receivers, a blocked destination, and dismantling by breaking a pillar or receiver edge. Save and reload the world to demonstrate retained colors and destination data.

Inspect the iron-textured roof and its UVs, the pillar supports, control alignment, all four controller orientations, and the portal sound in the client. The construction glass disappears on formation and returns on teardown. Server GameTests verify the behavior, while the client makes the appearance and sound visible and audible.

Minecraft 26.2 differences​

The 26.2 companion project implements the same structures, three-color codes, formation rules, and teleportation behavior with Minecraft 26.2, NeoForge 26.2.0.75, and Java 25. Tutorials 1–5 already cover many of the shared API changes. This section applies them to the multiblocks and explains the new receiver-directory and teleportation APIs. These are differences between the two projects; they did not all first appear in 26.2.

Area1.21.1 tutorial26.2 companion
Block registrationPrebuilt BlockBehaviour.Properties; BlockEntityType.Builder.of(...).build(null)Transform the supplied properties; new BlockEntityType<>(factory, blocks...)
Facing and common namesDirectionProperty, ResourceLocation, ResourceKey.location(), level.isClientSideEnumProperty<Direction>, Identifier, ResourceKey.identifier(), level.isClientSide()
Item interactionItemInteractionResult; PASS_TO_DEFAULT_BLOCK_INTERACTION / SKIP_DEFAULT_BLOCK_INTERACTIONInteractionResult; TRY_WITH_EMPTY_HAND / PASS
Dye and messagesDyeItem.getDyeColor(); displayClientMessageStack DataComponents.DYE; sendOverlayMessage / sendSystemMessage
Member removalonRemove receives the replacement stateaffectNeighborsAfterRemoval reads the replacement from the level
Color BE persistenceCompoundTag and registry accessValueOutput / ValueInput
Receiver directorySavedData.Factory, manual tag loading/savingCodec<ReceiverSavedData>, SavedDataType, computeIfAbsent(TYPE)
TeleportationchangeDimension(new DimensionTransition(...))teleport(new TeleportTransition(...))
Color rendererImmediate BER render with a buffer sourcecreateRenderState, extractRenderState, and submit with BlockModelRenderState
Model generationSeparate NeoForge BlockStateProvider for multiblocksHelper sharing the main ModelProvider collectors
GameTestsAnnotations and multiblocks.snbtExplicit test/function registration and data/tutorialmod/structure/multiblocks.nbt

Registration, empty-hand formation, and coloring​

ModBlocks.multiblockProperties now takes a BlockBehaviour.Properties argument and returns that same object after setting map color, strength, sound, and noOcclusion. Pass it to registerBlock as ModBlocks::multiblockProperties. This follows Tutorial 2's registration pattern and lets registration assign the block's registry ID. Both new BE types use direct BlockEntityType construction; the color-controller type still lists both Destination Controller and Receiver as valid blocks. The custom creative tab uses the same deferred registration and displayItems callback in both versions.

MultiblockBlock.useItemOn now returns InteractionResult:

return stack.isEmpty() ? InteractionResult.TRY_WITH_EMPTY_HAND
: InteractionResult.PASS;

TRY_WITH_EMPTY_HAND reaches the block's empty-hand interaction, while PASS lets the held item continue its normal use or placement. Keep the explicit player.getMainHandItem().isEmpty() check in useWithoutItem. Together these preserve formation with an empty main hand and ordinary block placement against bases without sneaking. For a handled formation or color click, return InteractionResult.SUCCESS on the client and SUCCESS_SERVER on the server; the old sidedSuccess helper is gone.

ColorControllerBlock.colorFromItem reads a dye with stack.has(DataComponents.DYE) and stack.get(DataComponents.DYE) instead of calling DyeItem.getDyeColor(). Vanilla wool still uses its tag and minecraft:<color>_wool registry name. When resolving wool for rendering, BuiltInRegistries.BLOCK.getValue(identifier) returns the actual block; get(identifier) now returns an optional holder. Formation success and missing/blocked-destination messages use sendOverlayMessage, replacing displayClientMessage(..., true). Invalid-pattern messages use sendSystemMessage, replacing displayClientMessage(..., false), and appear in chat.

Other small substitutions are level.getRandom() for the random source and Vec3.atCenterOf(pos) for a block position's center. Horizontal facing now has type EnumProperty<Direction> because DirectionProperty was removed.

Move teardown to the removal callback​

The old Block.onRemove(state, level, pos, nextState, moving) override is gone. The 26.2 member block overrides a server-only callback and obtains the replacement state from the world:

@Override
protected void affectNeighborsAfterRemoval(BlockState state, ServerLevel level,
BlockPos pos, boolean moving) {
super.affectNeighborsAfterRemoval(state, level, pos, moving);
BlockState next = level.getBlockState(pos);
if (state.getValue(FORMED)
&& !(next.getBlock() instanceof MultiblockBlock && next.getValue(FORMED))) {
Multiblocks.memberRemoved(level, pos, state);
}
}

The formed-to-formed check remains essential: formation replaces bases with different block types at the center and controller. Treating those replacements as destruction would dismantle the structure while building it. Keep the center-first restoration and recursive teardown guard in Multiblocks, along with skipping the removed position and restoring or refunding the roof glass. Breaking any structural member still dismantles the whole machine.

Save colors and the shared receiver directory​

ColorControllerBlockEntity.saveAdditional(ValueOutput) writes the same three byte fields with putByte. loadAdditional(ValueInput) reads them with getByteOr, defaulting missing values to white before applying DyeColor.byId. The three field names remain color1, color2, and color3. getUpdateTag still uses saveWithoutMetadata(registries), and getUpdatePacket still creates a ClientboundBlockEntityDataPacket, so nearby clients receive all three colors without a menu.

ReceiverSavedData needs a larger change. Replace its SavedData.Factory and manual tag-loading/saving methods with a codec and a namespaced type:

private static final SavedDataType<ReceiverSavedData> TYPE = new SavedDataType<>(
Identifier.fromNamespaceAndPath("tutorialmod", "receivers"),
ReceiverSavedData::new, CODEC);

Retrieve it with level.getServer().overworld().getDataStorage().computeIfAbsent(TYPE). Storage still belongs to the Overworld, so all dimensions share one directory, and changes still call setDirty.

The codec converts the map to a receivers list. Each entry stores a dimension key through ResourceKey.codec(Registries.DIMENSION), a packed position through Codec.LONG, and three dye IDs through DyeColor.LEGACY_ID_CODEC. The color fields use optionalFieldOf(..., DyeColor.WHITE), so white values may be omitted on disk. Entries remain separate locations: two receivers with the same code must survive a save/load round trip as two possible destinations.

The namespaced ID changes the file from data/tutorialmod_receivers.dat to data/tutorialmod/receivers.dat inside the Overworld's dimension directory. This companion port does not migrate 1.21.1 world saves. The matching color field names alone do not provide directory migration.

Teleport with a transition and use the returned entity​

Replace Entity.changeDimension(DimensionTransition) with Entity.teleport(TeleportTransition):

Entity moved = entity.teleport(new TeleportTransition(target, arrival, Vec3.ZERO,
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET));
if (moved != null) {
moved.resetFallDistance();
target.playSound(null, destination.pos(), SoundEvents.PORTAL_TRAVEL,
SoundSource.BLOCKS, 0.25F, 1.0F);
}

The transition still supplies the receiver's actual coordinates, zero velocity, the entity's look direction, and the portal-ticket callback. This API handles local moves, server players, and cross-dimension entity recreation. Continue using the returned entity after success, since a mob may have become a new object. Use resetFallDistance() to clear fall distance. Keep the border, collision, and liquid checks before teleporting, and keep the per-entity retry throttle and portal-travel sound.

Extract the three wool models before submitting them​

ColorControllerRenderer now implements BlockEntityRenderer<ColorControllerBlockEntity, ColorControllerRenderer.State>. Its State extends BlockEntityRenderState and holds the facing rotation, receiver flag, and three BlockModelRenderState objects.

createRenderState creates this snapshot. During extractRenderState, call the super implementation to populate shared data such as light, read the BE's facing and colors, and use the BlockModelResolver from context.blockModelResolver() to fill each wool model state. The subsequent submit callback reads only that snapshot. It applies the original rotation, cube positions, and scale, then calls each model state's submit with the SubmitNodeCollector, extracted lightCoords, and OverlayTexture.NO_OVERLAY.

This follows Tutorial 5's renderer-state pattern, with block model states in place of the pig's entity render state. Keeping the original transforms keeps the visible cubes aligned with the clickable slots on the controller's front and the receiver's top in all four orientations.

Share model collectors and regenerate the resources​

The 26.2 ModMultiblockModelProvider is a helper called from ModBlockStateProvider.registerModels, rather than a separate data provider. It receives the main provider's BlockModelGenerators and ItemModelGenerators. Sharing these collectors lets ModelProvider validate every mod block and item together, avoiding missing or duplicate definitions across the tutorial models.

Replace BlockModelBuilder, ConfiguredModel, ModelFile, and the ExistingFileHelper workflow with ExtendedModelTemplateBuilder cuboids, TextureSlot references, and TextureMapping values containing Materials. PropertyDispatch selects the formed/part/facing combinations; vanilla rotation mutators rotate their models. Preserve the roof's explicit UVs, the corner supports, and the gold first-slot frame when translating the geometry.

The two construction items get assets/tutorialmod/items/*.json definitions through ItemModelUtils.plainModel, replacing their old models/item parent files. Extend the existing recipe runner with shaped(...).save(output), add pickaxe tags using ResourceKey<Block> entries, and extend the existing loot and language providers. Keep the separate GatherDataEvent.Client and .Server listeners introduced in the earlier 26.2 chapters.

Register and run the ported GameTests​

All fourteen multiblock tests remain. Replace @GameTest/@GameTestHolder discovery with RegisterGameTestsEvent, TestData, and FunctionGameTestInstance, matching Tutorial 5. Register each test function in Registries.TEST_FUNCTION and reference its key from the test instance; registry paths use lowercase snake_case. The 13×6×9 arena is now compressed binary NBT at src/gameTest/resources/data/tutorialmod/structure/multiblocks.nbt, using the 26.2 data version. It remains outside the published mod jar.

GameTestHelper.getBlockEntity now takes the expected entity class. Tests use EntityTypes, typed/optional NBT accessors, and TagValueInput when reloading a BE. Colored item constants become collections such as Items.DYE.pick(DyeColor.RED) and Items.WOOL.pick(DyeColor.BLUE). The test player takes level/profile in its constructor, implements gameMode(), and captures messages through sendSystemMessage and sendOverlayMessage. The directory round-trip test encodes and decodes the actual ReceiverSavedData.CODEC.

From the 26.2 companion project, run:

./gradlew runClientData runServerData
./gradlew build
./gradlew runGameTestServer -Pexclude_optional_mods
./gradlew runClient

Use gradlew.bat on Windows. The separate data tasks replace runData. The porting notes record successful data generation and a build with all 21 required tests passing: fourteen multiblock tests, six existing machine tests, and NeoForge's default test. They also record successful client startup with JEI and The One Probe. In-world appearance and sound still need the manual demonstration above, especially roof UVs, control alignment, and the arrival sound.

Continue with the block entity chapter for persistence and the energy machine chapter for the previous world-rendering example. The NeoForge block entity guide explains the save, ticker, and update hooks used here.