Skip to content

CatFrame JSON Model System

dfdvdsf edited this page Aug 10, 2026 · 9 revisions

CatFrame Model Library — Developer Guide

CatFrame brings the 26.1+ resource pack model format to Minecraft 1.7.10 — full inheritance chains, blockstate variants, display transforms, tiled textures, and an extension rendering pipeline. If you're writing a mod that needs custom block or item models, this document is your starting point.

Three interfaces form the public surface of the model library:

Interface Purpose Package
IBlockStateProvider Register a block whose rendering is driven by a JSON blockstate file model
IItemStateProvider Register an item whose rendering is driven by a JSON item model model
IModelRenderExtension Hook into the rendering of every baked quad — tint, brightness, culling model.render.api

One-line summary: implement IBlockStateProvider on your Block for world rendering, implement IItemStateProvider on your Item for item rendering, and register IModelRenderExtensions when you need to reach into the pipeline and tweak individual quads.


Table of Contents


1. Architecture Overview

Two Rendering Entries, One Pipeline

Blocks and items enter the CatFrame pipeline through different doors, but both converge on the same quad-processing core:

Block side                        Item side
──────────                        ─────────
Block implements IBlockStateProvider     Item implements IItemStateProvider
  │                                    │
  ▼                                    ▼
BlockStateISBRH (renderType id)        ModelRegistry.registerItemModel()
  │                                    │
  ▼                                    ▼
VanillaRenderDispatcher.renderBlock    RenderJsonItemModel.renderItem()   ← Forge IItemRenderer
  │                                    │  (maps ItemRenderType → RenderPhase,
  │                                    │   applies preTransform counter-offsets)
  ▼                                    ▼
UniformRenderPipeline.renderBlockQuads  UniformRenderPipeline.renderItemQuads
  └──────────────┬─────────────────────┘
                 ▼
        Extension chain (IModelRenderExtension)
                 ▼
              Tessellator / RenderCommandBuffers

Module Layout

Module Responsibility
VMMDataLoader Data loading: namespace discovery, blockstate/mappings/items JSON loading
VanillaModelRegistry / ModelRegistry Registration API: block models, item models, state definitions, rotations
VMMModelBaking Creates lazy model wrappers, registers the Forge IItemRenderer
VanillaRenderDispatcher Rendering dispatch: blocks (world/GUI), items (GUI/hand/dropped)
UniformRenderPipeline Centralized quad submission: extension chain → Tessellator
BlockStateISBRH Universal ISBRH bridge for mod blocks
RenderJsonItemModel Forge IItemRenderer singleton for mod items
ModelRenderExtensions The only external entry point for registering render extensions

2. IBlockStateProvider — Registering JSON-Model Blocks

Source: model/IBlockStateProvider.java

2.1 Contract

Implement this interface on your Block to register it for blockstate-based JSON model rendering. The system will:

  1. Load the blockstate JSON from assets/{namespace}/blockstates/{name}.json
  2. On each render, call getStateProperties() to obtain the current property map
  3. Match the properties against blockstate variants to select the correct model

This is the code-level registration path — the JSON model itself lives in your resource pack, while the block tells CatFrame where to find it and how to map its metadata into variant properties.

2.2 Interface Methods

Method Required Description
getBlockstateNamespace() Namespace of the blockstate JSON, e.g. "mymod"assets/mymod/blockstates/…
getBlockstateName() Blockstate file name without .json, e.g. "cake"blockstates/cake.json
getStateProperties(world, x, y, z, metadata) Converts the current world position + metadata into a property map for variant matching
getStateDefinition() ❌ (v0.3+) Optionally returns a typed CatStateDefinition<?> for type-safe property handling
getBlockState(world, x, y, z, metadata) ❌ (v0.3+) Optionally returns a CatBlockState for this position, enabling O(1) state transitions

A classic metadata → properties example: a cake with metadata=3 returns {"bites": "3"}, which is matched against the variant key bites=3 in the blockstate JSON.

2.3 Required Resource Files

