-
Notifications
You must be signed in to change notification settings - Fork 0
Developer Guide
Everything you need to build the mod, find your way around it, and not break the rules that are load- bearing.
./gradlew build # both loaders → forge/build/libs, fabric/build/libs (what CI runs)
./gradlew :forge:runClient # dev client (the primary loop)
./gradlew :fabric:runClient # second loader, verified after Forge
./gradlew :common:test # the simulator's JUnit suite — Minecraft-free
./gradlew :common:test --tests '*Benchmark*' -Dsim.benchmark=true # opt-in perf benchmark
cd web && npm run build # frontend — commit web/dist/- Java 17 is enough (that is what CI uses); a 21 JDK works too. Bytecode target is 17.
- The Gradle daemon is disabled (
org.gradle.daemon=false), so every invocation cold-starts — allow a couple of minutes and use generous timeouts. - Dependency versions live in
gradle.properties. DragonLib and Create Railways Navigator resolve from the Modrinth maven by version id, not version name (maven.modrinth:<slug>:<versionId>) — copy those ids exactly. CRN ismodCompileOnly+modLocalRuntime; the compile-only entry is what makes loom remap its calls into Create. -
forge/gradle.propertiesholdsloom.platform=forge. Without it the Forge module fails to configure with "Loom is not running on Forge". - Steam 'n' Rails cannot run in the dev environment (see the note in
forge/build.gradle); its schedule support is covered by JUnit and verified on a production pack.
./gradlew :common:test --tests '*Benchmark*' -Dsim.benchmark=true prints a digest that must stay
19a84b2d9cbfade6 across any change meant to be pure refactoring or pure performance work. A
moved digest means behaviour moved — either you did not mean it, or the change is not a refactor.
Architectury multi-module. common/ holds essentially everything; forge/ and fabric/ hold glue.
-
commoncompiles against create-fabric (modCompileOnly); the Architectury transformer bridges the Forge side. -
@ExpectPlatformstatics in common (DispatcherExpectPlatform,DNetworking) are implemented per loader as*Implclasses.
common/src/main/java/net/Dispatcher/
├── content/graph/v2/ RailGraphTranslator → junction/signal/station nodes
├── content/simulator/core/ the engine — zero Minecraft imports
├── content/simulator/ the Minecraft-coupled glue around it
├── content/trains/schedule/ the Advanced Schedule item, menu, screen, presets
├── content/gui/ client screens (map, sim results, diagram, preset window)
├── mixin/ three mixins into Create
├── foundation/network/ hand-rolled packets
├── Interfaces/ the interfaces mixins implement
└── web/ the embedded web server — server-only, no MC packet/client classes
web/ Vite + Svelte 5 frontend; dist/ is committed
content/graph/v2/ — RailGraphTranslator collapses Create's track graph into junction, signal
and station nodes with deterministic, position-derived ids. Server-only; cached per TrackGraph.
content/simulator/core/ — the engine, and zero Minecraft imports by design. Deterministic:
sorted train order, fixed tick step, no wall clock. Mirrors Create 6.0.8 — bang-bang v²/2a control,
per-tick section reservations, atomic chain signals, persistent first-come-first-served claims.
Everything here is unit-testable, and is unit-tested.
content/simulator/ — the coupled glue: NetworkSnapshotter (server thread), ScheduleCompiler
(dispatches conditions and instructions by NBT id string — no CRN or SnR imports),
SimulationService (in-game item simulation, worker thread), HeadlessSimService (the web planner's
entry point).
web/ — server-only, and it must stay that way. It may never reference a client class or a
Minecraft packet class, or dedicated servers fail class verification. JDK com.sun.net.httpserver
plus SSE; no WebSocket, no new runtime dependencies. The coupling from core into web is exactly two
lines: WebBootstrap.init() in DispatcherMod.commonSetup() and WebCommands.register(…) in
DispatcherCommands.
web/ (repo root) — the frontend: Vite + Svelte 5, zero UI or chart libraries, hand-rolled
canvas.
Only these may touch Create.RAILWAYS or a Level, and only on the server thread: the live
sampler, the tick-side analyzers, WebGraphStore.rebuild, DeployService, preset train-import, and
HeadlessSimService.prepare. Everything else runs on worker threads over immutable snapshots.
HTTP handlers marshal to the server thread explicitly (onServerThread(…), with a 5-second timeout)
and everything else — store reads, JSON building — happens on the HTTP pool. SSE clients each own a
dedicated writer thread; they never occupy an HTTP worker, because SSE pins its connection.
Mixin + interface: a mixin in common/.../mixin/ adds state to a Create class and implements an
interface from common/.../Interfaces/; the rest of the code casts to that interface.
| Mixin | What it does |
|---|---|
ScheduleRuntimeMixin |
carries the advanced-schedule marker (fields are dispatcher$-prefixed, so Create Realism can mix into the same class without colliding) and swaps the item returnSchedule() hands back |
ConductorBlockInteractionMixin |
widens Create's registry-entry item gate to any ScheduleItem
|
StationBlockEntityMixin |
the same, for the station auto-schedule slot — on both insertion and application |
There is one mixin config, dispatcherCommon.mixins.json, declared in fabric.mod.json and via
loom.forge.mixinConfig(...) in forge/build.gradle. There are no platform-side mixins. Client-only
targets live in the config's "client" section, which is part of what makes server-only mode work.
Hand-rolled, not Architectury networking. Packets implement C2SPacket / S2CPacket, register with
sequential ids in DNetworking.register(), and travel over one channel (createdispatcher:net).
Registration order is the wire format — append at the end and bump DNetworking.VERSION, which
is checked on join and disconnects mismatched clients with a translated message.
Current order (ids are positional):
CheckVersion (S2C) · RequestGraphView (C2S) · GraphView (S2C) · AdvancedScheduleSave (C2S)
RequestSimulation (C2S) · SimulationResult (S2C) · PresetListRequest (C2S) · PresetList (S2C)
PresetUpload (C2S) · PresetDownload (C2S)
The web interface adds no packets; it is pure HTTP/SSE.
Never construct a client screen in a packet class — dedicated servers crash on class
verification. Packet → GUI goes through the client-only *Opener holders (SimulationResultOpener,
GraphMapOpener, PresetLibraryOpener).
DispatcherConfig is COMMON-only — nothing here reads a client value. It is registered through
Forge's ModLoadingContext on Forge and ForgeConfigAPIPort on Fabric.
Secrets and the allowlist never live in a config spec. COMMON tomls ship inside client installs
and SERVER configs sync to clients, so both are server-only JSON under config/createdispatcher/.
World state (presets, plans, folders, calibration, audit) lives under <world>/createdispatcher/.
LegacyMigration copies both from the old Create Realism locations once, on first start.
web/ is a Vite + Svelte 5 SPA, committed built in web/dist/, which common/build.gradle
copies into the jar. Gradle and CI never run Node.
cd web
npm install
npm run dev # :5173, proxies /api + /auth to a dev server on :8455
DISPATCHER_PROXY=http://host:8455 npm run dev # …or to any other server
VITE_MOCK=1 npm run dev # no backend at all: synthetic network + fixtures
npm run check # types
npm run build # → dist/ (commit it):common:verifyWebDist recomputes the source digest recorded in web/dist/.buildinfo.json and warns
when the committed build is stale — and fails when the CI environment variable is set. That
digest recipe is duplicated in common/build.gradle (Groovy) and web/vite.config.ts; change both
together.
For a dev-server login: enable Web Enabled, then use /dispatcher web session deployer.
./gradlew build also emits a -server jar per loader. serverJar is a Zip task, not a
Jar — a Jar would regenerate the manifest and lose loom's MixinConfigs attribute. It repacks
remapJar and adds a createdispatcher.server-only marker, plus a displayTest mods.toml from
src/serverVariant/ on Forge. Fabric needs no metadata change and gets none, which is what keeps
loom's accessWidener and nested-jar entries intact.
foundation/ServerOnly.enabled() — system property, classpath marker, or
config/createdispatcher/server-only.marker — gates the two register() calls in commonSetup().
The item and menu type register in static field initialisers, so gating the calls is exactly what
keeps them out of the registries. Anything new that reaches AllDispatcherItems or AllMenuTypes
needs the same guard. Full rationale in plans/server-only-build.md.
-
./gradlew :common:test— the simulator's JUnit suite plus the graph JSON and Tarjan SCC tests. Minecraft-free, and the only automated tests in the repo. - Runtime verification:
:forge:runClientfirst, confirm the item / editor / simulation / presets in-game, then:fabric:runClient. - Dedicated-server and web checks via
runServer+curl: auth codes, the SSE stream, and zeroNoClassDefFoundErrornamingnet.Dispatcher.
- The package is
net.Dispatcher— capital D — with directories likeInterfaces/. The nonstandard capitalisation is inherited convention; match it. -
realism:time_of_day_realisticinScheduleCompileris deliberate. Create Realism is an optional integration like CRN, Tramways and Steam 'n' Rails, resolved by NBT id string with no import. Do not add a compile dependency to support one. - Registrate registers the item in a static field initialiser. That is load-bearing for server-only mode; do not "clean it up" into an eager registration.
| File | What |
|---|---|
SPEC.md |
the Advanced Schedule design and its milestone history |
SPEC-WEB.md |
the web interface design and its milestone history |
SIM_DIVERGENCES.md |
where the simulator knowingly differs from Create at runtime |
plans/server-only-build.md |
the server-only variant, in full |
docs/web-interface.md |
the admin-facing web guide this wiki's Web Interface page is based on |
CHANGELOG.md |
release notes |
Create Dispatcher · MC 1.20.1 · Forge & Fabric · MIT · source
Players
Server admins
Developers
Help