Skip to content

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 ./gradlew with gradlew.bat. All scripts in scripts/ 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

./gradlew runClient

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

./gradlew runServer

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
# 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.

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:

# From the ansible-crafting/ directory
git config core.hooksPath .githooks

To disable them later:

git config --unset core.hooksPath

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 spotlessApply before committing. The CI pipeline runs spotlessCheck and 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
  • @DisplayName on every test method for human-readable descriptions
  • No @Disabled, no @SuppressWarnings โ€” tests must pass or be removed. Exception: assumeTrue is allowed specifically for Minecraft bootstrap guards on Forge/NeoForge (see Bootstrap Guards below).
  • Specific assertions (assertEquals, assertNull, assertThrows) over generic assertTrue

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 prevent InaccessibleObjectException from 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 SecurityException on digest mismatches. The build strips META-INF signature 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 @GameTestHolder and changed the GameTest registration API. The entire test body is conditionally compiled out for that target (documented in AnsibleCraftingGameTest.java).
  • InventoryStateManagerTest persistence tests compiled out for โ‰ฅ1.21.10: SavedData.save() and CompoundTag APIs were rewritten in 1.21.10. Persistence tests are conditionally compiled for <1.21.10 only (documented in InventoryStateManagerTest.java).
  • Forge game tests excluded in CI: 1.20.1-forge is 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

./gradlew buildAllVersions

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:

# Which recipe viewer to load in dev environment (emi, rei, or none)
recipe_viewer = emi

Set to emi, rei, or none and re-run ./gradlew runClient.


Debug Logging

For detailed runtime logging during development, see the Debug Logging Guide.