assets/{namespace}/
├── blockstates/{name}.json      ← the blockstate file (variants / multipart)
└── models/
    ├── block/{model}.json       ← referenced models
    └── item/{model}.json

Blockstate file example:

{
  "variants": {
    "type=0": { "model": "block/my_block_variant0" },
    "type=1": { "model": "block/my_block_variant1" }
  }
}

2.4 Wiring into the Render Pipeline

Two registration steps are required:

// ① PreInit: register the block for data loading (texture collection, blockstate parsing)
VMMDataLoader.registerBlock(myBlockInstance);

// ② Constructor (or preInit): obtain a dedicated renderType id
int renderTypeId = BlockStateISBRH.register(this);

Your block must return that id from getRenderType():

@Override
public int getRenderType() {
    return renderTypeId;
}

Why this matters: In 1.7.10 every block renders through a global renderType integer. CatFrame's MixinRenderBlocks intercepts only vanilla blocks (renderType 0). Mod blocks instead go through BlockStateISBRH — a universal ISBRH bridge that routes straight into the CatFrame pipeline. BlockStateISBRH.isRegistered(block) is also used by the mixin to skip already-registered mod blocks, avoiding double interception.

2.5 Complete Example

public class MyModBlock extends Block implements IBlockStateProvider {

    private final int renderTypeId;

    public MyModBlock() {
        super(Material.rock);
        this.renderTypeId = BlockStateISBRH.register(this);
    }

    @Override
    public int getRenderType() {
        return renderTypeId;
    }

    @Override
    public String getBlockstateNamespace() {
        return "mymod";
    }

    @Override
    public String getBlockstateName() {
        return "my_block";
    }

    @Override
    public Map<String, String> getStateProperties(IBlockAccess world, int x, int y, int z, int metadata) {
        Map<String, String> props = new HashMap<>();
        props.put("type", String.valueOf(metadata));
        return props;
    }
}

Registration in preInit:

VMMDataLoader.registerBlock(myBlockInstance);

2.6 Optional: Type-Safe State Definition (v0.3+)

For blocks with many properties, the raw Map<String, String> path can be replaced by a typed CatStateDefinition:

public static final Property<Integer> BITES = IntegerProperty.create("bites", 0, 6);
public static final Property<Boolean> WATERLOGGED = BooleanProperty.create("waterlogged");

@Override
public CatStateDefinition<?> getStateDefinition() {
    return new CatStateDefinition.Builder<>(this)
            .add(BITES, WATERLOGGED)
            .create();
}

@Override
public CatBlockState getBlockState(IBlockAccess world, int x, int y, int z, int metadata) {
    CatStateDefinition<Block> def = (CatStateDefinition<Block>) getStateDefinition();
    return def.any().setValue(BITES, metadata).setValue(WATERLOGGED, false);
}

When both are implemented, variant matching runs through CatBlockState with O(1) neighbor jump-tables instead of string maps.


3. IItemStateProvider — Registering JSON-Model Items

Source: model/IItemStateProvider.java

3.1 Dual Role: Discovery Marker + Render Model

IItemStateProvider is the single abstraction for CatFrame item rendering. It unifies the two layers that used to be separate ("state discovery interface" and "item model interface"):

  • As a discovery marker: when your Item implements this interface, CatFrame discovers it and collects its textures during ModelManagerDataLoader.init().
  • As a render model: the implementation directly provides render(stack, phase) and handles(phase), invoked by RenderJsonItemModel inside the Forge render pipeline.

Note: the historical auto-fallback from ItemBlock to block models has been removed. Items that don't implement IItemStateProvider fall back to vanilla item rendering — or, if explicitly registered, show the MissingNo model. There is no implicit block-model reuse for items anymore.

3.2 Interface Methods

Method Required Description
shouldHandle() ❌ (default true) Global switch: false → CatFrame never registers this item, vanilla renders it
handles(RenderPhase) ❌ (default true) Per-phase switch: false → that phase falls back to vanilla rendering
render(stack, phase) The actual rendering logic
render(stack, phase, preTransform) Rendering with a pre-transform matrix (counter-offset), applied before the display transform; default delegates to the 2-arg version
getGuiModelParts(stack) ❌ (empty) GUI-stage selected model parts — used by the oversized-in-gui overflow check to measure geometry bounds
getPropertyDefinitions() ❌ (empty) Declares custom item properties (modid:name → provider), auto-registered during discovery

