Set up a project
In which we point modern tools at 2011 and they just… work.
Every craft begins with a bench. Ours is the official ornithe-mod-template, the same starting point Ornithe recommends for any version, converted to target Beta 1.7.3 with biny mappings and RetroAPI. You can do the conversion by hand (this page shows every line), or skip it entirely:
⬇ bare-retroapi-template.zip , this page, already done
You need Java 21 to run Gradle and the game (the gen2 toolchain and the LWJGL3 runtime expect it). Your mod compiles for Java 21 too, modern language features on a 2011 game, courtesy of the modern JVM underneath.
The three files that matter
Starting from the stock template, exactly three files change: build.gradle (how to build), gradle.properties (which versions), and fabric.mod.json (who you are). Here is each one in its converted form.
build.gradle, Loom, Ploceus, and biny
The stock template uses Feather mappings on gen1 intermediary. We switch to gen2 intermediary and the community biny-ornithe mappings, and add the two mavens that host the b1.7.3 ecosystem:
plugins {
id 'java'
id 'net.fabricmc.fabric-loom-remap' version '1.17.13'
id 'ploceus' version '1.17.4'
// also builds a babric variant of your mod (build/libs/<name>-babric.jar)
id 'com.periut.retroconvert' version "${retroconvert_version}"
}
base {
archivesName = project.archives_base_name
}
version = "${project.version}+mc${project.minecraft_version}"
group = project.maven_group
ploceus {
setIntermediaryGeneration(2) // gen2, required for b1.7.3 + this ecosystem
}
repositories {
mavenLocal()
maven { url = 'https://matthewperiut.github.io/repository' } // gen2 mod deps (retroapi, retrodragon)
maven { url = 'https://maven.glass-launcher.net/releases/' } // biny mappings
// Loom >= 1.17 claims the whole org.lwjgl group for Mojang's libraries repo, which mirrors
// LWJGL 3.4.0 only for the platforms Minecraft ships - retrodragon also needs
// natives-linux-arm64, which lives on Central. Claim the group for Central too.
exclusiveContent {
forRepository { mavenCentral() }
filter { includeGroup('org.lwjgl') }
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
// exceptions/signatures/nests are per-side in beta (client and server were separate jars),
// and they MUST come before the mappings dependency -- see the note below.
clientExceptions ploceus.raven(project.client_raven_build, 'client')
serverExceptions ploceus.raven(project.server_raven_build, 'server')
clientSignatures ploceus.sparrow(project.client_sparrow_build, 'client')
serverSignatures ploceus.sparrow(project.server_sparrow_build, 'server')
clientNests "net.glasslauncher:biny-nests:b1.7.3-client+build.${project.biny_nests_build}"
serverNests "net.glasslauncher:biny-nests:b1.7.3-server+build.${project.biny_nests_build}"
// biny instead of Feather: human-readable names for the whole game
mappings ploceus.mappings("net.glasslauncher:biny-ornithe:b1.7.3+build.${project.biny_mappings}:mergedv2")
ploceus.dependOsl(project.osl_version) // entrypoints, networking, lifecycle events…
// OSL's block and item REGISTRY modules lock a registry of their own in Block.<clinit>,
// which is what station-blocks-v0 and station-items-v0 also do. With both present the
// game dies before it draws a frame on "Slot 0 is already occupied by Block{null}".
// Drop them unless you actually use them.
configurations.modImplementation {
exclude group: 'net.ornithemc.osl-gen2', module: 'blocks'
exclude group: 'net.ornithemc.osl-gen2', module: 'items'
}
modImplementation "com.periut:retroapi:${project.retroapi_version}"
// retrodragon brings LWJGL 3, which would clash with vanilla
// Minecraft's LWJGL 2 API, so keep it off the compile classpath.
modRuntimeOnly "com.periut:retrodragon:${project.retrodragon_version}"
}
// Exclude the Legacy Fabric LWJGL 2 wrapper at RUNTIME only (retrodragon provides LWJGL 3).
// It must stay on the compile/minecraft classpath so Condor (Ploceus LVT generator)
// can resolve org.lwjgl.* classes referenced by b1.7.3 - a blanket exclude here
// strips LWJGL 2 from minecraftCompileLibraries and crashes LVT generation.
configurations.matching { it.name.toLowerCase().contains('runtime') }.configureEach {
exclude group: 'org.lwjgl.lwjgl'
}
processResources {
inputs.property 'version', version
filesMatching('fabric.mod.json') {
expand 'version': version
}
}
tasks.withType(JavaCompile).configureEach {
it.options.encoding = 'UTF-8'
it.options.release = 21
}
java {
// Still required by IDEs such as Eclipse and Visual Studio Code
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
withSourcesJar()
}
jar {
from('LICENSE') {
rename { "${it}_${base.archivesName.get()}" }
}
}The order of that block matters, and only since ploceus 1.17. Exceptions,
signatures and nests have to be declared before the mappings dependency.
Ploceus resolves those configurations at the moment ploceus.mappings(...) is
evaluated; declared afterwards they are still empty when it reads them, and the game jar is
remapped with no inner classes at all. It fails silently, so what you get is not an error but a
jar where every nested class has been flattened into net/minecraft/unmapped/.
That flattening splits packages that have to stay whole. Beta bundles the argo JSON library,
whose helper classes then sit outside argo.jdom and lose package-private access to
it, and the game dies in Minecraft.init the moment there is a stats file to parse:
IllegalAccessError: failed to access class argo.jdom.JsonFieldBuilder
from class net.minecraft.unmapped.C_35499707
at PlayerStats.deserialize -> StatsSyncer.loadStats -> Minecraft.initWhich is a confusing thing to debug, because a fresh instance starts once, writes a stats file
on exit, and then never starts again. Ploceus 1.16 called provide() eagerly so the
order never mattered; if you are copying a dependency block from an older guide, move those six
lines up. To check a jar directly, unzip the remapped client and look for
argo/jdom/JsonListenerToJdomAdapter$ — three entries means nesting worked, none
means it did not.
Two guests came along with RetroAPI, both optional but very welcome in a dev environment:
| Dependency | What it does for you |
|---|---|
retroapi | The star of this guide, registration, persistence, networking, rendering. |
retrodragon | Runs the old client on LWJGL 3 with a WebGPU renderer, modern keyboards, displays, and macOS support. Hence the LWJGL 2 exclusion just below it. |
gradle.properties, the version pinboard
# Gradle Properties
org.gradle.jvmargs = -Xmx1G
org.gradle.parallel = true
# Mod Properties
version = 1.0.0
maven_group = com.example # ← change me
archives_base_name = example_mod # ← change me
# Dependencies
retroapi_version=0.4.1
retrodragon_version = 0.1.15
retroconvert_version = 1.0.4
# Ornithe Related Dependencies - check https://ornithemc.net/develop for the latest versions
minecraft_version = b1.7.3
loader_version = 0.19.3
biny_mappings = 9859f76
client_raven_build = 2
server_raven_build = 2
client_sparrow_build = 2
server_sparrow_build = 2
biny_nests_build = 2
osl_version = 0.20.3fabric.mod.json, your passport
Three things change from the stock template: the Minecraft version becomes beta (note the loader's spelling: 1.0.0-beta.7.3), the mod now depends on RetroAPI so the loader refuses to launch without it (a crash at the door beats a crash in the basement), and the entrypoint key becomes retroapi rather than init:
{
"schemaVersion": 1,
"id": "example_mod",
"version": "${version}",
"name": "Example mod",
"description": "This is an example description! Tell everyone what your mod is about!",
"authors": [ "Me!" ],
"license": "CC0-1.0",
"icon": "assets/example_mod/icon.png",
"environment": "*",
"entrypoints": {
"retroapi": [ "com.example.example_mod.ExampleMod" ]
},
"mixins": [ "example_mod.mixins.json" ],
"depends": {
"fabricloader": ">=0.17.3",
"minecraft": "1.0.0-beta.7.3",
"retroapi": ">=0.4.1",
"osl-entrypoints": ">=0.4.0"
}
}Why retroapi and not init? Both exist and both work, but the loader runs init entrypoints in an unspecified order, so your mod can start registering before RetroAPI has finished building the platform you are registering into. The retroapi door is opened by RetroAPI itself, at the one moment when everything you might touch is ready. It is the difference between "my recipes exist" and "my recipes exist on some launches". Entrypoints & sides covers it, and its client and server halves, properly.
settings.gradle stays exactly as the template ships it, it already knows about the Fabric and Ornithe mavens that host the Gradle plugins.
Light the forge
That's the whole conversion. Prove it:
./gradlew runClientFirst run downloads the game, remaps it with biny, and decompile-links everything, give it a minute. Then Beta 1.7.3 opens with your mod loaded. Check the log for the line your ExampleMod.initRetro() printed. To produce a shippable jar:
./gradlew build
# → build/libs/example_mod-1.0.0+mcb1.7.3.jarMake it yours
- Set
maven_groupandarchives_base_nameingradle.properties. - Set
id,name,description,authorsinfabric.mod.json, theidis load-bearing: it names your assets folder, your lang keys, and every identifier you register. - Rename the
com.example.example_modpackage andexample_mod.mixins.jsonto match.
The bench is built. Next: the three doors every mod walks through at launch.