ModelKit
Library for mod authors: load custom OBJ/MTL/PNG models without AssetBundles, so a game update cannot invalidate your art. Handles pipeline-correct materials, cutout transparency and player overrides.ModelKit
Custom models for How to Fish mods, without AssetBundles — so a game update can't break your art.
This is a library for mod authors. If you are a player, you only need it because something else told you to; it does nothing on its own.
The problem it solves
A Unity AssetBundle is compiled against the exact engine version the game was built with. When the game updates, every custom model in every mod that ships a bundle can stop loading, and the only fix is for each author to rebuild and re-release. For a game that patches as often as this one, that is a recurring tax on everybody who ships art.
ModelKit loads Wavefront OBJ, MTL and PNG instead. They're text and images. They have no engine version, they need no build pipeline, and every modelling tool already exports them. A model loaded this way keeps working across game updates because there is nothing in it that can go stale.
It is also how a player can restyle your mod without you doing anything: point the loader at a folder and anything they drop in overrides what you shipped.
What you get
- OBJ reader — positions, normals, texture coordinates, n-gons of any size, one submesh per material. Handles 1-based and negative indices, missing normals, empty groups, and meshes past the 65k vertex limit.
- MTL reader —
map_Kdtextures andKdcolours, with exporter absolute paths stripped. - Texture loading — PNG/JPG, point filtering by default (a smoothed 16×16 texture is mush), with automatic cutout detection so leaves and grates get holes rather than opaque rectangles.
- The material work — the part that isn't obvious. See below.
.gzsupport — OBJ text compresses about six to one. Shipmodel.obj.gzand it's read transparently.
Why the materials are the hard half
A mesh is a mesh in any engine. A material has to match the render pipeline the game was built with, and you can't know that at compile time.
The reliable approach is to clone a material off something the game already renders — a clone is guaranteed to be pipeline-correct, because the game is drawing with it right now. Three things make that harder than it sounds, and each one produces a bug that looks like "the model is broken":
-
Property names differ between pipelines.
_MainTexand_Colorin the built-in pipeline;_BaseMapand_BaseColorin URP. A material silently ignores a property it doesn't have, so you get an untextured model and no error. ModelKit sets both, every time. -
The thing you cloned from might use a Shader Graph shader. Those can render your model as a rainbow, and may not have the texture properties you're setting at all. ModelKit only clones from a known-plain lit shader, and otherwise builds one from scratch.
-
A clone carries everything the original had. Emission, normal maps, metallic maps, texture offsets. Left alone, your model glows, scrolls, or wears somebody else's bumps.
Materials.Calm()strips all of it.
If you've fought any of these, that's what this library is.
Using it
Add ModelKit as a dependency, reference ModelKit.dll, and:
using HtfModelKit;
// Player's folder first, your embedded copy as the fallback.
// The player can override your art by dropping a file in; if they
// don't, yours is used and there's nothing for them to install.
var source = AssetSource
.Folder(myModelFolder)
.Then(AssetSource.Embedded(Assembly.GetExecutingAssembly(), "MyMod.Assets."));
// Replace what a game object looks like:
GameObject[] parts = ModelKit.Replace(someStall, "mystall.obj", source);
ModelKit.Place(parts, scale: 1.2f, yaw: 90f, heightOffset: 0.35f);
Or in two steps if you want the mesh for something else:
ModelData model = ModelKit.Load("mystall.obj", source);
GameObject[] parts = ModelKit.Attach(target, model, null);
Point the library's logging at your own logger in one line:
ModelKit.UseLogger(Log.Info, Log.Warn);
Embedding a model in your DLL
mcs ... -resource:models/mystall.obj,MyMod.Assets.mystall.obj
The prefix you pass to AssetSource.Embedded is whatever comes before the filename — "MyMod.Assets." above. Subfolders map to dots.
Two promises
Replace only changes the appearance. Colliders, scripts, hover text and everything else on the object are left exactly as they were. A model that fails to load costs you the look and nothing else — the object still works, and the log says what went wrong. The alternative would be a mod that breaks a working game object because an art file was missing.
Nothing throws. Every entry point returns null or an empty array and logs the reason. A bad model should cost you a model, not the feature that was drawing it.
API
| Call | Does |
|---|---|
ModelKit.Load(name, source) |
Parse a model, cached by name |
ModelKit.Replace(target, name, source) |
Load it and swap it in, hiding the original renderers |
ModelKit.Attach(target, model, template) |
Build it as a child, touching nothing else |
ModelKit.Place(parts, scale, yaw, height) |
Sit it properly on what it's decorating |
ModelKit.BoundsOf(parts) |
World-space bounds, for framing or clip checks |
ModelKit.Available(name, source) |
Is it there, without building it |
ModelKit.Forget(name) |
Drop the cache so a changed file is re-read |
ModelKit.UseLogger(info, warn) |
Send log lines to your logger |
AssetSource.Folder(path) / .Embedded(asm, prefix) / .Then(other) |
Where files come from |
Materials.FindTemplate / Build / Calm / EnableCutout |
The material layer, if you want it on its own |
Forget is what makes iterating possible: reshape a model, reload the world, see the result — no rebuild, no restart.
What it does not do
No animation, no skinning, no smoothing groups, no curves. This is for static props. If you need a rigged character, you need a bundle and you need to accept the update tax that comes with it.
It also knows nothing about How to Fish itself — it never references Assembly-CSharp, only UnityEngine. That's deliberate, and it's the same reason the models survive updates.
Credits
Extracted from How To Fish - Extended, where this code loads the Lucky Charm stand. Published separately because the AssetBundle problem is everyone's, and this half was already solved.
- ModelKit on Nexus — https://www.nexusmods.com/howtofish/mods/143
- How To Fish - Extended on Nexus — https://www.nexusmods.com/howtofish/mods/70
- Discord — https://discord.gg/CgXJJBFqkY
Bug reports and pull requests welcome. If you ship something with it, say so — it's useful to know what needs supporting.
