Getting started
This page gets you from a fresh clone to a running server you can join from a vanilla Minecraft client.
Toolchain
| Tool | Version | Notes |
|---|---|---|
| JDK | 25 (Eclipse Temurin) | The only thing you install. Minestom's current release builds with Java 25. |
| Gradle | 9.8, via the wrapper | ./gradlew downloads it; never install Gradle separately. |
| Minestom | 2026.09.12-26.2 (pinned) | For Minecraft 26.2 clients. Minestom's master branch changes often, so the exact version is pinned in gradle/libs.versions.toml. |
| Docker | any recent | Only for the local ClickHouse database (optional, see below). |
If java isn't on your PATH, point JAVA_HOME at a JDK 25 and Gradle's
toolchain support does the rest.
Clone, build, run
git clone [email protected]:Preckster/mcrpg.git
cd mcrpg
git checkout develop
./gradlew build # compile, Javadoc (doclint on), tests, ArchUnit rules
./gradlew :server:run # start the server on 0.0.0.0:25565
To run without Gradle in the loop (for example on a test machine):
./gradlew :server:installDist
server/build/install/server/bin/server
Then join localhost:25565 from a Minecraft 26.2 client. With the welcome
module enabled (the default) you'll see the Welcome to Shatterveil title and
the emblem in the server list.
:::tip Always build before you finish
./gradlew build is the gate. It runs Javadoc with doclint (a missing comment
on a public member is an error), the whole test suite, and the ArchUnit
architecture tests that enforce the ground rules.
A change that doesn't build clean isn't done.
:::
Configuration
Everything is configured through environment variables; nothing secret is ever in git.
| Variable | Default | Meaning |
|---|---|---|
SHATTERVEIL_HOST | 0.0.0.0 | Bind address |
SHATTERVEIL_PORT | 25565 | Port |
SHATTERVEIL_ONLINE_MODE | true | Mojang authentication. false only for offline LAN tests. |
SHATTERVEIL_DATA_DIR | data | Runtime state (git-ignored), e.g. data/modules.json and the audit spill file |
SHATTERVEIL_DB_URL / _NAME / _USER / _PASSWORD | unset | ClickHouse connection (see data). Falls back to CLICKHOUSE_USER / CLICKHOUSE_PASSWORD, the names in deploy/clickhouse/.env. |
SHATTERVEIL_ADMIN_TOKENS / _PORT / _HOST | unset / 8765 / 127.0.0.1 | The admin API. No tokens → the API doesn't listen. |
SHATTERVEIL_DISCORD_WEBHOOK_AUDIT / _GAME / _SYSTEM / _ALERTS | unset | Discord delivery for the log pipeline |
Without a database configured, the server still runs. Module state falls
back to data/modules.json, roles and player records live in memory for the
session, and audit events are spilled to a file so they aren't lost. Each
component says so once in the log.
Local ClickHouse
Every module that persists anything uses one ClickHouse database, running on
the same machine as the server and bound to 127.0.0.1 only. For local
development it's optional; for anything resembling a real server it's
required.
cd deploy/clickhouse
cp .env.example .env && chmod 600 .env # set a long random password
docker compose up -d
docker compose ps # wait for "healthy"
| Item | Value |
|---|---|
| Image | clickhouse/clickhouse-server:26.8.10.6 (26.8 LTS, pinned) |
| HTTP | 127.0.0.1:8123 |
| Native | 127.0.0.1:9000 |
| Database | shatterveil |
| User | from .env (CLICKHOUSE_USER, CLICKHOUSE_PASSWORD) |
| Data | Docker volume shatterveil-clickhouse-data |
initdb/*.sql runs once, on the first start with an empty volume, and creates
the schema_migrations table. After that every schema change ships as a
migration inside the module that owns the table, applied by the
data library at startup.
To check it's alive:
set -a; . ./.env; set +a
curl -s -u "$CLICKHOUSE_USER:$CLICKHOUSE_PASSWORD" \
http://127.0.0.1:8123/ --data-binary 'SELECT version()'
:::danger Never expose the database
The ports are bound to localhost on purpose. Don't publish them on a public
interface, a tunnel or the LAN. Only the data and log libraries connect to
ClickHouse; modules, Hermes, the Discord bot and the web panel all go through
those or through the admin API.
:::
Memory is capped at 25% of RAM (config.d/shatterveil.xml) so the game server
always has room. Backups (BACKUP DATABASE shatterveil TO File(...)) need a
backup disk configured — that's on the roadmap before real player data exists.
Repository layout
mcrpg/
├── settings.gradle.kts subprojects: server, libs/*, modules/*
├── build-logic/ Gradle convention plugins (toolchain 25, doclint,
│ JUnit 5, dependency rules)
├── gradle/libs.versions.toml every third-party version in one place
├── server/ the launcher: boots Minestom, the ModuleManager,
│ /module, and the kernel modules
├── libs/ shared libraries (never depend on modules)
│ ├── core-api/ commands/ permissions/ data/ log/
│ ├── security/ text/ palette/ testing/
├── modules/ one game system each (depend only on libs)
│ ├── admin-api/ logging/ permissions/ player-data/ welcome/
├── deploy/clickhouse/ the local database (Docker Compose)
├── docs/commands.md generated command reference
├── tools/ small scripts (server-icon rasteriser)
└── design/ design documents; 04-architecture-rules.md binds all code
A module and a library may share a directory name (libs/permissions and
modules/permissions both exist). Gradle project identity is disambiguated by
the convention plugin, and module jars are named module-<name>.jar.
Branch workflow
- All work happens on
develop. Commit there, push there. mainis production. Nothing is merged tomainwithout Preck's explicit go-ahead for that specific change. This applies to every repo in the project, including documentation-only changes.- Every update pushed to
developbumps the version — see Versioning & releases.
Check git branch --show-current before editing, not just before committing.
Commit messages
Every commit explains itself:
- A short summary line.
- A blank line.
- A bullet body covering what changed and in which files, why, and anything to watch out for (follow-up work, breaking changes, restarts).
Changes to a CRITICAL module also include a security note: what input is trusted, and how it's validated. See Architecture › Security for what CRITICAL means.
Adding a module
Copy modules/welcome/ — it's the reference module and is kept deliberately
small. A module needs:
- One entry class implementing
ShatterveilModulewith a manifest. - A
META-INF/services/com.shatterveil.api.ShatterveilModulefile naming that class (modules are discovered withServiceLoader). - A
README.md(what it does, manifest, settings, commands, events, what happens when disabled, tests). - A toggle test using
ModuleTestSupport.assertToggleLeavesNothingBehind. - An
include("modules:<name>")line insettings.gradle.kts, and the module added as aruntimeOnly+testImplementationdependency inserver/build.gradle.kts.
Then add a page for it under Modules & libraries on this site. The full checklist for a "done" module is in Architecture › When a module is done.