Skip to content

Building from Source

Warlon Mhite edited this page Nov 8, 2026 · 3 revisions

Home › Contributing › Building from Source

How to build Hempdustry yourself, run it in a development client, regenerate its data and run its tests. For contributors and for anyone who wants a jar from the source; players should use the releases instead.

On this page

What you need

  • JDK 21. The build asks Gradle for a Java 21 toolchain and compiles to Java 21. On the 26.x lines, JDK 25 as well: the mod compiles to Java 25 there, and NeoForge's build tools still run on 21. On the Forge lines, JDK 17 and 21 for 1.18.2 (Gradle runs on 21, the game on 17), and JDK 21, 17 and 8 for 1.16.5 (Gradle runs on 21, it compiles with 17 to Java 8, and its dev runs use Java 8, as 1.16.5 does). Gradle finds installed JDKs itself; if it does not, list them in org.gradle.java.installations.paths in ~/.gradle/gradle.properties.
  • Git, to clone the repository (and the gates use it).
  • Python 3, only for the checking scripts in scripts/. They use the standard library alone; CI runs them on Python 3.12.
  • Nothing else: Gradle comes with the repository (./gradlew, or gradlew.bat on Windows) and downloads Minecraft, the loaders and the recipe viewers itself.
git clone https://github.com/warlon-mhite/hempdustry-fabric.git
cd hempdustry-fabric
./gradlew build

The first build takes a while: it downloads and decompiles Minecraft for both loaders. To work in an IDE, open the folder in IntelliJ IDEA (or any IDE that imports Gradle projects) as a Gradle project.

The Minecraft versions

Hempdustry is built for Minecraft 26.3, 26.2, 1.21.11 and 1.21.1, each on Fabric and NeoForge, and for 1.18.2 and 1.16.5 on Forge (below). Each Minecraft version is a separate line of the same repository, on its own branch, with the same layout, the same tasks and the same scripts; pick the one you want from the branch list on GitHub. What differs:

26.3 line 26.2 line 1.21.11 line 1.21.1 line
Java 25 (and 21 for NeoForge's tools) 25 (and 21 for NeoForge's tools) 21 21
Fabric Loader 0.20.1 0.20.1 0.18.2 0.16.5
Fabric API 0.165.0+26.3 0.161.0+26.2 0.139.4+1.21.11 0.105.0+1.21.1
NeoForge 26.3.0.101 26.2.0.93 21.11.45 21.1.209
Names Mojang's, shipped with the game Mojang's, shipped with the game Parchment 1.21.11 · 2025.12.20 Parchment 1.21.1 · 2024.11.17
Recipe viewers in the dev client JEI (default), REI JEI (default), REI on Fabric JEI (default), REI EMI (default), JEI, REI
Content only this line has the 26.x extras the 26.x extras — —
How a game test is written a function plus a test_instance file a function plus a test_instance file a function plus a test_instance file an @GameTest method

The loader versions are both what the mod is built against and the oldest it supports; gradle.properties holds them all. A change usually belongs on every line. If you only built and tested one, say so in your pull request.

The Forge lines

The 1.18.2 and 1.16.5 lines are Forge only, laid out as common/ and forge/, with no Fabric or NeoForge project:

