01 — Case study
stellar
Armature: one common codebase shipping Fabric + NeoForge with identical semantics. Tenet: 18 task types, 10 rewards, 6 conditions — authored as JSON, judged by the server.
· Java 21 · Fabric · NeoForge · Minecraft 1.21.1 · JSON · MIT
The problem
Minecraft modding runs on legacy debt: reflection-heavy multi-loader code, questing tools that bundle their content with the engine, and — the sharp edge — loaders that disagree about event ordering. Entity death fires after the fact on Fabric and before it on NeoForge; block breaks differ the same way. Every consumer either writes `if (fabric)` branches or ships subtly wrong behavior on one loader, and every new Minecraft version turns the old stack into a migration project instead of a version bump.
What I built
Stellar is a clean-slate ecosystem for Minecraft 1.21 and beyond, built as two version-locked MIT libraries. Armature is the multi-loader runtime: lifecycle, ordered events, typed networking, registries, energy, config, platform abstraction, and team resolution — one common codebase with two thin loader modules, documented at ellipog.dev/docs/armature. Tenet is the declarative quest engine on top of it: quest lines are plain JSON in config/tenet/quests/, validated to file and line, reloaded live with /tenet reload, backed by a JSON Schema — with an in-game chapter editor, party-aware progress, EMI/JEI/REI integration, and docs at ellipog.dev/docs/tenet.
How
- One common codebase, two thin loader modules — a single renderer seam is the only file that changes on port. Documented ordering differences (death and break fire after on Fabric, before on NeoForge) are normalized behind one ordered event API, so no consumer ever branches on loader.
- Strict everything: configs are validated for presence and type with did-you-mean suggestions and file-and-line errors; quest JSON gets the same treatment plus a schema and a legacy-payload compatibility suite. Bad input gets an explanation, never a stack trace.
- The server is the sole arbiter. Progress is event-counted server-side — break tasks match a block id, a tag, or a full blockstate like minecraft:wheat[age=7] — while the client only renders. The client never decides what counts.
- Open registries with string ids and aliases: 18 task types, 10 reward types, and 6 condition types out of the box, and addon mods teach new verbs through the same TaskType/RewardType API. No fork required to extend the engine.
- An editor, not a text field: the in-game chapter editor batches work into single undoable acts, deleted nodes linger as .deleted tombstones until restored, and component templates plus a type picker make new content declarative. Party modes (pooled, per-member, owner-only) resolve through Armature teams, which settle by precedence: explicit over FTB Teams over OPAC over stored.
- Tested like infrastructure: roughly 250 test files across the pair — full quest playthroughs, a 1,000+ file generated pack, validator and mapping suites — with most of the 60+ file UI toolkit proven in millisecond unit tests through the renderer seam instead of screenshots.
Result
A version-locked, MIT-licensed pair with docs re-synced to ellipog.dev on every change — the foundation the Create Stellar flagship (600+ quests) and the Kura server storefront delivery mod are being built on.
What I learned
The trick to supporting two loaders is not abstraction — it is documenting the exact ordering difference and normalizing it once, behind one API. Strictness compounds the same way: every file-and-line error is a support thread that never happens, and a renderer seam that keeps game code out of UI code is what makes any of it testable at all.
Stack
- Java 21
- Fabric
- NeoForge
- Minecraft 1.21.1
- JSON
- MIT