Skip to content

Building a mod on Conduit

This page is the map. If you are about to write a minigame on Conduit and you do not yet know the codebase, read this before anything else, then follow the links into the module docs you actually need.

Conduit is a Fabric engine that hands you the things every minigame re-solves: an isolated world per match, a lobby, phases, spectators, reconnects, in-world UI. Your mod does not build a server; it registers a game with the engine and implements the rules. The engine spins up a runtime dimension per match, drives the lobby and phase machine, and hands your code control at the points where your game is actually different from every other game.

Start from the job, not the module list.

You need to…Go to
Register a game, run a lobby, drive phases, manage instancesconduit-arena
Player sessions, state, permissions, safe teleports, tick budget, configconduit-core
In-world screens, labels, menus, block displays, custom UIconduit-render
A fresh dimension per match, void worlds, runtime dimension specsconduit-instance
Per-instance world rules, void generationconduit-world
Build scenes and prefabs, zones, bindingsconduit-prefab
Countdowns, boss bars, effectsconduit-fx
Spectator flowconduit-spectator
Custom entities, elite mobs, spawn waves, forces, flying propsconduit-mob
Pause, scrub back, resume a live matchconduit-rewind

Every shipped Conduit game converges on roughly the same layout. Taken from Block Shuffle:

me/zlex/<game>/
<Game>Mod.java entry point, registers with the engine
<Game>Game.java the game definition the engine drives
<Game>Config.java tunables
<Game>Rules.java the actual rules
RoundDriver.java per-round logic
<Game>Command.java admin/debug commands
lobby/
<Game>Lobby.java lobby state
LobbyManager.java
GamePhase.java the phase enum
game/
LobbyController.java
LobbyRoundDriver.java
<Game>ConfigMenu.java in-world config UI
ScreenIds.java

The split that matters: lobby/ is pre-match, game/ is in-match. Keeping them apart is what stops a round driver from quietly reaching into lobby state that no longer means anything once the match has started.

Games depend on the engine by version, and bundle it Jar-in-Jar so the person receiving the mod gets one file:

gradle.properties
engine_version=0.22.0+mc26.1.2
// build.gradle
implementation "me.zlex:conduit:${project.engine_version}"
include "me.zlex:conduit-core:${project.engine_version}"
include "me.zlex:conduit-instance:${project.engine_version}"
// …and every other module, see the trap below

The engine is published Mojang-mapped, so a plain implementation resolves correctly even though Loom 1.16.2 does not expose modImplementation.

These are the ones that have actually cost time. They are listed here because none of them announce themselves.

The dependency graph, read from each module’s fabric.mod.json (Loader resolves these by mod id):

conduit-core (nothing)
conduit-render core
conduit-world core
conduit-spectator core
conduit-instance core, world
conduit-fx core, render
conduit-prefab core, render, world
conduit-mob core, prefab
conduit-rewind core, render, world, fx
conduit-arena core, render, world, instance, fx, spectator, prefab, rewind

conduit-arena pulls in eight of the others, which is why “I only use arena” is never true.

Bundle every engine module, not the ones you think you use. conduit-arena hard-depends on conduit-rewind as of engine 0.17.0, and Fabric Loader resolves that by mod id, not by class. A jar missing the nested conduit-rewind compiles perfectly, builds perfectly, and then refuses to load conduit-arena at runtime. Verify what actually shipped:

Terminal window
unzip -l build/libs/<game>-*.jar | grep META-INF/jars

Do not double-bundle in an umbrella build. When several games ship inside one bundle, the umbrella JiJs the engine exactly once for all of them. A game that also nests it produces duplicate engine jars and fails mod loading. This is why the include block sits behind a if (!project.hasProperty('padPartyBundle')) guard.

Gate your tick loop on rewind’s freeze. The engine cannot stop your round loop. See cooperative pause, one line, and without it a paused match keeps counting down.

Register a snapshot slice for your own state. Scores and phase do not live in the world, so they do not rewind unless you say how. See snapshot slices.

Measure spawn cost before trusting a wave. /conduit mob spawnprobe reports what a scatter spawn costs on the tick it runs.

When this site and the code disagree, the code wins, and the site is the thing that needs fixing.

  • Engine source and per-module README.md files: the conduit repo under modules/.
  • Release history and the authoritative version: CHANGELOG.md on the engine’s main branch.
  • The version games are actually built against: engine_version in each game’s gradle.properties.

SemVer with a +mcXX.X.X build-metadata suffix recording the Minecraft version the artifacts were compiled against. While the engine is 0.x, MINOR is the breaking-change signal (a 0.4 → 0.5 bump may require consumer changes) and PATCH carries features and fixes. That relaxes at 1.0.0.