Skip to content

CatFrame ModernItem and ModernBlocks

dfdvdsf edited this page Aug 10, 2026 · 1 revision

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 ModernItemJson documentation. The class is now named ModernItem, and dual-model dispatch is built on the ItemStateNode decision tree (not the removed DualRenderIItemJsonStateProvider).


Table of Contents


1. ModernItem — Multi-Layer Item Base Class

Source: model/impl/ModernItem.java

1.1 Quick Start: Multi-Layer Item

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.

1.2 Layer API Reference

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.

1.3 Dual-Model Rendering (2D GUI + 3D Hand)

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 the inventoryModelPath / handModelPath fields are marked @Deprecated — the modern way is a full IItemStateProvider implementation or an items/{name}.json decision tree. The convenience API still works and is the intended entry for quick items.

1.4 How the Decision Tree Dispatches

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):

  1. Builds live properties via ItemProperties.buildProperties(stack, phase)
  2. Evaluates the tree (itemStateRoot.evaluate(props))
  3. For each selected model path: BakedModelCache.buildKey(path, 0, 0) → lazy bake on miss
  4. 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.

1.5 Integration with the Model System

  • Texture collection: getModelPath() (inventory) and getHandModelPath() (hand) are scanned during ModelManagerDataLoader.init() — both models' textures are collected automatically.
  • Discovery: implementing IItemStateProvider is Tier-3 code-level discovery; the init pass scans Item.itemRegistry incrementally, so late registration is fine.
  • isFull3D is kept true: RenderJsonItemModel.computePreTransform() reads it in the RenderBiped branch (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.

2. ModernBlock — One-Click Block Base Class

Source: model/impl/ModernBlock.java

2.1 Quick Start

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 server

And that's it — world rendering, item-in-hand, GUI, and dropped-item rendering all work from the one blockstate file.

2.2 setBlockstate() API

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.

2.3 What register() Does

ModernBlock.register(block) performs four steps:

  1. Derive the blockstate path from the registry name if setBlockstate() wasn't called (logs a warning if nothing can be resolved).
  2. Load blockstate dataModelManagerDataLoader.registerBlock(block).
  3. Register a BlockStateModel — a lazy LazyStateProviderBlockModel wrapper, so the ItemBlock can reuse the same model in inventory/hand. The wrapper resolves the real model on first render, which means register() works even before the blockstate JSON is loaded.
  4. 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.

2.4 Automatic Discovery — You May Not Even Need register()

The discovery pass is driven by the registries, not by manual calls:

  • During each TextureStitchEvent.Pre, ModelManagerDataLoader.init() scans Block.blockRegistry for IBlockStateProvider implementations and derives the participating namespaces from them. A registered block implementing the interface IS the "I use CatFrame" declaration — a resource pack can't fake instanceof.
  • 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-init refreshResources stitch).

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.

2.5 Dynamic Properties

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).

2.6 ItemBlock Rendering

ModernBlock registers a BlockStateModel automatically, so the block's ItemBlock renders from the same blockstate JSON — but through the item pipeline:

  • The LazyStateProviderBlockModel wrapper 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() returns false; GUI rendering is driven by the item's display.gui transform, not by ISBRH's legacy 3D inventory path.

3. Resource File Layout

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]
    }
  }
}

4. Combined Example: A Block + Its Item

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 stitch

That's the whole integration story: no mixins, no manual namespaces, no manual renderer registration.


5. Core Classes at a Glance

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

Summary

  • ModernItem — subclass and configure: setLayerTextureNames() for N-pass GUI icons, setModels() for 2D/3D dual rendering. Implementing IItemStateProvider makes discovery automatic; the display_context decision tree routes phases to the right model, and texture collection follows the model paths automatically.
  • ModernBlock — subclass, point at a blockstate file, call register() once. Everything else — JSON loading, ISBRH renderType, ItemBlock reuse — is handled. Registry-driven discovery even makes register() 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.