3.3 Four-Tier Model Discovery Priority

When CatFrame looks for the model of an item, it checks — in order:

  1. items/{item}.json (ItemState decision tree) — highest priority, data-driven
  2. model_mappings.json items field — legacy flat mapping
  3. Item implements IItemStateProvider — code-level registration
  4. Convention path assets/{namespace}/models/item/{name}.json — lazy discovery fallback

3.4 Render Takeover Control

@Override
public boolean shouldHandle() {
    return true;  // global switch — false = vanilla renders this item everywhere
}

@Override
public boolean handles(RenderPhase phase) {
    // Fine-grained per-phase control
    if (phase == RenderPhase.ITEM_GUI) return false;  // GUI → vanilla 2D sprite
    return true;                                      // hand / ground → custom 3D
}

handles() is consulted by RenderJsonItemModel.handleRenderType() — CatFrame only takes over the phases your model opts into.

The phase → Forge ItemRenderType mapping handled by RenderJsonItemModel:

CatFrame RenderPhase Forge ItemRenderType
ITEM_GUI INVENTORY
ITEM_HAND_FIRST_PERSON EQUIPPED_FIRST_PERSON
ITEM_HAND_THIRD_PERSON EQUIPPED
DROPPED_ITEM_GROUND ENTITY (non-block items)
DROPPED_BLOCK_GROUND ENTITY (block items)
(not handled) FIRST_PERSON_MAP

3.5 Declaring Custom Item Properties

Mirroring IBlockStateProvider.getStateDefinition() on the block side, getPropertyDefinitions() lets implementations declare custom item properties in place:

@Override
public Map<String, ItemPropertyProvider> getPropertyDefinitions() {
    Map<String, ItemPropertyProvider> defs = new HashMap<>();
    defs.put("mymod:charge", (stack, phase) ->
            stack.getTagCompound() != null ? stack.getTagCompound().getInteger("charge") : 0);
    return defs;
}
  • Keys must be fully namespaced (modid:name); bare names are rejected with a warning.
  • Entries are validated and auto-registered through CatItemProperties during discovery — you never call the registration facade manually.
  • Each ItemPropertyProvider.compute(stack, phase) is evaluated lazily, only when the decision tree actually accesses the property.

3.6 Complete Example

public class MyModItem extends Item implements IItemStateProvider {

    public MyModItem() {
        this.setUnlocalizedName("my_item");
        this.setTextureName("mymod:my_item");
    }

    @Override
    public void render(ItemStack stack, RenderPhase phase) {
        // Select the model part for this stack/phase and submit its quads.
        // CatFrame's decision-tree based implementations collect parts from
        // the evaluated ItemState and hand them to UniformRenderPipeline.renderItemQuads().
        BlockStateModelPart part = resolvePart(stack, phase);
        UniformRenderPipeline.renderItemQuads(part, stack, phase, null, 0, 0, 0, null, null, null);
    }
}

No manual Forge registration is needed — discovery handles it. For explicit registration at any time:

ModelRegistry.registerItemModel(Items.apple, myModel);  // registers Forge IItemRenderer immediately

4. IModelRenderExtension — The Rendering Extension Chain

Source: model/render/api/IModelRenderExtension.java

This is the most powerful of the three interfaces: every quad baked by CatFrame passes through the extension chain before hitting the Tessellator. Extensions can tint, brighten, cull, or re-texture any quad — blocks and items alike — without a single mixin.

4.1 Registration Mechanism

All registration goes through the facade ModelRenderExtensions (model.render.api package) — the only external entry point:

// Default priority 0 — appended to the tail of the priority-0 group
ModelRenderExtensions.register(ext);

// Explicit priority — smaller runs first, negatives insert before built-ins
ModelRenderExtensions.register(ext, -2000);

// Remove on hot-reload / unload
ModelRenderExtensions.unregister(ext);

