Port a mod to the b1.7.3_rewrite loader
Java mods cannot run. A port is a Rust rewrite (wasm guest, b173_api SDK) that uses the decompiled mod as its spec.
Goal: same look, feel and gameplay inside b1.7.3, from one file in both client and server mods/. Fix source bugs and
optimize freely when look and gameplay stay the same. Never replace a vanilla screen: extra UI is a toggleable overlay.
This file is the whole map. Do not read loader design or progress docs, guides or whole SDK files to orient. Use the tools, then open only the one exemplar file the step needs.
Workspace
Paths are relative to the workspace root, the folder that holds b1.7.3_rewrite/, ports/ and the kit repo
b1.7.3_modport/. The kit's install.sh links its tools to tools/modport/ (written T/ below) and this skill to
.claude/skills/modport; from a port folder T/ is ../../tools/modport/. Fix or extend tools in b1.7.3_modport/, never inside a port. On a new machine, or when a
tool fails on a missing dependency, run T/doctor.sh: it prints the command that fixes each gap. All tools are
bash: on Windows they run in Git Bash (the Bash tool of Claude Code on Windows) and handle .exe, py and paths
themselves. Call Python tools through T/py.sh, never python3 directly.
Modes
Check once per session: test -f .modport-maintainer in the workspace root.
- Maintainer mode (the marker exists): the loader, SDK and kit repos are this user's own. Commit and push to them only when the user asks, as the steps below say.
- User mode (no marker, the default): the loader, SDK and kit repos belong to their upstream author.
- Loader and SDK extensions stay in the local working tree. Never commit them to those repos, never push to their
remotes, never run
gh pr create,gh issue createor any otherghwrite against them, even when asked to "share" or "upstream" a change. Tell the user to do that themselves. - Log each loader or SDK change in
ports/<modid>/LOADER_CHANGES.md: what it adds, why, which files. - Tell the user that a port needing a local loader change runs only on their patched loader, not on a stock one.
- The port folder is the user's own repo: commit there when asked, push only to a remote the user set up.
- Never create the marker file.
- Loader and SDK extensions stay in the local working tree. Never commit them to those repos, never push to their
remotes, never run
Tools
| Tool | Use it when | Output |
|---|---|---|
T/new_port.sh <modid> <jar> [name] | Starting a port. Writes manifests, git init, decompiles into reference/src (Vineflower, downloaded and verified on first use), runs the inventory | ports/<modid>/ |
T/inventory.sh <src> | Listing what a mod registers: blocks, items and entities with classes, tiles, models, renderers, GUIs, worldgen, events | ~15 KB for a large mod, kept in reference/inventory.txt |
T/api.sh | Before writing code against the SDK. sdk <file> (world, register, screen, behavior, events, terrain, config, services), spec <Struct>, enum <Name> (HostCall, EntityField, ModelOp), templates, events, console, hooks | one line per signature with SDK line numbers |
T/py.sh extract_assets.py <jar> <modid> | Once, from the port folder. Textures, sounds and lang into assets/<modid>, lang keys mapped to tile.<modid>.x.name, item.<modid>.x.name, <modid>.<key> | never committed |
T/py.sh convert_models.py --out src/models/generated.rs <Model.java>... [--arg Model.scale=0.5] | Models. ModelBase constructors (SRG or MCP names) to parts and boxes. Wrap the call in the port's tools/convert_models.sh | generated Rust, never hand edit |
T/test.sh deploy ports/<modid> [--game] | Every test cycle. Builds, stops the test server, installs the mod alone, starts the server. --game first rebuilds the game and cargo-b173 (after loader or SDK changes). Refuses while a test server runs: another session may own it, ask before --force | server console tail |
T/test.sh errors [n] | First thing after any failure | panic, trap and error lines of both logs |
T/modtest.sh cmd <console line> / log server 80 | Driving and checking the server | reply lines only |
T/modtest.sh client / shot <png> | Visual checks only. Ask the user before starting a client: it opens a window on their desktop | game-window PNG |
Console (T/modtest.sh cmd loader ..., syntax from T/api.sh console): gen, map (top-down PNG), slice (vertical
cut PNG), column (check before tp: a player teleported into terrain suffocates), fill, tp, use, interact,
close, entities, summon, slot, tile. Read PNGs with the Read tool. Put repeated check chains in small scripts
in ports/<modid>/tools/ instead of retyping them.
Workflow
- Set up. Pick the source version closest to beta (1.7.10 over 1.12 over 1.21): its models, textures and data
shapes map almost 1:1. 1.7.10 jars use SRG names (
func_,field_): read meaning from use, do not hunt mappings. Runnew_port.sh, thenextract_assets.py. Copy vanilla model classes a mod model extends intoreference/vanilla/for the converter. Port specific asset extras (sprites from 3D renderers) go in the port's owntools/script. - Plan. Turn
reference/inventory.txtinto PORT_STATUS.md todo rows in dependency order: blocks and items, recipes, worldgen and dimension, entities, machines and GUIs, structures, bosses, player systems, extra slots, item behaviors, sounds and particles, config. Milestones end with a deploy and a console check. - Port module by module. Read the mod's class, check the SDK with
api.sh, copy the pattern from an exemplar. Cite the source class in PORT_STATUS.md, not in code comments. - Extend the loader only when
api.shshows no way (checklist below). - Test after each module with
deploy,cmd,errors. Keep a bug list in PORT_STATUS.md instead of looping on one issue; fix the list at the end. - Finish. Update PORT_STATUS.md. Run
cargo +nightly fmtin the port andcargo clippyinb1.7.3_rewrite. Commit only when asked: a port holds Rust source, tools and manifests, never assets,reference/or AI files.ports/is either one git repo per port or a monorepo with one folder per mod;new_port.shdetects which. In user mode, list the local loader changes the port needs fromLOADER_CHANGES.mdin the final report.
Exemplars
SDK examples, always present: b1.7.3_rewrite/vendor/b1.7.3-modding-sdk/examples/ with testmod (every API module in
one file), ruby (data only content), energy, energy_api, machines (services between mods).
A finished port, when the workspace has one, is the faster map. The Aether port (ports/aether/src):
| Need | File |
|---|---|
| Block table, templates, behaviors | blocks.rs (one const row array per kind) |
| Items, tools, armor, food, recipes | items.rs, tools.rs, gear.rs, recipes.rs |
| Config options | config.rs (port every spawn rate roll, missing ones flood the world) |
| Entity registration and helpers | entities/mod.rs |
| Passive mob and mount, hostile, flyer, projectile | entities/phyg.rs + mount.rs, cockatrice.rs, zephyr.rs, projectile.rs |
| Client-visible entity state | entities/swet.rs, entities/moa.rs (setSynced/synced) |
| Models and animation | models/mod.rs (part, cuboid, Program; copy it into a new port) |
| Boss, boss bar HUD, dialogue screen | entities/bosses/mod.rs, valkyrie_queen.rs (transparent() screen plus serverbound channel) |
| Machine with GUI and tile data | machines.rs (ScreenBuilder, #[tile_behavior]) |
| Extra equipment slots, render layers | accessories.rs (screens().overlay("GuiInventory", ..), playerLayer) |
| Local player movement | player.rs (client side in TickPost, server state in thread_local maps by entity id) |
| Dimension, terrain, decoration | world/mod.rs, world/terrain.rs, world/decorate.rs |
| Structures | world/dungeons/mod.rs, writer.rs (terrain::structure::candidates, clip to StructureBox::populated, compress 256-high heights into 128) |
| Portal and travel | portal.rs |
Shapes that always apply: renderer GL calls become Program ops (push, translate, rotate, scale, texture, blend,
lighting) in the entity's pose; onLivingUpdate/updateEntityActionState go to onAi (replaces template AI) or
onTick; terrain::Random and the noise ports keep seeds identical; check isAuthority() before changing the world
(single player loops packets back, same code both sides); validate every serverbound payload; chat through
translate("<modid>.gui.…"); sounds as <modid>.<path.with.dots>; textures as <modid>:<path>.
Data shapes (loader API 1.1, ABI 3). Identity is always by name, never by numeric id:
-
Block variants and facing. Source metadata or block states become named states:
ctx.blocks().states(name, BlockStatesSpec)ordata/<modid>/states/<block>.toml.- List each property's values in the source's metadata order, so
ctx.meta()code keeps working. - A block with more than 16 states gets extra hidden ids from the loader. Read and write such a block only through
world.blockState/setBlockStateorctx.state()/setState(). - Textures per state go in
variants.
- List each property's values in the source's metadata order, so
-
Per-stack data. Item NBT and capabilities become data components:
stackComponent/setStackComponentwith aStackRef.- A mod writes only
<modid>:*components. - Stacks with different components never merge.
- Damage stays durability. Use one item per variant instead of damage subtypes.
- A mod writes only
-
Data on things the mod does not own. Entity, player, tile, chunk and world NBT or capabilities become per-mod slots:
- entity:
entity.setDataorsetSynced - player:
player.setData(key, ..) - tile entity:
world.setTileAttachment - chunk:
world.setChunkAttachment - world:
setWorldData
Every slot is keyed by the mod id, so two mods never clash.
- entity:
-
Edits to vanilla or other mods' content. Property edits (hardness, light, flammability, tool) become
ctx.blocks().modify(BlockModifySpec)ordata/<modid>/modify/blocks/*.toml. Spawn list edits becomemodifySpawnsormodify/spawns/*.toml. Mining speed for a tool class comes from theminecraft:mineable/<pickaxe|axe|shovel>tags. Conflicting edits are reported in the Mod Loader screen. Never replace a whole vanilla block for a property change. -
Worldgen and commands.
- Name each worldgen feature uniquely. The name seeds its random, so other mods never shift it.
- Commands answer to
/<modid>:<name>. The short/<name>goes to the first mod that registers it.
Extending the loader
General features only, never a one-mod hack. Paths below are inside b1.7.3_rewrite/. Touch points, in order:
- Protocol: append variants at the end of
HostCall,GuestCall,GuestReplyorEntityFieldinvendor/b1.7.3-modding-sdk/mod_protocol/src/abi.rs; new spec fields inspec.rsasOptionorDefaulttypes (spec structs are#[serde(default)], so old mods still load). Released contracts never change: a changed one gets a new version. - Host: match arm in
crates/modloader/src/host/world.rs(runtime calls) orhost/register.rs(register phase); larger systems in their owncrates/modloader/src/<system>.rsorextensions.rs. New templates are rows incrates/modloader/src/entities.rs(ENTITY_TEMPLATES) orcontent/templates.rs(BLOCK_TEMPLATES). - SDK wrapper next to its siblings in
b173_api/src/{world,register,screen}.rs, in the prelude if mods use it everywhere. - Client side in
crates/client/src/modded/. Vanilla code calls new behavior through a one-line hook with a// rewrite:comment, listed with a version incrates/modloader/hooks.toml. - Dev console tools in
crates/modloader/src/devtools.rs(add toUSAGE). cargo buildandcargo clippy, thenT/test.sh deploy ports/<modid> --game. Note the addition where the workspace keeps loader notes. Maintainer mode: SDK changes are committed in the SDK submodule, then bumped in the rewrite, only when the user asks. User mode: nothing is committed there, the change goes inLOADER_CHANGES.md.
Example chain, World::lightning: HostCall::Lightning (abi.rs) to its arm in host/world.rs to the wrapper in
b173_api/src/world.rs.
Known traps
-
HostCall,HostReplyand spec structs are postcard encoded. Never add a field to an existing struct: ABI 2 guests would stop decoding. New data needs a new variant or a new struct. -
T/modtest.shdrives one shared test server and client (b1.7.3_rewrite/target/modtest, server in itsserver/). Never stop, reset or take over one another session started without asking the user. -
cargo b173 buildalready runs the wasm build; there is no separatecargo buildstep. -
Loader or SDK change without
deploy --game: the server still runs the old binary without the new host calls. -
The test client's window loses focus and opens the pause menu; it is the harness, not the port.