Building a mod for a lab¶
The agent may want its own observation code in the game - a logger, a tick
sampler, a command. A lab makes that a two-step loop: build a jar against
the jars the lab already has, then deploy it with lab_server.py and
restart. Minecraft 26.2 ships unobfuscated class files, so no Gradle and no
mapping download are needed; tools/build_mod.py compiles with javac
directly.
Prerequisites¶
- A provisioned lab that has started at least once. The first start makes
the Fabric launcher unpack the server jar into
labs/<lab>/versions/<mc>/server-<mc>.jarand install libraries. Those files are the compile classpath. - A JDK 25 with
javac. A JDK home can be passed with--jdk, or recorded at provision time (provision --jdk PATH), or found throughJAVA_HOMEorPATH. The lab's ownjavais remembered inlab.json, so--jdkis normally the only extra flag. A plain JRE cannot compile. - You do not need internet access for the build: everything comes from
labs/<lab>/andlabs/_cache/.
Source layout¶
my-mod/
src/main/java/... Java sources
src/main/resources/
fabric.mod.json mod metadata (required)
assets/... anything else, copied into the jar
fabric.mod.json must at least name the entrypoint and the environment, for
example:
{
"schemaVersion": 1,
"id": "example-logger",
"version": "${version}",
"name": "Example Logger",
"environment": "*",
"entrypoints": { "main": ["dev.example.logger.LoggerMod"] },
"depends": {
"fabricloader": ">=0.19.0",
"minecraft": "*",
"fabric-api": "*",
"java": ">=25"
}
}
${version} is replaced from --version when the jar is packed. Declare only
dependencies the lab actually has; Fabric refuses to start when a declared
dependency (depends) is missing, which is the correct behaviour for an
auditable instance.
environment is "*" for a server-side mod that only registers server
callbacks and commands; use "server" if it must never load in a client. A lab
is a dedicated server, so the entrypoint runs in the main stage
(net.fabricmc.api.ModInitializer).
Build it¶
python tools/build_mod.py --source examples/smoke-mod --lab lab-a `
--out labs/build/smoke-mod.jar --version 0.1.0 --compression store
Output:
building examples\\smoke-mod for lab lab-a (minecraft 26.2)
javac C:\\...\\bin\\javac.exe (javac 25.0.1)
javac @...\\javac.args (1 sources, 87 classpath entries)
built labs\\build\\smoke-mod.jar (14.8 KB, sha256 e66ebdb16d53238a...)
version 0.1.0, compression store, release 25
87 classpath entries from lab lab-a
metadata labs\\build\\smoke-mod.jar.build.json
The classpath is assembled from the lab, in this order:
labs/<lab>/versions/<mc>/server-<mc>.jar- the unpacked, unobfuscated Minecraft server classes;- every jar under
labs/<lab>/libraries/- Fabric loader, mixin, Brigadier, Gson and the rest of the server's libraries; - every jar under
labs/<lab>/.fabric/processedMods/- the Fabric API modules the running server will actually load; - every jar in
labs/<lab>/mods/- so a mod can compile against Carpet or another installed mod; - anything passed with
--classpath-extra.
Everything is compiled with --release 25 and packaged by the same tool. The
jar is deterministic: entries are sorted, timestamps are fixed, and the default
--compression store keeps equal-length edits at equal size. Rebuilding
identical sources gives byte-identical output; changing one string changes the
hash. The sidecar <out>.build.json records the jar hash and size, the JDK and
javac version, the server jar hash, a classpath digest and every source file
hash - keep it with the experiment's audit trail.
Deploy it and read the result¶
Fabric loads mods at server start, never while running. Deploy while stopped, then start:
python tools/lab_server.py stop --name lab-a
python tools/lab_server.py provision --name lab-a --mod-jar labs/build/smoke-mod.jar
python tools/lab_server.py start --name lab-a --wait 300
python tools/lab_server.py exec --name lab-a "mcagent-smoke status"
python tools/lab_server.py exec --name lab-a "mcagent-smoke sample first"
provision compares the jar with the deployed one by SHA-256. If it says
replaced, the bytes are new even when the size did not change; if it says
unchanged, the bytes really are identical. It refuses to deploy mods into a
running lab. start records the on-disk hashes in run.json (modsAtStart),
so the run is tied to the exact build.
A game-side mod writes its evidence where the lab points it: start passes
-Dmcagent.auditDir=... (and -Dmcagent.labName, -Dmcagent.labInstance,
-Dmcagent.worldDir, -Dmcagent.serverDir, -Dmcagent.serverPort) so the mod
never has to guess which lab it is in. Files under labs/<lab>/audit/ and the
console log labs/<lab>/logs/console.log are the result to read back.
The smoke mod¶
examples/smoke-mod/ is a deliberately generic observation mod: it logs
[mc-agent-smoke] loaded build=... lab=... at load time, writes
audit/start.json plus a smoke.log line when the server is ready, and adds
two console commands (mcagent-smoke status, mcagent-smoke sample <label>).
It has no ROM or use-case logic - it exists to verify the build/deploy/restart
path. Copy it as a starting point for a real logger.
When the build or the load fails¶
The tool never hides compiler output. A missing symbol looks like:
...\\SmokeMod.java:51: error: cannot find symbol
private static final Object BROKEN = doesNotExist();
^
symbol: method doesNotExist()
error: build failed: javac exited 1; the compiler output above is the error
A load failure is in the server log and in start's failure output. Fabric
prints a clear block for a missing dependency:
[main/ERROR]: Incompatible mods found!
Fix: add [add:mc-agent-absent-mod 1 (( -β,β ))], remove [], replace []
Mod 'MC Agent Lab Smoke Mod' (mc-agent-lab-smoke) 0.1.0 requires mc-agent-absent-mod ...
and for a broken entrypoint:
java.lang.ClassNotFoundException: dev.mcagent.smoke.AbsentEntrypoint
lab_server.py start exits non-zero, prints the last log lines, and repeats the
lines that look like errors (exception, failed, missing, requires,
incompatible) prefixed with !!. A lab that recorded a required mod
(--test-mod or --require-mod) refuses to start when that jar is missing, so
a broken audit environment cannot be mistaken for a clean run.
Limitations¶
build_mod.pycompiles server-side mods. A lab has server jars only; a client-side mod has to be built against an installed client instance (the interface mod's ownbuild.pyis an example of that).- The tool builds against the committed lab state. Re-provision the lab with the new jars before building if Minecraft, the loader or a mod was updated.
- Gradle/loom-style resource processing is not implemented;
fabric.mod.jsonand plain resources are copied (with${version}substitution). - A lab build has no remapping step, which is correct for the unobfuscated 26.2 server; an older, obfuscated Minecraft would need intermediary mappings and a different build path.