1.18.2 line 1.16.5 line
Java 17 (Gradle on 21) compiled by 17 to Java 8 (Gradle on 21); dev runs on 8
Forge 40.3.12 (ModDevGradle's legacy plugin) 36.2.39 (Architectury Loom)
Names Parchment 1.18.2 · 2022.11.06 Parchment 1.16.5 · 2022.03.06
Recipe viewers in the dev client JEI (default), REI JEI

The tasks take the forge project: ./gradlew :forge:runClient (with the same -Pviewer), :forge:runServer and :forge:runGameTestServer; ./gradlew build makes forge/build/libs/hempdustry-forge-<version>+<minecraft>.jar. These lines have no data generators: their data is converted from a newer line's generated data by scripts/convert_data_118.py or convert_data_116.py (the docstring says how), so a data change starts on the newer line. scripts/gates.py runs the gates that apply there.

The Gradle tasks

Name the project in every run task: a bare runClient starts both loaders, one after the other.

Task What it does
./gradlew build Builds both jars: fabric/build/libs/ and neoforge/build/libs/, named hempdustry-<loader>-<version>+<minecraft>.jar
./gradlew :fabric:runClient A development client with the mod, on Fabric
./gradlew :neoforge:runClient The same on NeoForge
-Pviewer=jei, -Pviewer=rei, -Pviewer=none Added to a runClient: which recipe viewer the dev client loads. JEI is the default. On the 1.21.1 line -Pviewer=emi too, and EMI is the default there
./gradlew :fabric:runServer A development dedicated server on Fabric (:neoforge:runServer on NeoForge)
./gradlew :fabric:runDatagen Regenerates the shared data: models, recipes, loot tables, tags, advancements, worldgen, strains and the balance data
./gradlew :neoforge:runData Regenerates the few files only NeoForge reads: compost chances, furnace fuel and the wild patches' biome modifiers (on 26.3 only the biome modifiers: compost and fuel are shared data there)
./gradlew :fabric:runGametest Boots a headless server and runs every game test, on Fabric
./gradlew :neoforge:runGameTestServer The same on NeoForge

The dev client takes one recipe viewer at a time on purpose: with two in one client, which one owns the screen depends on load order (and on the 1.21.1 line, EMI switches JEI's plugins off when both are present). -P reaches only a build started from the terminal: to use another viewer from the IDE, put viewer=rei (or whichever) in gradle.properties or ~/.gradle/gradle.properties and re-sync.

Each loader keeps its own dev world and logs in its own run/ folder (fabric/run, neoforge/run). Don't open one loader's saves with the other.

The project layout

common/      the mod itself: content, logic, mixins, assets, data, the recipe-viewer plugins, the game tests
fabric/      Fabric's entry points, its side of the platform interfaces, the addon events, the data generators
neoforge/    NeoForge's entry points, its side of the platform interfaces, the addon events, NeoForge-only data
buildSrc/    shared build logic: Java 21, repositories, the version, the licences packed into each jar,
             and how a loader's project compiles common's sources into its own jar
scripts/     the checking scripts, and gates.py, which runs them all
  • common/ is plain Minecraft, with no loader. It compiles against vanilla alone, so an import from Fabric or NeoForge there fails the build. Its own jar is never shipped: each loader's project compiles common's sources against its own Minecraft and packs common's resources into its own jar, so every class has the same package and name in both jars.
  • The platform interfaces (com.warlonmhite.hempdustry.platform) are how common code asks for what each loader does its own way: registration, events, menus, item transfer to pipes and hoppers, player data, the addon events, recipe lookup, client registration. Each loader ships one implementation of each, found at run time through Services.
  • Fabric's datagen lives in fabric/ because it runs on Fabric, but what it writes is loader-neutral and goes to common/, which ships it in both jars.
  • Mappings: both loaders build in Mojang's official names, with Parchment on top for parameter names and javadoc. Code reads the same on Fabric and NeoForge, in the names NeoForge itself uses.
  • What vanilla keeps closed that the mod needs opened is listed twice, in common/src/main/resources/hempdustry.accesswidener (Fabric's form) and META-INF/accesstransformer.cfg (NeoForge's). Change the two together.

Generated and hand-written resources

Most of the mod's data is generated, and the rest is written by hand. Knowing which is which saves a lot of confusion.

Folder What is in it How it changes
common/src/generated/resources models, recipes, loot tables, tags, advancements, worldgen, strains, balance data generated by :fabric:runDatagen from the providers in fabric/src/main/java/.../datagen/
neoforge/src/generated/resources NeoForge's compost and fuel data maps and biome modifiers generated by :neoforge:runData, from the same calls Fabric makes in code
common/src/main/resources textures, sounds, lang files, the blockstates and models of the crops, machines and grow blocks, paintings, music, the banner pattern, biome tags hand-written
  • The generated folders are committed, and must stay committed. Every jar ships them, so a fresh clone has to build a working mod without running datagen first. Never delete them, and never commit a mass deletion under them: a mod built without its generated data loads with no recipes, loot or worldgen, and the first symptom is a crash on world load that points nowhere near the cause. (gradlew clean leaves them alone.) If git status ever shows hundreds of deleted generated files, run :fabric:runDatagen rather than committing it.
  • Never hand-edit a generated file. Change the provider and run the data run. The next run would overwrite your edit anyway, or worse, leave it in place because its cache thinks nothing changed.
  • When to run which: :fabric:runDatagen after changing any provider: a recipe, loot table, tag, model, advancement, worldgen feature, strain or balance file. Also :neoforge:runData after changing a compost chance, a fuel or a feature added to biomes, since those are data on NeoForge.
  • A stale or hand-edited generated file survives a data run, because the generators keep a cache of what they last wrote. Delete the cache and run again: rm -rf common/src/generated/resources/.cache then ./gradlew :fabric:runDatagen (and neoforge/src/generated/resources/.cache for NeoForge's). The .cache folders are not committed.

The game tests

The game tests run real server code against the real registry, with no client and no player. They live in common/src/gametest, are plain Minecraft like the rest of common, and both loaders run them: :fabric:runGametest and :neoforge:runGameTestServer. Each run exits non-zero if any test fails. Nothing in them reaches the mod jar; a small test mod of its own carries them.

What they hold the mod to:

  • Sweeps over the registry. Rather than one test per item, most checks walk every item, block, recipe, tag and block entity of the mod, so new content is held to every rule the day it is registered: no two recipes take the same ingredients, no recipe reads a stack size, every container spills when broken and survives a save, every block state survives a tick and a break, every stack survives saving and the network, every tag has something in it, every item can be had in survival, every config extreme stays sane, every tooltip names a real key, every translation keeps its placeholders. A new feature needs a test of its own only for behaviour no sweep already states.
  • The log gate. A game test run fails if the server logs any warning or error nobody expected, even when every test passes. A datapack file the game refuses or a tag that resolves to nothing says so only in the log. The expected lines are listed in fabric/build.gradle and neoforge/build.gradle, each with the reason it is expected; if your change logs a warning on purpose, add it there with its reason.
  • The frozen-surface test (the_frozen_surface_holds). Each public release's ids, block state values, advancement criteria, asset paths and config keys are recorded in common/src/gametest/resources/frozen/, and the test fails if a build has lost any of them. This is the Stability Promise as a test. Additions are free; removing something on purpose means an entry in frozen/retired.txt saying how old worlds and packs survive it, and that is a conversation to have first.
  • Old data still loads (old_data_still_loads): a released build's data files, stacks and machine contents still decode.

Adding a test on the 1.21.11 and 26.x lines: a function taking a GameTestHelper, one register(...) line in HempdustryGameTests.registerAll(), and one JSON in common/src/gametest/resources/data/hempdustry-gametest/test_instance/. A JSON with no function, or a function with no JSON, stops the test server at start-up rather than silently skipping. On the 1.21.1 line a test is an @GameTest method in a class listed in HempdustryGameTests.CLASSES (Minecraft changed its test framework in 1.21.5, so tests don't copy between the lines unchanged). Something only one loader can answer goes through TestPlatform.

Tip: a new test is only finished once you have watched it fail. Break the thing it guards, run it, read the failure message, then put the guard back.

The gates

python3 scripts/gates.py runs every check a change has to pass, cheapest first, and prints one table of what each said. The full output of each gate is kept in build/gates/.

Gate What it checks
lang_coverage.py en_us names every item, block and item tag the mod has, and no name is left over from something renamed
use_with_item_pass.py no block swallows a right-click with an item in hand, which would stop its screen opening
biome_tint_pairing.py every plant is tinted on both sides: its model asks for the tint, and the client registers a colour for it
tall_plant_loot.py every plant taller than one block can only pay out once when broken
recipe_collisions.py no two crafting recipes, ours or vanilla's, can match the same grid
game tests both loaders' game tests, each with its log gate
cold data runs :fabric:runDatagen and :neoforge:runData from an empty cache, which must leave the committed generated files exactly as they are
build both jars build
api_additive.py when given a released jar (--released <jar>, --released-neoforge <jar>): nothing an addon compiled against that release could call has gone
  • --quick leaves out the cold data runs and the build, for while you iterate.
  • Run a build once before the first gates run. Two scripts read files from Gradle's cache: lang_coverage.py reads Fabric API's own tag names, and the recipe check reads vanilla's recipes out of the game jar.
  • Commit your generated files before the full run. The cold data runs compare against what git has, so freshly generated but uncommitted files count as a change.
  • api_additive.py needs javap on the PATH, which comes with the JDK.

Each script can also run on its own, e.g. python3 scripts/recipe_collisions.py common/src/generated/resources <extracted vanilla data folder>; the docstring at the top of each says how.

CI

.github/workflows/gates.yml runs the gates on every push and every pull request, on Linux with Java 21 (21 and 25 on the 26.x lines, 17 and 21 on 1.18.2, 8, 17 and 21 on 1.16.5) and Python 3.12: it compiles both loaders, fetches the last release's jars so the API check has something to hold the build to, runs scripts/gates.py, and keeps the logs of a failed run as a downloadable artifact. Get a pull request green in CI before asking for a review; running the gates yourself first saves a round trip.

Where new code goes

You are adding It goes
content, logic, a block entity, a recipe type, a screen, a renderer common, registered the way the existing holders (ModItems, ModBlocks, …) do it
something vanilla keeps closed both access files (see the layout)
something only a loader can do a method on a platform interface, implemented on both loaders in the same change
a datapack registry Hempdustry.registerDynamicRegistries(), never init(): NeoForge asks for those before anything else
a compost chance, a fuel or a feature in biomes the registration call in Hempdustry.init, then both data runs
a recipe viewer page common/.../compat/, then check it in both viewers on both loaders
a game test common/src/gametest, then watch it fail on both loaders
a mixin common's mixin config, since mixins target vanilla; a loader's own only to cover a gap in that loader

Addon authors building on the mod rather than in it want Addon Development instead.

See also: Contributing · Stability Promise · Addon Development · Translating

Clone this wiki locally