Fabric 1.20.4 mod: server-driven modpack auto-sync (packwiz + automodpack hybrid, no hardcoded server)
Find a file
2026-07-31 22:57:42 +02:00
gradle/wrapper Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00
src/main Retire le bouton Levi de l'ecran-titre (demande explicite) 2026-07-31 22:57:42 +02:00
.gitignore Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00
build.gradle Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00
gradle.properties v0.7 : architecture fidele a AutoModpack (preLaunch, pas de clic Rejoindre) 2026-07-31 22:52:29 +02:00
gradlew Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00
gradlew.bat Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00
README.md v0.6 : synchronisation declenchee par le clic sur Rejoindre 2026-07-31 22:21:03 +02:00
settings.gradle Initial commit: Levi v0.2 2026-07-31 16:48:08 +02:00

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 (.mrpack distribution) 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:

  1. 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 queries http://<même hôte>:<port>/levi-pack automatically. Zero address ever typed by the player — only a word, if one is configured. Requires the admin to have a lookup port actually open (see LeviServer below); not always available.
  2. No marker, but LeviKnownServers remembers this address — a pairing done here before. Checks silently against the already-known pack.
  3. Neither — first time seeing this address, and it isn't advertising a port. LeviJoinPairingScreen asks, once, for the admin's Levi lookup address (HTTPS domain or http://host:port) and the word. Remembered by LeviKnownServers for 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 (see LeviPackStore) and syncs anything out of date. Also registers the /levi command (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 #addRenderableWidget is protected, so a plain Fabric API event can't call it from outside TitleScreen). 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 /levi in.
  • 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 (see LeviLookupClient) — 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 — fetches pack.toml/index.toml/*.pw.toml from 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 (ServerStatusPacketListenerImplMixin server-side, LeviJoinInterceptor client-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>:

  • 200 with the pack's base URL in the body → word accepted (or none required).
  • 403 with 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:

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 from mods.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 (LeviPackStore supports it), but checkAllPairedPacks only ever shows one LeviSyncScreen per 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.