Development Guide¶
This guide covers everything you need to set up a development environment, build the mod, run tests, and work with the multi-version build system.
See also: Contributing for the contribution workflow, Guidelines for code style and conventions.
Prerequisites¶
| Requirement | Version | Notes |
|---|---|---|
| Java | 21 | Required โ all targets compile on JDK 21. The 1.20.1 targets use --release 17 (via javaTarget in Modstitch) to produce Java 17 bytecode, so only JDK 21 is needed. |
| Git | 2.x+ | For version control and conventional commits |
| IDE | Any | IntelliJ IDEA recommended (free Community edition works) |
| git-cliff | Latest | Changelog generation for releases โ winget install git-cliff or git-cliff.org |
Note: You do not need to install Gradle โ the project includes the Gradle Wrapper (
gradlew/gradlew.bat).
Getting Started¶
Clone and Build¶
Windows users: All commands use Unix syntax (
./gradlew). On Windows, use WSL 2 (recommended), Git Bash, or replace./gradlewwithgradlew.bat. All scripts inscripts/require bash โ run them via WSL or Git Bash.
# Clone the repository
git clone https://gitlab.com/starshadow/games/minecraft/ansible-crafting.git
cd ansible-crafting
# Build the mod (includes git SHA in version for dev builds)
./gradlew build
Run the Development Client¶
This launches a Minecraft client with the mod loaded. Hot-swap is supported for minor code changes if your IDE is configured for it.
Run the Development Server¶
Build Types¶
flowchart LR
DEV["./gradlew build"] -->|"includes git SHA"| DEV_JAR["ansiblecrafting-0.1.0+abc1234.jar"]
REL["./gradlew build -Prelease"] -->|"clean version"| REL_JAR["ansiblecrafting-0.1.0.jar"]
| Build Type | Command | Version Format | Use Case |
|---|---|---|---|
| Development | ./gradlew build |
0.1.0+abc1234 |
Local testing, CI builds |
| Release | ./gradlew build -Prelease |
0.1.0 |
Distribution, mod platforms |
Gradle Task Reference¶
| Task | Description | Example |
|---|---|---|
build |
Compile and create the mod JAR | ./gradlew build |
test |
Run JUnit 5 unit tests | ./gradlew test |
spotlessCheck |
Check code formatting (fails on violations) | ./gradlew spotlessCheck |
spotlessApply |
Auto-fix code formatting | ./gradlew spotlessApply |
runClient |
Launch Minecraft client with the mod | ./gradlew runClient |
runServer |
Launch Minecraft server with the mod | ./gradlew runServer |
clean |
Delete all build artifacts | ./gradlew clean |
buildAllVersions |
Build for all Stonecutter-configured MC versions | ./gradlew buildAllVersions |
printReleaseMatrix |
Show all targets with release channels | ./gradlew -q printReleaseMatrix |
generateDeployPipeline |
Generate CI deploy pipeline YAML | ./gradlew generateDeployPipeline |
Recommended Pre-Commit Workflow¶
# Format code, then build and test in one command
./gradlew spotlessApply build --no-configuration-cache
The --no-configuration-cache flag is needed because Stonecutter and configuration caching can conflict.
Git Hooks (Optional but Recommended)¶
The project includes git hooks in .githooks/ that automatically enforce formatting and commit message conventions:
| Hook | What It Does |
|---|---|
pre-commit |
Runs spotlessCheck on staged Java files โ blocks commits with formatting violations |
commit-msg |
Validates that commit messages follow Conventional Commits format |
To enable the hooks:
To disable them later:
Note: The hooks are opt-in. CI will still catch formatting and commit message issues even without local hooks.
Code Formatting¶
The project uses Spotless with Palantir Java Format for consistent code style.
Key rules:
- Tabs for Java indentation, 4 spaces for Gradle
- 120-character line length
- Import order: java.* โ javax.* โ net.minecraft.* โ net.fabricmc.* โ com.ansiblecrafting.* โ everything else
# Check formatting (CI will fail if this fails)
./gradlew spotlessCheck
# Auto-fix formatting
./gradlew spotlessApply
Always run
spotlessApplybefore committing. The CI pipeline runsspotlessCheckand will reject improperly formatted code.
Testing¶
The project uses a two-tier testing strategy: fast unit tests for business logic and game tests (Minecraft GameTest Framework) for integration testing inside a headless server.
Test Architecture¶
| Layer | Location | Framework | Purpose |
|---|---|---|---|
| Unit tests | src/test/java/ |
JUnit 5 | Fast, isolated tests for business logic โ runs outside Minecraft |
| Game tests | src/main/java/.../test/ |
Minecraft GameTest Framework | Integration tests running inside a headless Minecraft server |
Unit tests mirror the main source tree structure and mock Minecraft dependencies where needed. On Forge and NeoForge, some tests that require Minecraft bootstrap use assumeTrue guards to skip gracefully when the environment hasn't been initialized (see Bootstrap Guards below). Game tests live under src/main/ because they must be compiled as part of the mod to access the GameTest Framework APIs.
Running Tests¶
| Command | What It Does |
|---|---|
./gradlew :1.20.1-fabric:test |
Run unit tests for Fabric 1.20.1 |
./gradlew :1.20.1-fabric:runGameTestServer |
Run game tests in headless server |
./gradlew test |
Run unit tests for all versionรloader targets |
Additional useful commands:
# Run a specific test class
./gradlew test --tests "com.ansiblecrafting.config.ModConfigTest"
# Run with verbose output
./gradlew test --info
Test Conventions¶
See Guidelines โ Testing for full details. Key rules:
- Naming:
methodName_stateUnderTest_expectedBehavior(e.g.,scanRange_setToZero_clampsToOne) - Pattern: AAA (Arrange, Act, Assert) with comments marking each section
@DisplayNameon every test method for human-readable descriptions- No
@Disabled, no@SuppressWarningsโ tests must pass or be removed. Exception:assumeTrueis allowed specifically for Minecraft bootstrap guards on Forge/NeoForge (see Bootstrap Guards below). - Specific assertions (
assertEquals,assertNull,assertThrows) over genericassertTrue
Example:
@Test
@DisplayName("scanRange clamps to minimum of 1 when set to 0")
void scanRange_setToZero_clampsToOne() {
// Arrange
ModConfig config = new ModConfig();
// Act
config.setScanRange(0);
// Assert
assertEquals(1, config.getScanRange());
}
Coverage Expectations¶
| Category | Target | Notes |
|---|---|---|
Business logic (crafting/, inventory/, config/, network/) |
80%+ | Core mod functionality |
| Critical paths (packet serialization, state persistence) | 100% | Data integrity |
Mixins (mixin/) |
N/A | Tested via game tests, not unit tests |
Client rendering (client/HighlightRenderer, client/InventoryOverlayRenderer) |
N/A | GPU-dependent, tested via game tests |
Recipe viewer integrations (integration/) |
N/A | Thin wrappers, tested via game tests |
| Loader-specific glue code | N/A | Tested via CI build matrix |
Adding New Tests¶
Unit test: Create ClassNameTest.java in src/test/java/ mirroring the source structure.
src/main/java/com/ansiblecrafting/crafting/Foo.java
โ src/test/java/com/ansiblecrafting/crafting/FooTest.java
Game test: Add a method to AnsibleCraftingGameTest.java with the @GameTest annotation:
@GameTest(template = EMPTY_STRUCTURE)
@DisplayName("description of what is being tested")
public void featureName_condition_expectedResult(GameTestHelper helper) {
// Arrange โ set up world state using helper
// Act โ trigger the behavior
// Assert โ verify with helper.succeed() or helper.fail()
helper.succeed();
}
Bootstrap Guards¶
Forge and NeoForge test classpaths include the full Minecraft + loader JARs, which access internal JDK APIs via reflection. The build system configures several accommodations for this:
forkEvery = 1: Each test class runs in its own JVM process on Forge/NeoForge, preventing a single class-loading failure from killing the entire test executor.--add-opens/--add-exports: Extensive module-access flags are passed to the test JVM to preventInaccessibleObjectExceptionfrom Forge's class transformers and Mockito/ByteBuddy.- JAR signature stripping: Forge's universal JAR is cryptographically signed, but NFRT modifies its class files during artifact creation, causing
SecurityExceptionon digest mismatches. The build stripsMETA-INFsignature files (.SF/.RSA/.DSA) from signed JARs on the test classpath before the test JVM reads them.
Tests that require Minecraft bootstrap (e.g., registry initialization) use assumeTrue to skip gracefully when the environment isn't available:
@BeforeAll
static void checkBootstrap() {
assumeTrue(
MinecraftBootstrapReady.check(),
"Skipping โ Minecraft bootstrap not available on this loader"
);
}
This is the only approved use of assumeTrue โ it must not be used for any other purpose.
Known Gaps¶
- Game tests compiled out on NeoForge โฅ1.21.10: NeoForge removed
@GameTestHolderand changed the GameTest registration API. The entire test body is conditionally compiled out for that target (documented inAnsibleCraftingGameTest.java). InventoryStateManagerTestpersistence tests compiled out for โฅ1.21.10:SavedData.save()andCompoundTagAPIs were rewritten in 1.21.10. Persistence tests are conditionally compiled for<1.21.10only (documented inInventoryStateManagerTest.java).- Forge game tests excluded in CI:
1.20.1-forgeis excluded from the game test matrix due to EMI Forge 1.20.1 SRG mapping incompatibility, which prevents the game test mod from loading. Unit tests still run for this target.
Current Test Inventory¶
| Package | Unit Test Classes | Unit Tests | Game Tests |
|---|---|---|---|
Root (com.ansiblecrafting) |
2 | 10 | โ |
client |
2 | 22 | โ |
client.ui |
2 | 21 | โ |
config |
1 | 66 | โ |
crafting |
3 | 48 | โ |
inventory |
2 | 48 | โ |
network |
2 | 15 | โ |
| Game tests | โ | โ | 13 |
| Total | 14 | 230 | 13 |
Multi-Version Support (Stonecutter)¶
The project uses Stonecutter to support multiple Minecraft versions from a single codebase.
How It Works¶
flowchart TD
SRC["src/main/java/<br/>Shared source code"] --> SC["Stonecutter<br/>Preprocessor"]
SC -->|"MC 1.20.1"| V1["versions/1.20.1/<br/>Version-specific overrides"]
SC -->|"MC 1.20.2+"| V2["versions/1.20.2/<br/>Version-specific overrides"]
V1 --> JAR1["ansiblecrafting-*-mc1.20.1.jar"]
V2 --> JAR2["ansiblecrafting-*-mc1.20.2.jar"]
Conditional Compilation¶
Use Stonecutter comment markers for version-specific code:
/*? if >=1.20.2 {*/
// Code for Minecraft 1.20.2 and above
/*?} else {*/
// Code for Minecraft 1.20.1 and below
/*?}*/
Version-Specific Resources¶
Version-specific resource overrides go in versions/<mc-version>/src/main/resources/. These override files in the main src/main/resources/ directory.
Building All Versions¶
This produces JARs for every configured Minecraft version.
Project Structure¶
ansible-crafting/
โโโ src/main/java/com/ansiblecrafting/
โ โโโ AnsibleCraftingMod.java # Main mod entry point
โ โโโ client/ui/ # UI components (panels, widgets)
โ โโโ config/ # ModConfig, Cloth Config integration
โ โโโ crafting/ # CraftingSession, CraftingSessionManager
โ โโโ integration/ # Recipe viewer integrations
โ โ โโโ emi/ # EMI plugin
โ โ โโโ rei/ # REI plugin
โ โโโ inventory/ # InventoryScanner, AggregatedInventory
โ โโโ mixin/ # Server-side mixins
โ โ โโโ client/ # Client-side mixins
โ โโโ network/ # Client-server packet definitions
โโโ src/main/resources/
โ โโโ assets/ansiblecrafting/ # Textures, lang files
โ โโโ fabric.mod.json # Fabric mod metadata
โ โโโ ansiblecrafting.mixins.json # Mixin configuration
โโโ src/test/java/com/ansiblecrafting/ # Unit tests
โโโ versions/ # Stonecutter version overrides
โโโ scripts/ # Release scripts, splash taglines
โโโ docs/ # Technical documentation
โโโ build.gradle # Build configuration
โโโ gradle.properties # Mod metadata and dependency versions
โโโ stonecutter.gradle # Multi-version build config
โโโ cliff.toml # git-cliff changelog config
For a deeper dive into the architecture, see Architecture Overview.
Recipe Viewer Development¶
The mod integrates with both EMI and REI. You can switch which recipe viewer loads in the dev environment by editing gradle.properties:
Set to emi, rei, or none and re-run ./gradlew runClient.
Debug Logging¶
For detailed runtime logging during development, see the Debug Logging Guide.