// Current extension count (including built-ins)
int n = ModelRenderExtensions.size();
  • Register during client init (or postInit).
  • The facade delegates to the internal ModelRenderRegistry; you should never touch the registry class directly.
  • One chain serves every path: block world rendering, GUI rendering, item hand rendering, dropped items — all phases funnel into the same ordered list of extensions. Extensions discriminate by ctx.phase.

4.2 How the Chain Works — Lifecycle

For each "part" (a BlockStateModelPart — the quad container selected for this render), the pipeline drives three hooks:

beforePart(allQuads, phase, part)     ← once, before any quad is processed
    │                                   (GL state setup, global decisions)
    ▼
for each quad:
    apply(RenderContext ctx)          ← once per quad, in priority order
    │                                   (modify ctx.color / brightness / skip…)
    │   └─ if ctx.skip == true → chain terminates, quad is discarded
    ▼
afterPart()                           ← once, after all quads processed
                                        (GL state restore, cleanup)
Hook Frequency Default Typical use
beforePart(List<BakedQuad>, RenderPhase, BlockStateModelPart) once per part no-op Detect model lighting mode and set GL_LIGHTING, apply display-transform GL matrices
apply(RenderContext) once per quad mandatory Per-quad tint, brightness, culling, icon override
afterPart() once per part no-op Restore GL state modified in beforePart

A legacy two-argument beforePart(List, RenderPhase) is kept for compatibility and defaults to delegating to the three-argument version with part = null — prefer overriding the three-argument one.

Phase awareness is your job: RenderPhase tells you where the quad is being processed. BLOCK_WORLD carries world/x/y/z/block; item phases carry stack and null world fields. Guard your logic accordingly.

4.3 Modifying Quad Rendering via RenderContext

RenderContext is a mutable context object per quad. Fields split into inputs (final, read-only) and outputs (mutable — write these to affect rendering):

