Steve's Simple Storage uses a multi-module Gradle project with ModDevGradle 2.0.141. The project is split into two modules:
- core - Platform-agnostic code compiled against vanilla Minecraft (via
neoFormVersion) - neoforge - NeoForge-specific code (registration, config, packet handlers, JEI integration)
- Java: 21 or higher (required for NeoForge 1.21.1)
- Gradle: 8.10.2 (provided by wrapper)
- Memory: 4GB+ RAM recommended for development
- Storage: 2GB+ free space for dependencies and build artifacts
- IDE: IntelliJ IDEA (recommended) or Eclipse
- Git: For version control
- Internet: For dependency downloads
steves-simple-storage/
├── build.gradle # Root: shared subproject config (Java 21, repos)
├── settings.gradle # Includes core and neoforge modules
├── gradle.properties # Mod version and dependency versions
├── gradlew / gradlew.bat # Gradle wrapper
├── core/
│ ├── build.gradle # neoFormVersion (vanilla MC only), JUnit
│ └── src/
│ ├── main/java/ # Platform-agnostic mod code
│ ├── main/resources/ # Assets and data (textures, models, recipes, etc.)
│ └── test/java/ # Unit tests
├── neoforge/
│ ├── build.gradle # Full NeoForge, depends on :core
│ └── src/
│ ├── main/java/ # NeoForge-specific code
│ ├── main/resources/ # neoforge.mods.toml, log4j2.xml
│ └── generated/resources/ # Datagen output
└── docs/ # Documentation
Shared config applied to both subprojects:
subprojects {
apply plugin: 'java'
version = rootProject.mod_version
group = 'io.github.scuba10steve.s3'
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
repositories {
mavenCentral()
maven { name = 'ProgrammingLife'; url = 'https://maven.blamejared.com' }
}
}Uses neoFormVersion for vanilla Minecraft classes only (no NeoForge APIs):
plugins {
id 'net.neoforged.moddev' version '2.0.141'
}
base { archivesName = 's3-core' }
neoForge {
neoFormVersion = "${minecraft_version}-20240808.144430"
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.14.3'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test { useJUnitPlatform() }Full NeoForge with all run configs, depends on :core:
plugins {
id 'net.neoforged.moddev' version '2.0.141'
}
base { archivesName = 's3' }
neoForge {
version = neoforge_version
runs {
client { client() }
server { server() }
gameTestServer {
type = "gameTestServer"
systemProperty 'neoforge.enabledGameTestNamespaces', 's3'
}
data {
data()
programArguments.addAll(
'--mod', 's3', '--all',
'--output', file('src/generated/resources/').getAbsolutePath(),
'--existing', file('src/main/resources/').getAbsolutePath(),
'--existing', rootProject.file('core/src/main/resources/').getAbsolutePath()
)
}
}
mods {
s3 {
sourceSet sourceSets.main
sourceSet project(':core').sourceSets.main
}
}
}
dependencies {
implementation project(':core')
compileOnly "mezz.jei:jei-1.21.1-common-api:19.27.0.336"
compileOnly "mezz.jei:jei-1.21.1-neoforge-api:19.27.0.336"
runtimeOnly "mezz.jei:jei-1.21.1-neoforge:19.27.0.336"
testImplementation 'org.junit.jupiter:junit-jupiter:5.14.3'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test { useJUnitPlatform() }Key detail: The mods block must include both sourceSets.main and project(':core').sourceSets.main so NeoForge discovers classes from both modules as part of the s3 mod.
minecraft_version=1.21.1
neoforge_version=21.1.218
mod_version=0.12.0
mod_id=s3
mod_name=Steve's Simple Storage
mod_license=MIT
mod_authors=zerofall, SBlectric
mod_description=Massive, scalable storage for Minecraft.pluginManagement {
repositories {
gradlePluginPortal()
maven { name = 'NeoForged'; url = 'https://maven.neoforged.net/releases' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
rootProject.name = 'steves-simple-storage'
include 'core'
include 'neoforge'# Build the mod (neoforge module produces the installable JAR)
./gradlew :neoforge:build
# Run core module unit tests
./gradlew :core:test
# Build everything
./gradlew build
# Clean build artifacts
./gradlew clean
# Run development client
./gradlew :neoforge:runClient
# Run development server
./gradlew :neoforge:runServer
# Run game tests
./gradlew :neoforge:runGameTestServer
# Run datagen
./gradlew :neoforge:runData# Build with full clean
./gradlew clean build
# Run tests with detailed output
./gradlew :core:test --info
# Build without daemon (CI/CD)
./gradlew build --no-daemon
# Refresh dependencies
./gradlew build --refresh-dependencies
# Compile only (no JAR)
./gradlew :core:compileJava :neoforge:compileJavaThe multi-module split uses a platform abstraction layer to decouple core code from NeoForge APIs:
S3Platform- Central static holder for all platform-specific referencesS3Config- Interface for config values (implemented byNeoForgeConfig)S3NetworkHelper- Interface for sending packets (implemented byNeoForgeNetworkHelper)S3Platform.MenuOpener- Abstraction forServerPlayer.openMenu(provider, pos)
All platform holders are initialized in StevesSimpleStorage (the NeoForge @Mod entry point) during mod construction.
- Block classes (except port blocks that create NeoForge-specific BEs)
- Block entities (except port BEs that use
IItemHandler) - Menus and screens (except ExtractPort which is coupled to its neoforge BE)
- Packet record definitions (TYPE + STREAM_CODEC)
- Storage logic, utilities, enums
- Assets and data resources (textures, models, recipes, advancements, lang)
@Modentry point (StevesSimpleStorage)DeferredRegisterregistration classes (ModBlocks,ModItems, etc.)StorageConfig(usesModConfigSpec)- Packet handler methods (use
IPayloadContext) ModNetworkpacket registration- Port block entities (
IItemHandler) - Port blocks and extract port menu/screen
- JEI integration, datagen, game tests
- Event handlers (
SecurityEvents,ClientEvents) ModTab(creative tab, uses NeoForge-patchedCreativeModeTab.builder())
Main Jar (installable):
- Location:
neoforge/build/libs/s3-<version>.jar - Contains: Compiled mod classes from both modules and resources
Common Jar (not installable directly):
- Location:
core/build/libs/s3-core-<version>.jar - Contains: Platform-agnostic classes only
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew build --no-daemon
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: mod-jar
path: neoforge/build/libs/*.jar"Cannot find symbol" in core module: Ensure you're not importing net.neoforged packages. Common module only has vanilla MC classes.
"project(':core').sourceSets not found": Make sure settings.gradle includes both core and neoforge.
Datagen can't find core resources: The --existing argument in the data run config must point to rootProject.file('core/src/main/resources/').
Dependency Resolution:
./gradlew build --refresh-dependenciesClean Build:
./gradlew clean build