Registration groups
Experimental, additive API: existing registrars and specification builders still work.
Blocks and items
Keep declarations in separate classes. Supply the namespace once per group; returned references already contain their final IDs, even before initialization.
public final class ModBlocks {
public static final Registration.Blocks BLOCKS = Registration.blocks("my_mod");
public static final BlockRef WORKSTATION = BLOCKS.block("workstation", p -> p
.copyFrom(ResourceId.of("minecraft", "oak_planks"))
.strength(3, 6)
.withItem());
public static final BlockRef REINFORCED = BLOCKS.block("reinforced", p -> p
.copyFrom(WORKSTATION)
.strength(5, 10)
.withItem());
}
public final class ModItems {
public static final Registration.Items ITEMS = Registration.items("my_mod");
public static final ItemRef CRYSTAL = ITEMS.item("crystal");
public static final ItemRef TOOL = ITEMS.item("tool",
p -> p.maxStackSize(1).durability(256).fireResistant());
}
In common initialization:
Registration.register(context, ModBlocks.BLOCKS, ModItems.ITEMS);
There is no consumer .build() call. Configuration callbacks run during attachment,
not class loading. Keep them declarative: do not perform native registration or
modify registration groups inside them. Attach each group once. Group attachment
freezes further declarations; native failures abort startup rather than retrying a
partially modified registry.
Duplicate IDs, block-item/item collisions, namespace mismatches, invalid specifications,
missing copied declarations and copy cycles are rejected before native registration.
Block copies may refer across groups passed to the same register call; group order
does not matter. This is not a global mod registry, and it does not use reflection
to discover classes.
Custom block definitions
BLOCKS.block("workstation", MyBlock::new, p -> p.strength(3).withItem())
accepts a factory for an SDK PortableBlock, not a native Minecraft subclass.
The factory runs once per registered ID during preflight. Its configure method
runs before the declaration's property overrides. onUse(context, event) receives
unhandled, uncancelled server block-use events for that ID through the existing SDK
interaction bus. Calling event.handle() prevents native fallback; leave the event
unchanged to preserve other handlers. Property copies do not copy these callbacks.
Instances are shared definitions, not placed-block instances: per-position mutable
data belongs in declared persistent storage. Static shapes are
supported, together with opt-in horizontal facing.
Custom placement, general typed block states and state-dependent shapes are now
available, including pure neighbor-derived state updates. Scheduled ticks and
waterlogging are still pending;
this initial interface is not yet a replacement
for every Minecraft Block method. See the furniture-port roadmap.
Copy semantics
copyFrom(BlockRef)copies the portable properties of a declaration in the same attachment batch, including any native base. It does not copy its block item, storage, callbacks, or recipes.copyFrom(BlockSpec)copies an existing immutable portable specification.copyFrom(ResourceId)uses the target's native property-copy operation. The source must exist when properties are resolved. PreferBlockReffor SDK declarations.- A later copy replaces earlier property overrides. Calls after the copy override the copied properties.
- Native copying is available in generated feature runtimes
(
-Penderfall.persistence=true). Reference/baseline runtimes reject it explicitly. - No fallback is silently substituted for an absent block.
The native implementation uses Properties.copy on 1.20.1 and
Properties.ofFullCopy on newer supported targets. These inherit Minecraft's own
copy semantics; they are not a backport of the source block. Models, loot, recipes,
block subclasses and state definitions are separate. State-dependent native property
callbacks can require state properties that a basic block does not have; do not use
such a copy as a substitute for implementing the block's behavior.
Explicit portable overrides currently cover strength, friction, jump factor,
luminance, tool requirement and sound. Item declarations expose the existing
stack-size, durability, fire-resistance and rarity settings, with
ItemSpec.Builder.copyFrom(ItemSpec). This is not a claim that every Minecraft
property, item component, or arbitrary native registry is portable.
Persistent blocks
For storage used only by the block:
BLOCKS.block("tank", p -> p.strength(2).storage(s -> s.tank(RESERVOIR)));
Storage automatically creates the block item and assigns its owner.
For storage shared with a workbench definition, declare an immutable schema using
Registration.storage(blockRef, s -> ...), then pass it to p.storage(schema).
The persistent preview's PreviewBlocks demonstrates static declarations and
shared schemas without .build().
Recipes, menus and creative tabs
Registration.Recipes RECIPES = Registration.recipes("my_mod");
WorkbenchRecipeTypeRef ASSEMBLY = RECIPES.workbench("assembly", 3);
WorkbenchRecipeTypeRef WASHING = RECIPES.machine("washing", 1);
Registration.Menus MENUS = Registration.menus("my_mod");
WorkbenchRef BENCH = MENUS.workbench("workbench", "Workbench", ASSEMBLY,
p -> {}, craft -> {});
MenuRef STATUS = MENUS.menu("status", "Status", p -> p.size(220, 140),
action -> {});
Registration.Tabs TABS = Registration.tabs("my_mod");
CreativeTabRef TAB = TABS.tab("main", "itemGroup.my_mod", CRYSTAL,
p -> p.entry(CRYSTAL).entry(new ItemRef(WORKSTATION.id())));
Attach these groups alongside blocks and items. Recipe types are registered before menus; creative tabs are registered last. Recipe JSON/datagen and gameplay callbacks remain separate from registration. The SDK supplies the synchronized client screen for these portable menus; authors do not register the same screen on both sides. Arbitrary native screens and recipe serializers remain outside this facade.
Client renderers
From the client entrypoint, with renderer code in src/client:
Registration.renderer(clientContext, WORKSTATION, r -> r.inventorySlot(0, transform));
The renderer still goes through the existing client registrar and its capability, duplicate-registration and lifecycle checks. It is never invoked automatically during common initialization.
Runtime folders
Do not delete the old runtime directories yet: default settings still include them and parity/build tooling still references their layouts. Generated runtime directories are target build outputs, not a need for consumer target-specific source. Removal of reference sources requires a separate build/publishing/parity migration. No runtime folders were removed for this feature.