Repository navigation
Development
Building and testing Legacy Connector. Requires JDK 21 (newer JDKs fail with "Unsupported class file major version"). Back to Home.
Requires JDK 21.
./gradlew build # runtime mod + loader (also builds the distributable jar)
./gradlew bundleJar # single distributable jar: build/dist/legacyconnector-<ver>.jar, the ONLY jar for a mods folder
# (build/libs/*-runtime-only.jar has no loader: every legacy mod is then rejected as built for an old Minecraft)
./gradlew runClient # dev client, mods from run/mods
./gradlew runServerTest # headless smoke test with run-servertest/mods; the fixture mod self-tests and stops
LEVELTYPE=biomesoplenty:biomesop tools/dev/run-subset.sh biomesoplenty # same, in a mod's world type
tools/dev/run-subset.sh hbm thaumcraft # server test over a few mods only, with the
# block/item/entity self tests; the whole corpus takes minutes
./gradlew runClientTest # client smoke test (run-clienttest, mods linked from run/mods): reports per mod
# which models show the missing texture, then quits
tools/dev/run-screenshot-test.sh agricraft appliedenergistics # graphical test: creates a world (WORLDTYPE=biomesoplenty:biomesop
# for BoP), builds block/item showcases, writes screenshots to run-screenshots/shots
SCENARIO=village tools/dev/run-screenshot-test.sh witchery agricraft # flies over the nearest village (legacy village components)
SCENARIO=gui MODS=rftools tools/dev/run-screenshot-test.sh rftools mcjtylib # right-clicks each block of the mods and
# screenshots the screens that open (custom GUIs); SCENARIO=portal tests a Twilight Forest portal
tools/dev/docker-headless.sh run-screenshot-test.sh ... # same, inside a Linux container with a virtual display
# (Xvfb + Mesa; needs `colima start`), so no window opens on the desktop;
# also works for run-client-test.sh
tools/dev/session.sh start <jar fragments> # one long-lived headless client you drive without restarting it:
tools/dev/session.sh send cmd "legacyconnector dim 7" # run a command / open GUIs / `mouse x y` / `click x y` / `useitem` / `inv` / `info` ...
tools/dev/session.sh shot name # screenshot -> ~/.cache/legacyconnector-docker/work/run-screenshots/control/out/name.png
tools/dev/survey.sh <namespace> <name> # places every block of a mod on a sky platform and screenshots it
./gradlew checkLegacyMod -PlegacyJar=/path/a.jar,/path/b.jar # e.g. every jar in test-mods/
# translate offline as one modset, list unlinkable references,
# generate + JVM-verify the synthesized API (no game launch)Development commands (operator, any world): /legacyconnector mods lists the loaded legacy mods;
/legacyconnector dim list|<id> [pos] switches to a legacy dimension; /legacyconnector gui <mod> <id> [pos] opens a mod
GUI by its IGuiHandler id; /legacyconnector place <block> [meta], give <item> [meta] [count] and spawn <entity> [count]
create legacy content with metadata; /legacyconnector list <blocks|items|entities> <mod> prints a mod's ids; /legacyconnector biome <id> jumps to a legacy biome; /legacyconnector inspect
describes the block or entity you look at (legacy class, metadata, tile entity, render type). Ids tab-complete.
Project layout:
| Path | What |
|---|---|
loader/ |
FML locator service: jar inspection, translation, linking, API synthesis, caching, report |
src/main/java/legacyconnector/v1_7_10/ |
1.7.10 shim (legacy package names, relocated) |
src/main/java/org/kylepitcock/legacyconnector/ |
runtime: bootstrap, bridges to NeoForge, JDK/library compat, synth runtime |
testmods/ |
fixture mod compiled against legacy API stubs, built like a real 2014 jar |
tools/ |
developer scripts: reference data download, signature/hierarchy extraction, API lookup, shim hierarchy audit |
tools/docker/, tools/dev/
|
headless-display container image; dev test scripts (subset server test, client/screenshot tests) |
test-mods/ |
(not committed) local corpus of real legacy mods for checkLegacyMod / runServerTest
|
Reference data (MCP mappings, Forge userdev sources, Mojang client jars) is downloaded into build/devtools/ by
tools/fetch_devtools.sh and the extraction scripts; none of it is committed. Only class names and supertypes
(hierarchy.txt) and MCP name tables are shipped.
To regenerate the 1.7.10 hierarchy data:
tools/fetch_devtools.sh
python3 tools/extract_legacy_signatures.py 1.7.10 # needs build/devtools/mojang/client-1.7.10.jar
python3 tools/extract_legacy_hierarchy.py 1.7.10-
./gradlew compileJava --rerunis the compile check (compileJava is non-incremental: the shim access widener rewrites class files), then./gradlew generateShimIndex, which catches "phantom field access" errors: shim code must read legacy fields that exist only as virtual fields through themcc$get$/mcc$set$accessors. -
tools/dev/run-corpus.sh [dir]runs the server self test over a whole directory of jars. Run only one server at a time and never compile while one runs. - Translation results are cached per jar and mod set (
run-*/.legacyconnector/cache). After changing the loader, linker, stubber or shim, delete the cache or the old translation is reused. - On a Windows desktop (no Docker)
tools/dev/quick-shots.shruns the graphical test directly in about a minute (superflat world, short render distance, a dozen blocks and items per mod); mods go inrun-screenshots/mods. - Headless server probing (no window):
./gradlew runServerSubTest -PautostopTicks=0 -PserverScript=probe -PserverScriptMod=<legacy mod id>runs a long-lived server that executes the*.jsfiles dropped intorun-subtest/probeon the server thread (Nashorn,core/ServerScript.java);tools/dev/server-script.sh -f file.jssubmits one and prints the answer (globals persist; a functiononTick(n)runs every tick).tools/dev/fisk/lib.jsis a library for it: it joins fake clients (a captured connection that answers keep-alives and summarises every packet the server sends, so data syncing and custom packets can be checked), puts a hero suit on, drives data vars and interactions and runs multi-tick scenarios (scenario([step, ...]), a step may return the ticks to wait;tools/dev/fisk/wait.shwaits and prints). Theprobe-*.jsand*-sweep.jsfiles there are the Fisk's Heroes ability checks (damage, beams, web, shield throw, sizes, gravity, healing, recipes, machines, items). A fake client never moves its chunk view by itself:tp()tells the chunk map.tools/dev/fisk/probe-client-keys.jsis the client-side counterpart (run withtools/dev/js.sh -fin a control session wearing the Mk50 chest piece): it records, per client tick, every change of the worn suit, the nanites data, sprint/sneak state, the ability keys and the key hint list the HUD draws, andFLK.report()prints them (the key hint flicker and the dead Iron Man blade/cannon of issue #118). - Live probing: start a session with
SCENARIO=control tools/dev/quick-shots.sh, thentools/dev/js.sh '<JavaScript>'evaluates in the running game (bindingsmc,server,log; any class viaJava.type(...)). - Headless server probing:
./gradlew runServerSubTest -PserverTickScript=<file.js> [-PserverTickScriptFrom=200]evaluates the file on every server tick (bindingsserver,tickcounted from the first run,log; globals persist, so place on one tick and read on the next); output is logged asSERVERSCRIPT:. Example: count a mod's blocks per height over the loaded overworld chunks.tools/dev/server-scripts/lib.js(prepend it:cat lib.js script.js > run.js) addsrealPlayer(name)(a real, non-fakeServerPlayeron an embedded channel, so mods that skip fake players keep progression data),placeTile(right-click placement with the item),P(label, fn)(catch + short stack) andlegacy(...)/lworld(...)wrappers;chromaticraft-*.jsthere are worked examples (research walk and a finished casting-table cast, dimension 60 stay, machines). -
tools/dev/thaumcraft/lib.js(fake player,scanAt= what the Thaumometer computes for a block) andscan-thaumometer.js(realItemThaumometer.doScanon placed blocks, PASS/FAIL per block) run on a-PserverScript=run-subtest/probe -PserverScriptMod=thaumcraftserver (submitlib.jsfirst; the directory is relative torun-subtest). Place blocks inside chunk 0,0 only: a block in an unloaded chunk makes the script block the server thread on chunk generation.SERVERSCRIPT:. Example: count a mod's blocks per height over the loaded overworld chunks. Ready-made gameplay rigs live intools/dev/server-scripts(lib.jsplaces items through a fake player'suseOn, forceloads the area and reads tiles/fields; ElectriCraft generator -> wire -> motor/battery/RF cable chains and ReactorCraft fission reactor, steam turbine -> shaft and radiation rigs). Gotchas:forceload addtakes block coordinates, not chunk coordinates (otherwise the rig unloads after one tick); legacy mods that kill entities outside y 0..256 (DragonAPI particle entities such as ReactorCraft neutrons) need rigs at y >= 0; calling methods on a tile whose class mentions an absent optional API (BuildCraft) from Nashorn throwsNoClassDefFoundError, so read fields viaf(). - Gameplay scenarios without a window:
tools/dev/run-server-script.sh <scenario> [autostopTicks] [firstScriptTick] [--fresh]runs a file oftools/dev/server-scripts/scenarioswith the helpers oflib.js(platform, placement through the real item use with a fake player, field access, a task scheduler) andrig.js(engine, shafts, machine in a line, also machines fed from below through a bevel gear). The RotaryCraft set builds real engines, gearboxes, clutches and machines, powers them and asserts power, fluids, items, GUIs (containers and shift-click), capabilities (items, fluids, RF), persistence across a restart (persist, run twice) and the handbook data. Jars go inrun-subtest/mods/1.7.10. - Legacy mod jars are supplied by the developer and never committed (
test-mods/is gitignored).
The server self test (-Dlegacyconnector.entitySelfTest=true, on in runServerSubTest) also runs core/SilentFailureChecks once at server start. They are read-only and target bugs that crash nothing and log nothing, which link and translation checks cannot see (Sinytra Connector's "attribute registration bypassed, SWIM_SPEED stayed 0, nobody can swim" class). Output: SILENT FAILURE CHECKS: N passed, M failed, then one SILENT FAILURE CHECK FAILED: <check>: <offenders by registry name> line at ERROR level per failing check (tools/dev/run-subset.sh prints them). Add -Dlegacyconnector.selfTestFailFast=true to halt the JVM with exit code 3 on any failure.
What is asserted:
- Attributes: the default attribute map of every legacy living-entity host type of both versions (and the player map) exists and has a positive base value for max health, movement speed, step height, gravity, safe fall distance, fall damage multiplier, scale, NeoForge
SWIM_SPEEDandNAMETAG_DISTANCE, plus follow range and attack damage for mobs (attack damage and block break speed for the player). - Network: no
legacyconnectorpayload is registered for the configuration phase (login-phase payloads are not possible through NeoForge's registrar; a handler in either early phase would break proxies and vanilla-compatible joins), the play-phaseLegacyPayloadis registered, encoding aLegacyPayloadtwice gives identical bytes (NeoForge's packet splitter may run an encoder more than once), encoding does not mutate the payload, and decode(encode(p)) equals p. - Registries: every host entity type is registered under its host id; every non-vanilla block has a default state, every non-vanilla item is not air and has a non-empty default stack, block items place registered blocks, and every non-vanilla sound event resolves to its own location.
Not covered: music and sound playback (client only; a global hook that cancels or swallows sound events cannot be seen on a dedicated server), proxy login (Velocity modern forwarding needs a real proxy), client rendering (missing models, renderers, textures; see the screenshot tests).
Like the Sinytra Connector checker, tools/compat/probe.py translates mod jars offline, without starting the game, and records whether every
reference links:
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
python3 tools/compat/probe.py test-mods2 --mc 1.7.10 --version 0.3.18 # --dry-run to preview, --json out.json keeps the raw records
python3 tools/compat/probe.py --from-json out.json --mc 1.7.10 # convert an earlier run
- It runs
./gradlew checkLegacyMod -PlegacyJar=... -PcheckJson=<file>on all jars as ONE mod set (mods that extend each other link), then merges the per-jar records intodocs/data/mods.jsonwith sourceprobe. -
links(every reference resolves; missing legacy API is synthesized) becomes statusloads,unresolvedbecomesfailswith the first unresolved references in the note. Engine patches (obsolete) and library jars without a mod are not listed. - A probe proves only that a jar translates and links, never that it works in game. The site ranks it below player reports, maintainer, client and startup-test results, and the script never overwrites those. Re-running refreshes earlier probe entries.
- Every run is also appended to
docs/data/probe.json(counts per result plus the per-mod list of the last 10 runs per version), which the pie chart pagedocs/probe.html("Automated results" on the compatibility site) shows with a run selector and a history table. -
Jar sources.
tools/compat/fetch_probe_mods.py <mc> <dir> [--limit N]downloads the most downloaded Forge mods of a version from Modrinth's public API (SHA-1 checked). The jars are only for the probe: keep them in a gitignored or temporary folder, never commit or redistribute them. -
CI.
.github/workflows/probe.ymlruns fetch + probe for 1.7.10 and 1.12.2 on every published release, weekly and on demand (workflow_dispatch, inputlimit), then commitsdocs/data/mods.jsonanddocs/data/probe.json. The jars exist only on the runner. Local runs against your own corpus (test-mods2) work the same way, only the committed results matter.
Fixes are generic first (see CLAUDE.md). tools/dev/check_mod_specific.py enforces it: it scans the Java sources (comments ignored) for the
names in tools/dev/mod-names.txt and fails when one appears outside the designated places, which are any compat/ package and the shim of
another mod's API (legacyconnector/v*/codechicken, cofh, baubles, thaumcraft, ...). Mentions that existed when the check was added are frozen
in tools/dev/mod-specific-baseline.txt per file and can only go down: python3 tools/dev/check_mod_specific.py --update lowers the numbers after you
remove some. A new mod-specific fix goes under a compat/ package, labelled and keyed by mod id, so it stays easy to remove. If outside contributors
start sending per-mod fixes, the next step is a plugin interface plus a separate optional jar for them (how Sinytra Connector does it with ConnectorExtras).
checkLegacyMod also runs loader/.../tools/BytecodeCheck on every translated jar: ASM's data-flow analyzer over each method, with type relations answered by
Hierarchy (translated mod, shim, synthesized API, libraries). It finds what the JVM rejects as a VerifyError when the class first loads (a retyped value
used as the wrong type); methods touching a type nobody knows (a mod that is not installed) are skipped. The line bytecode verification: N method(s) analysed, M failing is printed per jar, and failures go into the JSON record as verifyFailures / verifyErrors.
./gradlew checkLegacyMod (and so tools/compat/probe.py) also runs loader/.../tools/ResourceCheck on the converted assets of every jar, against the
jars of the same set and the vanilla 1.21.1 client jar (found in the Gradle cache, or -Dlegacyconnector.check.vanillaJar=...; switch off with
-Dlegacyconnector.check.assets=false). It finds the references that point at nothing, which is how nearly every purple-and-black texture and invisible
block starts: a model's parent, a literal texture, an item model override, a blockstate variant's model, and an element face's #variable that the parent
chain never defines (checked only for models a blockstate or the item folder uses directly, since templates are completed by their children); plus JSON that
does not parse and PNGs without a readable header.
Every problem gets a verdict so the layer is not blamed for the mods' own defects: layer (the original jar ships the file, or it is a vanilla file the
rename tables miss, but it is not there after translation) and mod (no original jar ships it, so the game showed it missing in 1.12 too). References into
the namespace of a mod that is not in the set are ignored (optional integrations). The probe record keeps both counts and the first examples ([layer] ones
first); the site shows them in the run summary and in each mod's detail, and tools/compat/missing_report.py lists the mods that lose the most.
What it cannot see: what a texture looks like, animations, lighting, translucency order, shaders, anything drawn by code, and 1.12 Forge-format blockstates
(translated at runtime, so they are skipped; blockstate models in the vanilla namespace that the runtime generates can show up as possible layer problems).
For those use the headless client tests (tools/dev/survey.sh, quick-shots.sh).