Input fields (read, don't write):

Field Type Available in
phase RenderPhase All phases
quad BakedQuad All phases (face, tintIndex, icon, vertices)
world / x / y / z IBlockAccess / int Block phases only (null/0 for items)
block Block Block phases only
stack ItemStack Item phases only (read NBT/damage/enchantments)
metadata int All (block metadata, default 0 — e.g. BLOCK_GUI tinting)
baselineBrightness int All (renderer-computed base light)
aoBrightness[4] int[] BLOCK_WORLD (per-vertex, -1 = fall back to uniform)
aoColorMul[4] float[] BLOCK_WORLD (per-vertex occlusion, 1.0 = none)

Output fields (write these to change behavior):

Field Type Effect
skip boolean true → quad is discarded and the chain stops immediately (face culling)
color int 0xRRGGBB Color multiplier, default 0xFFFFFF. Prefer accumulating via mulColor()
brightnessOverride int ≥ 0 → force this brightness (self-illumination / shadows); -1 → use baselineBrightness
shade float Directional light coefficient (top 1.0 / side 0.8 / bottom 0.5), multiplied into final color
iconOverride IIcon Non-null → renderer samples this icon instead of quad.icon (runtime texture swap)
displayTransform Matrix4d Display-transform matrix, computed and set by DisplayTransformExtension; the pipeline applies it to vertices before submission

Convenience methods:

ctx.mulColor(0xFFAA66);              // channel-wise multiply into ctx.color (stackable)
int b = ctx.effectiveBrightness();   // override >= 0 ? override : baseline

The final color fed to the Tessellator combines everything: finalColor = color × shade × aoColorMul[i] per vertex, with per-vertex brightness when AO data is present.

4.4 Priority System

  • Every extension carries an integer prioritysmaller runs first.
  • Same priority → stable registration order (first registered runs first).
  • Default priority is DEFAULT_PRIORITY = 0.
  • Built-in extensions live at BUILTIN_PRIORITY_BASE = -1000 and occupy the head of the chain, so your priority-0 extension sees the built-ins' modifications (AO data, tint result, shade).
  • To run before a built-in, register with a priority < -1000.
  • Re-registering the same instance = re-positioning: the old entry is removed and the instance is re-inserted at the new priority. One instance never occupies two slots.
  • Setting ctx.skip = true in any extension terminates the chain immediately — a clean way to take exclusive control of a quad.
priority:  -1000        -999 … -994        0            +10
           ┌──────────────┬───────────────┬───────────┬──────────┐
           │ Built-ins    │ (chain head)  │ Your ext  │ Later mod│
           │ FaceCull …   │               │ (default) │          │
           └──────────────┴───────────────┴───────────┴──────────┘

4.5 Thread-Safety Contract

Applies since v0.5. Rendering may enter from any thread (e.g. Beddium's multithreaded chunk compilation).

  • The extension list is a CopyOnWriteArrayList — registration/unregistration is safe concurrently with render traversal.
  • Extensions must not hold shared mutable state across threads. Per-part temporary data goes into ThreadLocals or is written into RenderContext.
  • Error isolation: an exception (including Error) thrown by one extension is caught and logged; the rest of the chain and the whole render continue. Your extension can't crash the frame.
  • Same-priority ordering is stable even under concurrent registration.

4.6 Built-in Extension Order

Installed lazily on first registration/first render. They stay at the chain head in this fixed order:

# Extension Priority Job
1 FaceCullExtension -1000 Process JSON cullface — cull hidden faces before AO runs
2 AOComputeExtension -999 Per-vertex AO computation (BLOCK_WORLD only)
3 AOShadeExtension -998 ambientocclusion / shade element toggles
4 GuiLightExtension -997 gui_light lighting mode + GL_LIGHTING lifecycle
5 TintRenderExtension -996 tintindex processing via TintRegistry
6 DisplayTransformExtension -995 display transforms (GUI/hand) — writes ctx.displayTransform
7 BlockDestroyExtension -994 Destroy decal: iconOverride + full bright + pure white, BLOCK_DESTROY only

4.7 Examples

1. Warm tint on a block's top face (world rendering only):

ModelRenderExtensions.register(ctx -> {
    if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
    if (ctx.block != MyBlocks.LAVA_ROCK) return;
    if (ctx.quad.face != Direction.UP) return;
    ctx.mulColor(0xFFAA66);
});

2. Custom shadowing — halve brightness of all hand-held items:

ModelRenderExtensions.register(ctx -> {
    if (ctx.phase == RenderPhase.ITEM_HAND_FIRST_PERSON
            || ctx.phase == RenderPhase.ITEM_HAND_THIRD_PERSON) {
        ctx.brightnessOverride = 0x800080;
    }
});

3. Face culling — skip a north quad when the north neighbour is opaque:

ModelRenderExtensions.register(ctx -> {
    if (ctx.phase != RenderPhase.BLOCK_WORLD) return;
    if (ctx.quad.face != Direction.NORTH) return;
    if (ctx.world.getBlock(ctx.x, ctx.y, ctx.z - 1).isOpaqueCube()) ctx.skip = true;
});

4. Full lifecycle — GL state around a part (explicit implementation):

public class GlowExtension implements IModelRenderExtension {

    private boolean lightingDisabled = false;

    @Override
    public void beforePart(List<BakedQuad> allQuads, RenderPhase phase, BlockStateModelPart part) {
        if (phase == RenderPhase.ITEM_GUI) {
            lightingDisabled = true;   // per-thread! see thread-safety note
            GL11.glDisable(GL11.GL_LIGHTING);
        }
    }

    @Override
    public void apply(RenderContext ctx) {
        if (ctx.stack != null && isGlowing(ctx.stack)) ctx.brightnessOverride = 0xF000F0;
    }

    @Override
    public void afterPart() {
        if (lightingDisabled) GL11.glEnable(GL11.GL_LIGHTING);
    }
}

4.8 Best Practices

  1. Early return on phase/block/stack before doing any work — apply runs per quad per frame; keep the hot path cheap. No reflection, no I/O, no string concatenation inside apply.
  2. Chain order is observable: later extensions see earlier modifications. If you need to "own" a quad, set skip = true — the chain stops and the quad is dropped.
  3. Never mutate BakedQuad — it is shared across render caches; modifying it pollutes other renders. All per-render state goes through RenderContext.
  4. Thread-safe by construction: no instance fields that change per render unless they're ThreadLocal — the render path can enter from Beddium worker threads.
  5. Accumulate colors with mulColor() instead of overwriting ctx.color, so multiple tint sources compose predictably.
  6. Unregister on unload: call ModelRenderExtensions.unregister(yourExt) during hot-reload or mod unload.
  7. Prefer JSON first: tintindex, ambientocclusion, shade, gui_light, cullface, display are all handled by built-ins. Write an extension only when the JSON knobs aren't enough (dynamic per-stack logic, world-neighbour queries, runtime texture swaps).

5. Resource Pack Author Guide — Replacing Models with JSON Only

TL;DR: This section is for resource pack authors (no Java required) — what can and cannot be replaced with JSON alone, and how to actually replace block / item models. One-line conclusion: model_mappings.json can now be overridden by a pack (whole-file replacement, merged by ResourcePackModelDetector on reload), and so can models/ model files and blockstates/ / items/ files — the full mapping chain is pack-editable.

5.1 The Boundary: What a Resource Pack Can and Cannot Touch

Source Load path Pack override works?
models/**/*.json ModelResolverIResourceManager (pack-aware), re-read on every (re)bake ✅ Yes
blockstates/{name}.json classpath at startup + ResourcePackModelDetector merge on reload ✅ Yes (override & pure-add)
items/{name}.json (ItemState tree) classpath at startup + ResourcePackModelDetector merge on reload ✅ Yes (override & pure-add)
model_mappings.json classpath at startup + ResourcePackModelDetector merge on reload (whole-file replacement) ✅ Yes (override & pure-add)

Why blockstates/, items/ and model_mappings.json work: ResourcePackModelDetector re-scans on every resource reload — blockstate/item candidates are the already-loaded entries ∪ registry entries of the namespace, mappings candidates are all registered namespaces. Pack layers whose content differs from the classpath baseline are merged into loadedBlockstates / loadedItemStates / loadedMappings, then models are re-registered (registerAllModels for blocks, incremental rebuild for items). Pure additions count too — a pack can ship a blockstate for a block that had none, or a model_mappings.json for a namespace whose jar shipped none.

Key points: models/ is re-read through IResourceManager on every (re)bake, so pack overrides take effect naturally; blockstates/, items/ and model_mappings.json are content-compared by ResourcePackModelDetector on every reload, and both overrides and pure additions are merged into the data layer and trigger re-registration. One caveat: the mappings merge is whole-file replacement, not per-entry merging — a pack must ship the complete file, and entries missing from the pack version vanish.

5.2 Block Support in model_mappings.json (for reference)

blocks entries carry two semantics selected by the state_mapping flag:

Mode Value Registered as Behavior
legacy (state_mapping absent / false) model path, e.g. "block/stone" LazySingleBlockModel Single lazy-baked model per block; no metadata variants; only applies to blocks without a blockstate
state_mapping: true blockstate name (optional ns: prefix) Full blockstate pipeline (registerResidentBlockModel) variants / multipart / Pane / Stairs / redirect all supported

Registration order in registerAllModels makes blockstate win over model_mappings.blocks: Step 2 registers every loaded blockstate (including pack overrides) as a resident model, Step 3 skips any block already registered.

Key points: the blocks field is only a fallback for blocks without a blockstate: legacy mode = a single lazy-baked model with no metadata variants; state_mapping: true = old IDs routed straight into the full blockstate pipeline. Any loaded blockstate (including pack overrides) outranks a mappings entry — editing mappings is useless for a block that already has a blockstate.

A pack can override the whole file (see §5.1): drop assets/{ns}/model_mappings.json into your pack and re-ship every entry you want to keep plus your additions — entries omitted from the pack file are dropped (whole-file replacement). Keep the state_mapping flag consistent with the entries you ship.

5.3 Item Support in model_mappings.json (for reference)

items entries mirror the blocks dual semantics, selected by the same state_mapping flag:

Mode Value Registered as Behavior
legacy (state_mapping absent / false) model path, e.g. "item/stick" ItemStateModel (single-model wrapper) One model per item — no decision-tree logic; textures collected into the item atlas, model pre-baked by AsyncBakePipeline
state_mapping: true ItemState tree name (optional ns: prefix) ItemStateModel wrapping the referenced tree Full decision-tree behavior; tint bridges and oversized_in_gui flags follow the referenced state
  • Keys are item registry IDs; the namespace defaults to the file's location (assets/{ns}/), so bare names like "stick" are the norm.
  • Priority: an item already registered by items/{name}.json always wins — its mappings entry is skipped. Effective order: items/ tree > model_mappings.items > IItemStateProvider > convention path models/item/{name}.json.
  • In legacy mode the value is a plain model path — parent / display / gui_light still apply, but per-stack logic (damage, NBT, display context) requires an ItemState tree; use state_mapping: true to route an old ID straight into one.
  • Pack overrides: whole-file replacement, same as blocks (see §5.1). Re-ship every items entry you want to keep, with a state_mapping flag consistent with the entries you ship.

Key points: the items field is the item-side twin of blocks — legacy values are plain model paths (single-model wrapper, no decision-tree logic), while state_mapping: true values are ItemState tree names (full tree behavior, with tint bridges and oversized_in_gui following the referenced state). Items already registered by items/ files always outrank mappings entries.

5.4 How to Replace a Block Model (practical recipes)

Recipe A — Overwrite the model file (recommended, cleanest)

assets/minecraft/
└── models/
    └── block/stone.json          ← overwrite the target model JSON
  • Works for both blockstate-referenced models and legacy model_mappings models.
  • On reload, BakedModelCache.clear() + ModelResolver.clearCache() drop old bakes; AsyncBakePipeline re-bakes through IResourceManager — the pack version wins.
  • The model must use the 26.1+ JSON format CatFrame supports (parent, textures, elements, display, gui_light, …).

Recipe B — Add / overwrite a blockstate

assets/minecraft/
└── blockstates/
    └── stone.json                ← new or overriding blockstate
  • ResourcePackModelDetector picks it up (candidates include every registered block of the namespace), merges it, and registerAllModels re-registers the block as a resident state model — full variants / multipart support.
  • This also upgrades a block that previously had only a model_mappings entry (no blockstate): the new blockstate takes over, the mappings entry is skipped.

Recipe C — Replace item models

  • Overwrite items/{name}.json (ItemState decision tree — highest priority tier), or models/item/{name}.json (convention-path fallback). See ITEM_STATE.md for the tree format.
  • Or replace the whole model_mappings.json in your pack (whole-file replacement — keep the state_mapping flag consistent and re-ship every entry you want to keep).

Key points:

  • Recipe A (recommended): overwrite the target model file — geometry / texture references / properties are replaced, effective in both modes;
  • Recipe B: overwrite or purely add a blockstates/ file — upgrades the block to the full state pipeline (variants / multipart), also works for blocks that previously had only a model_mappings entry and no blockstate;
  • Recipe C: on the item side, overwrite the items/ decision tree, the models/item/ convention path, or the whole model_mappings.json (whole-file replacement).

5.5 Known Limits

  1. New textures are not in the atlas. Texture collection runs at startup over classpath models; a pack model referencing a texture that was never registered falls back to missingno (purple-black). Geometry / property / decision overrides work fully; brand-new textures require mod-side registration (preInit texture collection).
  2. model_mappings.json override = whole-file replacement. A pack that ships the file replaces the jar version entirely — partial edits are impossible, so re-ship every entry you want to keep.
  3. Legacy blocks entries are single-model, no metadata variants — use a blockstate for per-metadata visuals.
  4. Blockstate priority: to change a block that already has a blockstate, use Recipe A or B; editing model_mappings.json is useless for it.

Key points: the biggest trap is new textures are not in the atlas — geometry replacement is fine, but any texture referenced by the model must already have been collected by CatFrame at startup (reusing vanilla / existing textures is safest), otherwise it renders as missingno. Other limits: mappings overrides are whole-file replacements, legacy single models have no metadata variants, and blockstates always outrank mappings.

5.6 Verification Workflow

  1. Confirm the referenced textures are already in CatFrame's collected set — reuse existing textures when in doubt.
  2. Pick the override level: geometry only → Recipe A; state logic (variants / multipart) → Recipe B.
  3. Reload resources in-game (F3+T in 1.7.10 triggers refreshResources) and check the log:
    • ResourcePackModelDetector: detected N model overrides, N blockstate overrides, N item state overrides, N mapping overrides
    • VanillaModelManager: Registered N block models, N item models (N Forge renderers)
  4. If the block renders missingno, the referenced texture was never collected — switch to an existing texture or ask the mod author to register it.

Key points: verification = confirm the textures are in the collected set → drop the files at the right override level → reload with F3+T → check the ResourcePackModelDetector / VanillaModelManager lines in the log to confirm the override took effect; missingno means the texture was never collected — switch to an existing texture or have the mod side register it.


6. Pipeline at a Glance

flowchart TD
    subgraph PREINIT["preInit"]
        INIT["VMMDataLoader.init()<br/>discover namespaces, collect textures"]
        REGB["VMMDataLoader.registerBlock()<br/>BlockStateISBRH.register()"]
    end

    subgraph TEX["Texture Events"]
        TP["TextureStitchEvent.Pre"]
        TPOST["TextureStitchEvent.Post<br/>collect IIcon references"]
    end

    subgraph BAKE["Model Registration"]
        REGALL["VMMModelBaking.registerAllModels()<br/>lazy wrappers + Forge IItemRenderer"]
    end

    subgraph RENDER["Runtime Rendering"]
        BLOCK["VanillaRenderDispatcher<br/>→ BakedModelCache (lazy bake on miss)"]
        ITEM["RenderJsonItemModel<br/>→ IItemStateProvider.render()"]
        PIPE["UniformRenderPipeline<br/>beforePart → apply per quad → afterPart<br/>→ Tessellator / RenderCommandBuffers"]
    end

    INIT --> REGB
    REGB --> TP
    TP --> TPOST
    TPOST --> REGALL
    REGALL --> BLOCK
    REGALL --> ITEM
    BLOCK --> PIPE
    ITEM --> PIPE
Loading

Key classes at a glance:

Class Responsibility
IBlockStateProvider Block-side code-level registration (blockstate JSON + variant mapping)
IItemStateProvider Item-side code-level registration (discovery marker + render model)
IModelRenderExtension Quad-level hook: tint / brightness / culling / icon override
ModelRenderExtensions External registration facade for extensions
ModelRenderRegistry Internal chain registry (priority-sorted, thread-safe)
RenderContext Per-quad mutable context (inputs + outputs)
RenderPhase Phase enum with getDisplayKey() display mapping
BlockStateISBRH ISBRH bridge routing mod blocks into the pipeline
RenderJsonItemModel Forge IItemRenderer singleton mapping ItemRenderTypeRenderPhase
UniformRenderPipeline Quad submission: builds RenderSubmit, drives the extension chain
BakedModelCache Thread-safe LRU with StampedLock — lazy baking on miss

Summary

The model library exposes three developer-facing interfaces:

  • IBlockStateProvider — put it on your Block, point it at a blockstate JSON, return a property map, and your block renders through the modern model pipeline. Optionally upgrade to typed CatStateDefinition/CatBlockState for O(1) state dispatch.
  • IItemStateProvider — the one abstraction for item rendering: it marks your item for discovery and supplies the render call. Control takeover per phase with shouldHandle() / handles(), and declare custom properties in place via getPropertyDefinitions().
  • IModelRenderExtension — the surgical instrument: a priority-ordered, thread-safe chain that runs beforePart → apply → afterPart around every quad. Tint with mulColor, force light with brightnessOverride, cull with skip, swap textures with iconOverride — no mixins, no shared state, and one bad extension can't take down the frame.

Start with the JSON knobs, reach for IModelRenderExtension when you need runtime logic, and both worlds run through the same battle-tested pipeline.

Clone this wiki locally