- Java 100%
| gradle/wrapper | ||
| src/main | ||
| .gitignore | ||
| build.gradle | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| README.md | ||
| settings.gradle | ||
Levi
A Fabric 1.20.4 mod that auto-syncs a server's packwiz modpack at game launch, without automodpack's known file-cleanup bug — and, unlike a first draft of this idea, with nothing hardcoded to any one server. Named after Levi Ackerman (Attack on Titan), matching this project's existing anime-character naming convention for tooling (the Discord bridge bot is "Yor", after Yor Forger from Spy x Family).
The problem this solves
- packwiz alone (
.mrpackdistribution) is robust and hashed, but has no in-game sync — every pack update requires each player to manually re-run their launcher's modpack update. - automodpack syncs automatically on connect, but has a known bug: when a mod is removed from the server's pack, it can leave orphaned files on the client, or in some cases delete files it never actually managed.
Levi reuses the packwiz manifest format as-is (no second manifest to maintain)
and fixes the cleanup problem by keeping an explicit ledger
(ManagedFiles/.minecraft/levi/managed.json) of exactly which files it
downloaded — a file is only ever auto-deleted if it's in that ledger, so a
player's manually-installed mod is never touched, and a mod Levi itself
installed is always cleanly removed when the pack drops it.
Why the sync check never uses Minecraft's own connection protocol
Two early designs tried to sync exactly when a player joined a server (first
a ClientPlayConnectionEvents.JOIN hook, then a Fabric configuration-phase
handshake). Both failed the same way in live testing: vanilla Minecraft
disconnects a client with a plain "Connection Lost / unknown registry
entries" error (actually Fabric API's own registry-sync check, confirmed by
inspecting the actual fabric-registry-sync-v0 module) the instant it would
enter play with missing custom blocks/items — and it does this before any
mod networking hook, in any connection phase, ever gets a chance to run. A
player with none of the server's mods — exactly who this mod exists to help —
never got a chance under either design; the disconnect always won the race,
and no ordering trick fixes it: downloading files to disk mid-connection
doesn't update registries already loaded in memory for this JVM run either
way, so entering play this same session was never actually possible once mods
are missing — a restart is unavoidable no matter how early the check runs.
Decompiling AutoModpack's actual compiled jar (not just its GitHub source)
showed the structural fix: it never participates in Minecraft's connection
protocol for the sync step at all. A Preload class runs at game launch,
well before any join attempt, using a locally-stored, previously paired
modpack address. Levi copies that same choice for its actual file-sync step —
but the trigger for a first-time pairing is still the "Rejoindre" click
itself (see below), just resolved via an ordinary status ping and a plain
HTTP(S) lookup, never Minecraft's real connection state machine.
Where the check actually happens: the "Rejoindre" click
ConnectScreenMixin intercepts ConnectScreen.startConnecting — the single
static method every real connection attempt goes through (multiplayer-list
"Rejoindre", direct connect, quick play alike). LeviJoinInterceptor cancels
the real attempt, does an ordinary status ping (the exact same one the
multiplayer list already does to show MOTD/player count — not a real join),
and decides:
- Ping marker present (see
LeviPingMarker) — the server advertised Levi's lookup port right in its ping response, as a fake entry in the player-sample list ("Levi_<port>", never shown prominently, just data). Levi querieshttp://<même hôte>:<port>/levi-packautomatically. Zero address ever typed by the player — only a word, if one is configured. Requires the admin to have a lookup port actually open (seeLeviServerbelow); not always available. - No marker, but
LeviKnownServersremembers this address — a pairing done here before. Checks silently against the already-known pack. - Neither — first time seeing this address, and it isn't advertising a
port.
LeviJoinPairingScreenasks, once, for the admin's Levi lookup address (HTTPS domain orhttp://host:port) and the word. Remembered byLeviKnownServersfor every subsequent click on this same server address — an "Ignorer, rejoindre normalement" button lets the player skip Levi entirely for a server that just doesn't run it.
Whichever path resolves, the outcome is the same: nothing to sync → the real
connection proceeds (ConnectScreen.startConnecting called again, guarded by
a bypass flag so it isn't re-intercepted); something to sync → LeviSyncScreen,
never a real connection attempt this session. Any failure anywhere in this
check (ping failed, lookup unreachable, ...) is caught and falls back to
letting the real connection proceed normally — a network hiccup during this
courtesy pre-check must never be the reason a join fails or the game crashes.
The game-launch check (LeviClient/ClientLifecycleEvents.CLIENT_STARTED,
LeviPackStore) still runs too, as a secondary safety net for packs paired
through the title-screen button — the join-click path above is primary.
Architecture
LeviClient(ClientModInitializer) — at game launch (ClientLifecycleEvents.CLIENT_STARTED), checks every pack the player has ever paired with (seeLeviPackStore) and syncs anything out of date. Also registers the/levicommand (opens the pairing screen — useful once already in a world/chat, where the title-screen button isn't reachable).TitleScreenMixin— the only mixin this mod needs, purely to inject a small "Levi" button into the vanilla title screen (Screen #addRenderableWidgetisprotected, so a plain Fabric API event can't call it from outsideTitleScreen). This is the primary, always-reachable entry point for pairing a server for the first time, since a player with no world loaded yet has no chat to run/leviin.LeviPairingScreen— where a player pairs a server: type the admin's Levi lookup address and, if the admin set one, the word. Levi asks that address for the pack's URL automatically (seeLeviLookupClient) — the player never has to know or type the pack URL itself.LeviPackStore(config/levi/packs.json) — every pairing, keyed by the pack's base URL, not the Minecraft server's address. The pack URL is the stable identifier: it's a separate, independently-hosted web address, so it survives the game server itself moving to a new IP. The last-used lookup address is kept alongside purely as a best-effort hint for opportunistically re-checking the word later — a stale/unreachable hint is never fatal, since the pack URL already works independently of it.PackSync— fetchespack.toml/index.toml/*.pw.tomlfrom the paired pack URL and diffs against the local install, exactly the packwiz format packwiz itself already produces.ManagedFiles— the actual fix for automodpack's cleanup bug, see above.LeviSyncScreen— sync is not optional/proposed, it starts immediately on a mismatch, with two live progress bars (current file's own byte progress on top, overall file-count completion below). Downloads are SHA1-verified before being moved into place. Since mods can't hot-reload, the only action once done is a single "Fermer le jeu" button (Minecraft.stop()) — no silent auto-restart, no "later" option.LeviJoinInterceptor/ConnectScreenMixin— the join-click path described above; the primary UX.LeviPingMarker— encodes/decodes Levi's lookup port as a fake player in a status ping's sample list, both sides (ServerStatusPacketListenerImplMixinserver-side,LeviJoinInterceptorclient-side).LeviKnownServers(config/levi/known-servers.json) — Minecraft server address → pack base URL, purely a "don't ask again for this address" convenience cache;LeviPackStore(keyed by pack URL) remains the real source of truth.LeviJoinPairingScreen/LeviJoinPromptScreen— the first-time and wrong-word screens shown from the join-click path.LeviServer(DedicatedServerModInitializer) — optional, see below.
The mot → URL du pack lookup
LeviLookupClient does a plain GET against <adresse>/levi-pack?word=<mot>:
200with the pack's base URL in the body → word accepted (or none required).403with a human-readable reason in the body → word rejected.
Any address can be typed with an explicit http:// or https:// prefix; with
no prefix, https:// is assumed. This is deliberately not Minecraft
networking of any kind — see "Why sync happens at launch, not at join" above.
There are two ways for a server admin to answer this contract:
Recommended: HTTPS on your existing web server
If you already host pack.toml/index.toml/the mod files somewhere on the
web (you have to, for packwiz to work at all), add one more location to that
same web server. No extra Minecraft-adjacent port to open, no Fabric-side
listener needed at all — you don't even need to install any Levi jar on the
Minecraft server for this path.
Example nginx config (adapt the domain and word):
server {
server_name levi.TONDOMAINE.fr;
location /levi-pack {
default_type text/plain;
if ($arg_word !~* "^mtrfrance$") {
return 403 "Mot incorrect pour ce serveur.";
}
return 200 "https://mods.exemple.fr/pack/";
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/levi.TONDOMAINE.fr/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/levi.TONDOMAINE.fr/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = levi.TONDOMAINE.fr) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name levi.TONDOMAINE.fr;
return 404; # managed by Certbot
}
Setup, once DNS for levi.TONDOMAINE.fr points at your server:
sudo tee /etc/nginx/sites-available/levi.conf > /dev/null << 'EOF'
server {
server_name levi.TONDOMAINE.fr;
location /levi-pack {
default_type text/plain;
if ($arg_word !~* "^mtrfrance$") { return 403 "Mot incorrect pour ce serveur."; }
return 200 "https://mods.exemple.fr/pack/";
}
listen 80;
listen [::]:80;
}
EOF
sudo ln -sf /etc/nginx/sites-available/levi.conf /etc/nginx/sites-enabled/levi.conf
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d levi.TONDOMAINE.fr
Players then pair with the address levi.TONDOMAINE.fr and word mtrfrance.
Leave the if ($arg_word ...) block out entirely (just return 200 "...";)
if you don't want a word at all — every request gets the pack URL back.
Fallback: no domain, no web server
Install the Levi jar server-side too (environment: "*", server
entrypoint). Fill in config/levi-server.toml (auto-created with placeholders
on first boot):
word = "changez-moi"
pack-base-url = "https://example.com/pack/"
lookup-port = 25566
Levi then opens a small plain HTTP (not HTTPS) listener on lookup-port,
completely separate from the Minecraft port — open/route that port
separately (under Pterodactyl: add a dedicated allocation). Once this is
configured, LeviPingMarker starts advertising that port in every status
ping automatically — this is what unlocks the fully zero-address join-click
path above; without a reachable port, only the one-time LeviJoinPairingScreen
fallback is available. Players who do pair manually with this path type an
explicit scheme, e.g. http://1.2.3.4:25566.
/levi setup <pack-base-url> [mot] (in-game/console, op level 3+)
Writes config/levi-server.toml from chat or the server console instead of
hand-editing TOML over SSH, and prints back a ready-to-paste nginx snippet
with your real values already filled in — a hand-adapted template is exactly
the kind of thing that's easy to get subtly wrong. Works for either setup
above: the nginx path can just copy the printed snippet; the fallback path
gets its config written directly. A restart is needed afterward either way
(to actually start/restart the fallback listener, or just for the record —
the nginx path takes effect as soon as nginx reloads).
Why a "word" and not a real secret
The word is a lightweight "yes, I mean to sync with this specific server" identifier, not real access control — it's meant to be shared openly with a server's own playerbase (Discord, MOTD, etc.), not kept secret from them. Comparison is case-insensitive. Its only job is stopping a Levi client from accidentally syncing against the wrong pack.
Building
Standard Fabric Loom project, Java 17, Mojang official mappings
(loom.officialMojangMappings() — not Yarn; double-check method/class names
against Mojang mappings if porting code in from a Yarn-mapped reference).
./gradlew build
Output: build/libs/Levi-<version>.jar.
No special multi-JDK dance needed here (unlike the FAWE-Fabric fork in this
same org) — this project's own fabric-loom version doesn't have that
constraint.
Status / what's not done yet
- Live-tested end-to-end against the real MTR France server as of v0.5 (lookup
via
levi.adlmrl.fr, pack served frommods.adlmrl.fr). - A local singleplayer/LAN world never has anything pre-paired — by design,
not a bug. Levi does nothing at all until a player explicitly pairs a
server via the title-screen button or
/levi. - No per-file download progress persistence across a restart mid-download (a restart mid-sync just re-runs the whole check from scratch next launch, which is safe — hash verification means nothing corrupt ever gets installed — just not resumable byte-for-byte).
- Pairing more than one server works (
LeviPackStoresupports it), butcheckAllPairedPacksonly ever shows oneLeviSyncScreenper launch (the first pack found needing work) — a second pack needing updates as well would only be caught on the next launch, after the first one's restart.