Repository navigation
Java Addon Tutorial
This tutorial builds Haven, a god with a craftable automatic statue, a water-healing blessing and a short trial. You finish with an independent Forge addon jar. It uses the same public APIs as other addons, not modifications to Divinity's source. KubeJS is not required.
Download the Java starter
and extract it into its own directory. Its full source is also in
templates/java-addon.
Do not put the source ZIP in Minecraft's mods folder.
Install JDK 17, then set your terminal's JAVA_HOME and your IDE's Gradle JVM
to that JDK. java -version should report 17. The project includes Gradle 8.8's
wrapper; do not install an unrelated system Gradle. Forge's official
development setup guide
explains the underlying IDE and JDK setup.
Obtain the current full Divinity Engine 1.12-1.20.1 bundle, not the slim jar. This guide documents the current source update, which can be newer than a public mod-hosting release bearing the same version number. The importer checks for the automatic god/statue API. It cannot prove every behavior in an older build is identical; record the exact bundle/hash you test.
Run these terminal commands from the extracted project root:
chmod +x gradlew
./gradlew -b setup.gradle importDivinity "-PdivinityJar=/absolute/path/to/divinity_engine-1.12-1.20.1.jar" --console=plain
./gradlew build --console=plainOn Windows PowerShell, use the included batch wrapper:
.\gradlew.bat -b setup.gradle importDivinity "-PdivinityJar=C:\Downloads\divinity_engine-1.12-1.20.1.jar" --console=plain
.\gradlew.bat build --console=plainsetup.gradle is a separate setup build so missing mod dependencies do not prevent
the importer from running. It reads the bundle's JarJar metadata and writes
development-only copies into libs/managed/, including required Core, Vanguard,
Player Animator and MixinExtras libraries. Nested copies are removed from those
local jars, then ForgeGradle remaps each dependency independently. The original
bundle is untouched; the importer does not alter your Minecraft installation.
libs/managed/dependencies.json records versions and hashes. You do not need a
Maven Central release or private Core/Vanguard repository access for this route.
Do not copy these derived jars into a normal game or distribute them: players
install the original Divinity bundle and your finished addon.
Expected checkpoint: build succeeds and build/libs/haven_addon-1.0.0.jar
exists. A successful compile is not a substitute for the gameplay checks below.
| File | Responsibility |
|---|---|
gradle.properties |
Output name/version, namespace setting and Java package group |
src/main/resources/META-INF/mods.toml |
Forge mod identity and required Divinity/Minecraft/Forge versions |
HavenAddon.java |
Connect the registers to the mod event bus and submit the trial definition |
HavenGod.java |
Define domains, blessings, accepted offerings and prayer response |
GuidingBreath.java |
Implement server-side blessing behavior |
ShelterVigil.java |
Implement the encounter without duplicating session ownership |
assets/haven_addon/lang/en_us.json |
Names, blessing description and messages |
data/haven_addon/recipes/haven_statue.json |
A survival crafting recipe for the automatically registered statue |
The four Java classes are under src/main/java/com/example/haven/. Client assets
and server data are ordinary resources included in the addon jar. The generated
statue fallback means you can test gameplay before making a custom model.
@Mod(HavenAddon.ID) makes Forge construct the addon once. The constructor
attaches the blessing register and GodsDeferredRegister to the mod event bus.
It does not attach them to the general Forge gameplay event bus.
private static final DeferredRegister<Blessings> BLESSINGS = DeferredRegister.create(
ResourceLocation.fromNamespaceAndPath("divinity_engine", "blessings"), ID);
public static final RegistryObject<Blessings> GUIDING_BREATH = BLESSINGS.register(
"guiding_breath", GuidingBreath::new);
private static final GodsDeferredRegister GODS = GodsDeferredRegister.create(ID);
public static final RegistryObject<BaseGod> HAVEN = GODS.register("haven",
() -> new HavenGod(Set.of(GUIDING_BREATH)));These are suppliers, not immediate instances. Pass registry handles into the
god; do not call GUIDING_BREATH.get() while static fields are initializing.
Do not create a new shared blessing registry with makeRegistry: Divinity already
owns it. Your entries belong to haven_addon, not divinity_engine.
GODS.register(bus) owns the god's statue block, block item and block-entity type
as well. Haven's statue ID is haven_addon:haven_statue. Creating another block
with that ID is a duplicate-registration error, not a way to customize its model.
Use resource overrides instead.
Trials are custom deferred definitions, not Forge registry objects. The starter
declares a lazy GodTrial(HAVEN.get(), ShelterVigil::new) factory, then submits the
trial register in enqueued common setup. At that point the god is available;
the registry accepts submissions whether its built-ins initialized just before
or after the addon. The factory creates a new encounter per player session.
Extending BaseGod with a name is not enough to define custom worship rewards.
HavenGod supplies the blessing handles, accepts amethyst shards and bread,
and calls GodHelper.increaseFavor from valid server-side prayer/offering events.
It rejects canceled events, empty stacks and client-side calls.
The offering handler returns true only for accepted offerings. The shared
shrine processor consumes them and handles worship budgets/story attribution.
Do not shrink the stack yourself or run the same favor mutation on the client.
The prayer callback is invoked by the shared prayer system; do not add a second
player tick loop just to award prayer favor.
This addon uses favor-based compatibility rules. Its blessing requires 50 favor. Do not assume the built-in pantheon's proof-based progression automatically becomes the progression rule for a newly registered god.
GuidingBreath calls:
super(50, 12, false, true, false, HavenAddon.COLOR);The arguments mean required favor, cooldown seconds, passive, active, toggle, and display color. Its implementation:
@Override
public boolean onPressed(BlessingsInstance instance, LivingEntity owner) {
if (owner.level().isClientSide || !owner.isInWater()) return false;
float before = owner.getHealth();
if (before >= owner.getMaxHealth()) return false;
owner.heal(2.0F);
if (owner.getHealth() <= before) return false;
var data = instance.getOrCreateTag();
data.putInt("haven_successful_heals", Math.min(1_000_000, data.getInt("haven_successful_heals") + 1));
instance.markDirty();
return true;
}The full starter also plays a quiet confirmation sound. Two HP is one heart.
Healing can be capped or canceled by another system, so test the actual health
change before returning true. The shared dispatcher validates the equipped slot
and starts the cooldown after success. Do not start another cooldown or replay
the same action in onRelease.
The registry definition is shared by every player. An ordinary int field on
GuidingBreath would be shared too. The example's counter lives in the individual
BlessingsInstance tag, and markDirty() lets the owning data synchronize.
Do not put entities, worlds or other runtime references in saved data. Do not
assume every LivingEntity is a player; check its type before player-only APIs.
For passive/toggle handlers, implement only the callbacks your mode supports, bound any area search, and clean up your own temporary effects. Return success only when an effect happened. See Java API and Effects and UI before adding custom effects.
ShelterVigil is intentionally a five-second noncombat encounter. runPhase
sets a 200-tick safety deadline and a 100-tick watch objective. tick reports
progress once per second and calls completeTrial() when the watch finishes.
onComplete awards five favor and sends its message.
The shared session manager handles preparation, transport, room protection,
terminal outcome, return and batched cleanup. Do not teleport or remove rooms
inside onComplete, and do not maintain a global list of active players. For
combat encounters, track each spawned entity with trackEntity, update targets
at spawn rather than every tick, and use the session's routed death callbacks.
Do not count an unrelated entity's death just because it happened nearby.
Trial registration does not automatically create a quest. To add one, use Stories and Upgrades; a story can require this trial's fresh victory and award a separate once-only specialization.
Open the extracted folder as an IntelliJ Gradle project after importing dependencies. With Gradle JVM set to JDK 17, run:
./gradlew genIntellijRuns --console=plain
./gradlew runClient --console=plainThe test game uses the starter's run/, not your normal instance. Create a new
disposable world. No admin commands are needed for these checks:
- Find Haven's statue in the statue creative tab or craft it with the supplied recipe.
- Make contact, pray and offer bread/amethyst. Reach 50 favor through normal worship.
- Equip Guiding Breath. Check injured/in-water success, dry-land refusal, full-health refusal and cooldown.
- Open the physical statue's blessings menu and start Shelter Vigil. Check success, return and room cleanup.
- Leave another trial early, then retry. Check failure does not leave a running encounter.
- Save/rejoin and confirm the statue and blessing remain available.
Also test a dedicated server with ./gradlew runServer; its separate directory
is run-server/. Read and accept Minecraft's EULA yourself if you agree. Test
with a second player: one player's ability counter/cooldown must not affect
the other. Never import personal worlds for development checks.
Choose a lowercase namespace and replace haven_addon in all of these places:
gradle.properties, HavenAddon.ID, mods.toml mod/dependency tables,
resource directory names, recipe IDs and translation keys. Refactor the Java
package and mod_group separately. Changing mod_id alone does not rewrite Java.
God, blessing, trial, story and specialization IDs are save identities. Once a release has reached players, keep those IDs stable. Change user-facing names via translations instead. Removing or renaming released registry IDs requires an explicit migration and backups.
Run ./gradlew build --console=plain. Distribute
build/libs/haven_addon-1.0.0.jar, after the build's reobfJar step, on both client
and server with the original Divinity bundle. The addon jar includes only your
classes/resources; it does not repackage Divinity or its dependencies.
Test the finished jar in a separate normal Forge instance. Publish installation steps, exact tested versions, a license for your own work and an upgrade policy. Exclude development dependency copies, logs, caches, worlds and credentials. Use Addon Templates for download inventories and the starter README for common setup failures.