-
Notifications
You must be signed in to change notification settings - Fork 1
CatFrame ModernItem and ModernBlocks
CatFrame ships two ready-made base classes that plug your content into the JSON model pipeline with almost no boilerplate:
| Class | Extends | Implements | What you get |
|---|---|---|---|
ModernItem |
Item |
IItemStateProvider |
Multi-layer textures (N passes) + dual-model rendering (2D GUI / 3D hand) |
ModernBlock |
Block |
IBlockStateProvider |
One-call registration: blockstate JSON loading + ISBRH + ItemBlock model |
Both live in model/impl/. They implement the provider interfaces, which means registration is automatic — the moment your block/item is in the game registry, CatFrame discovers it and pulls in its models and textures. No manual namespace calls, no manual IItemRenderer registration.
Note: This document replaces the old
ModernItemJsondocumentation. The class is now namedModernItem, and dual-model dispatch is built on theItemStateNodedecision tree (not the removedDualRenderIItemJsonStateProvider).
- ModernItem & ModernBlock — One-Click JSON Model Base Classes
Source: model/impl/ModernItem.java
Vanilla items render 1 or 2 passes. ModernItem supports N passes — each pass draws its own full-brightness flat quad in the GUI, with its own texture and tint:
public class GemSwordItem extends ModernItem {
public GemSwordItem() {
super(3); // blade + handle + gem = 3 render passes
setLayerTextureNames(
"mymod:items/sword_blade",
"mymod:items/sword_handle",
"mymod:items/sword_gem"
);
setUnlocalizedName("mymod.gem_sword");
setCreativeTab(CreativeTabs.tabCombat);
}
@Override
public int getColorFromItemStack(ItemStack stack, int pass) {
// Per-layer tint — e.g. a dyed gem on layer 2
if (pass == 2 && stack.getTagCompound() != null) {
return stack.getTagCompound().getInteger("gem_color");
}
return 0xFFFFFF;
}
}That's the whole registration story: GameRegistry.registerItem(...) in preInit, and CatFrame's discovery pass picks it up — IItemStateProvider on the class is the declaration.
| Method | Notes |
|---|---|
ModernItem() |
1 render pass |
ModernItem(int layers) |
N passes; values < 1 are clamped to 1 |
setLayerTextureNames(String... names) |
One texture per layer; updates the layer count; forwards the first name to setTextureName() so vanilla icon fallback keeps working. Returns this
|
setLayerCount(int) |
Change pass count at runtime — must run before registerIcons
|
getLayerCount() |
Current pass count |
getLayerIcon(int layer) |
Direct icon access for a layer (falls back to itemIcon) |
getColorFromItemStack(stack, pass) |
Per-layer tint, default 0xFFFFFF — override for dyed items |
getSubItems(item, tab, list) |
Default adds one ItemStack(item, 1, 0); override for multi-damage subtypes |
Internally, requiresMultipleRenderPasses() returns layerCount > 1 and getRenderPasses() returns layerCount, so vanilla multi-pass machinery does the heavy lifting in the GUI.
A common pattern: flat 2D icon in the inventory, a proper 3D model in the hand. setModels() sets both paths:
public class PlushyItem extends ModernItem {
public PlushyItem() {
setModels(
"mymod:item/plushy_inventory", // 2D GUI / dropped-item model
"mymod:item/plushy_hand" // 3D handheld model
);
setUnlocalizedName("mymod.plushy");
}
}With only one model path, that model is used for all phases. With two, the internal decision tree routes by render phase:
| Phase | Model |
|---|---|
ITEM_HAND_FIRST_PERSON / ITEM_HAND_THIRD_PERSON / ITEM_FIXED
|
3D hand model |
ITEM_GUI / DROPPED_ITEM_GROUND / DROPPED_BLOCK_GROUND
|
2D inventory model |
Compatibility note:
setModels()and theinventoryModelPath/handModelPathfields are marked@Deprecated— the modern way is a fullIItemStateProviderimplementation or anitems/{name}.jsondecision tree. The convenience API still works and is the intended entry for quick items.
setModels() builds an ItemStateNode tree rooted at a SelectNode on the display_context property:
SelectNode(display_context)
├─ ITEM_HAND_FIRST_PERSON / ITEM_HAND_THIRD_PERSON / ITEM_FIXED → ModelLeaf(handModel)
├─ ITEM_GUI / DROPPED_ITEM_GROUND / DROPPED_BLOCK_GROUND → ModelLeaf(inventoryModel)
└─ (fallback) → ModelLeaf(inventoryModel)
At render time, render(stack, phase) (and its preTransform variant):
- Builds live properties via
ItemProperties.buildProperties(stack, phase) - Evaluates the tree (
itemStateRoot.evaluate(props)) - For each selected model path:
BakedModelCache.buildKey(path, 0, 0)→ lazy bake on miss - Submits the part through
UniformRenderPipeline.renderItemQuads(part, stack, phase, ...)
handles(phase) returns itemStateRoot != null — an item with no model configured simply isn't taken over by CatFrame.
-
Texture collection:
getModelPath()(inventory) andgetHandModelPath()(hand) are scanned duringModelManagerDataLoader.init()— both models' textures are collected automatically. -
Discovery: implementing
IItemStateProvideris Tier-3 code-level discovery; the init pass scansItem.itemRegistryincrementally, so late registration is fine. -
isFull3Dis kepttrue:RenderJsonItemModel.computePreTransform()reads it in theRenderBipedbranch (third-person, non-player entity, non-ItemBlock) to pick the full3D counter-transform vs. the 2D fallback. Removing it looks safe but silently mis-positions mob-held items.
Source: model/impl/ModernBlock.java
public class MyBlock extends ModernBlock {
public MyBlock() {
super(Material.rock);
setBlockName("my_block");
setBlockTextureName("mymod:my_block");
setBlockstate("mymod", "my_block");
}
}Register in preInit — GameRegistry.registerBlock first, so the registry name can be derived:
GameRegistry.registerBlock(myBlock, "my_block");
ModernBlock.register(myBlock); // returns the ISBRH renderType id on the client, -1 on the serverAnd that's it — world rendering, item-in-hand, GUI, and dropped-item rendering all work from the one blockstate file.
| Method | Meaning |
|---|---|
setBlockstate(namespace, name) |
Explicit path → assets/{namespace}/blockstates/{name}.json
|
setBlockstate(name) |
Default namespace "minecraft" → assets/minecraft/blockstates/{name}.json
|
| (neither called) | Derived from the registry name: "ns:name" → namespace ns, file name; no colon → minecraft namespace |
All setters (setBlockName, setBlockTextureName, setCreativeTab, setBlockstate) are chainable — they return this.
ModernBlock.register(block) performs four steps:
-
Derive the blockstate path from the registry name if
setBlockstate()wasn't called (logs a warning if nothing can be resolved). -
Load blockstate data —
ModelManagerDataLoader.registerBlock(block). -
Register a
BlockStateModel— a lazyLazyStateProviderBlockModelwrapper, so the ItemBlock can reuse the same model in inventory/hand. The wrapper resolves the real model on first render, which meansregister()works even before the blockstate JSON is loaded. -
Register the ISBRH handler (client only) —
RenderJsonBlockModel.register(block)allocates a renderType id (starting at 90000) and returns it;getRenderType()returns it. The server path returns-1.
The ISBRH bridge's renderWorldBlock delegates to RenderDispatcher.renderBlock (lazy bake → extension chain → Tessellator). Its renderInventoryBlock is a deliberate no-op — item-context rendering of block items goes through the item pipeline (RenderJsonItemModel), never through ISBRH.
The discovery pass is driven by the registries, not by manual calls:
- During each
TextureStitchEvent.Pre,ModelManagerDataLoader.init()scansBlock.blockRegistryforIBlockStateProviderimplementations and derives the participating namespaces from them. A registered block implementing the interface IS the "I use CatFrame" declaration — a resource pack can't fakeinstanceof. -
register()merely front-loads the bookkeeping; blocks present in the registry are auto-discovered at the next stitch. Registration timing is no longer a constraint (late preInit / early init registrations get picked up by the post-initrefreshResourcesstitch).
So even if you skip ModernBlock.register(), world rendering still kicks in after the next stitch — but calling it in preInit gets the renderType id immediately and avoids surprises.
getStateProperties() defaults to an empty map, which matches the "normal" variant in the blockstate file. Override it to drive variant selection from world state / metadata:
@Override
public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
Map<String, String> props = new HashMap<>();
props.put("facing", EnumFacing.getFront(metadata).name());
props.put("powered", String.valueOf(world.isBlockIndirectlyGettingPowered(x, y, z)));
return props;
}The map is matched against variant keys like facing=east,powered=true in the blockstate JSON. For heavier state machines, override getStateDefinition() / getBlockState() with a typed CatStateDefinition (see MODEL_SYSTEM.md §2.6).
ModernBlock registers a BlockStateModel automatically, so the block's ItemBlock renders from the same blockstate JSON — but through the item pipeline:
- The
LazyStateProviderBlockModelwrapper resolves the blockstate on first use, so registration order between block and item never matters. - The item model is looked up via
ModelRegistry.getRegisteredItemModel— for block items without an explicit item model, CatFrame falls back through the registered block model path. -
RenderJsonBlockModel.shouldRender3DInInventory()returnsfalse; GUI rendering is driven by the item'sdisplay.guitransform, not by ISBRH's legacy 3D inventory path.
assets/mymod/
├── blockstates/
│ └── my_block.json ← referenced by ModernBlock.setBlockstate("mymod", "my_block")
├── models/
│ ├── block/
│ │ └── my_block.json ← variant target
│ └── item/
│ ├── plushy_inventory.json ← ModernItem.setModels() path #1 (2D GUI)
│ └── plushy_hand.json ← ModernItem.setModels() path #2 (3D hand)
└── textures/
├── blocks/
└── items/
├── sword_blade.png ← ModernItem.setLayerTextureNames() layer 0
├── sword_handle.png
└── sword_gem.png
Typical model JSONs:
// assets/mymod/models/item/plushy_inventory.json — flat GUI icon
{
"parent": "item/generated",
"textures": { "layer0": "mymod:items/plushy_inventory" }
}
// assets/mymod/models/item/plushy_hand.json — 3D handheld
{
"parent": "item/handheld",
"textures": { "layer0": "mymod:items/plushy_hand" },
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
}
}
}A full minimal pair — block with a variant-driven model, item with layered textures:
// Block side
public class OreBlock extends ModernBlock {
public OreBlock() {
super(Material.rock);
setBlockName("ore_block");
setBlockTextureName("mymod:ore_block");
setBlockstate("mymod", "ore_block");
setCreativeTab(CreativeTabs.tabBlock);
}
@Override
public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
Map<String, String> props = new HashMap<>();
props.put("rich", String.valueOf(metadata >= 8));
return props;
}
}
// Item side
public class GemItem extends ModernItem {
public GemItem() {
super(2); // gem + glow layer
setLayerTextureNames(
"mymod:items/gem",
"mymod:items/gem_glow"
);
setModels(
"mymod:item/gem_inventory",
"mymod:item/gem_hand"
);
setUnlocalizedName("mymod.gem");
}
}
// preInit
GameRegistry.registerBlock(new OreBlock(), "ore_block");
GameRegistry.registerItem(new GemItem(), "gem");
// ModernBlock.register(oreBlock) — optional, auto-discovery covers it at next stitchThat's the whole integration story: no mixins, no manual namespaces, no manual renderer registration.
| Class | Responsibility |
|---|---|
ModernItem |
Item + IItemStateProvider: N-layer textures, dual-model decision tree |
ModernBlock |
Block + IBlockStateProvider: blockstate path config + one-call registration |
ItemStateNode |
Decision tree nodes (SelectNode / ModelLeaf) built by setModels()
|
LazyStateProviderBlockModel |
Lazy BlockStateModel resolving the blockstate on first render |
RenderJsonBlockModel |
ISBRH bridge for mod blocks (world rendering → RenderDispatcher) |
RenderJsonItemModel |
Forge IItemRenderer singleton (item phases → IItemStateProvider.render) |
ModelManagerDataLoader |
Discovery driver: registry scan, namespace derivation, texture collection |
BakedModelCache |
Thread-safe LRU, lazy baking on miss |
UniformRenderPipeline |
Quad submission: extension chain → Tessellator |
-
ModernItem— subclass and configure:setLayerTextureNames()for N-pass GUI icons,setModels()for 2D/3D dual rendering. ImplementingIItemStateProvidermakes discovery automatic; thedisplay_contextdecision tree routes phases to the right model, and texture collection follows the model paths automatically. -
ModernBlock— subclass, point at a blockstate file, callregister()once. Everything else — JSON loading, ISBRH renderType, ItemBlock reuse — is handled. Registry-driven discovery even makesregister()optional at the next texture stitch. - Both are plain vanilla subclasses, so they stay compatible with existing tooling (
setTextureName, creative tabs,getSubItems), while the JSON pipeline handles the modern rendering.