feat(mods): replace packwiz with distribution-fed custom mod volume

Drop packwiz entirely. The server now loads a filtered mod subset from a
local ./server-mods volume instead of fetching a packwiz pack at boot.

New flow:
- tooling/classify-mods.py (tomllib only, no network) reads each jar's
  META-INF/neoforge.mods.toml and seeds mods-sides.json with a side
  (client|server|both; default both). Existing entries are preserved.
- mods-sides.json: committed, keyed by jar filename, hand-editable override
  surface. Initial seed: 65 both, 11 client, 0 server (76 jars).
- tooling/sync-server-mods.sh mirrors the both/server jars from
  $DISTRIBUTION_WEB_ROOT into ./server-mods/ (authoritative; prunes strays;
  halts on a missing jar). Replaces render-pack.sh.
- compose: minecraft mounts ./server-mods:/mods:ro with REMOVE_OLD_MODS=TRUE
  (itzg auto-syncs /mods -> /data/mods). Removed PACKWIZ_URL, the pack.
  extra_host (auth. kept for authlib), and depends_on caddy (drasl kept).

Both classify-mods.py and sync-server-mods.sh resolve their source dir as:
CLI arg / MODS_DIST_DIR env / $DISTRIBUTION_WEB_ROOT (in that order).

Removed: tooling/render-pack.sh + add-custom-mod.sh (packwiz tooling),
pack/ (packwiz source), custom/ (76 jars now sourced from the distribution),
the pack. caddy vhost + its mounts/alias, and the packwiz .gitignore block.

Client distribution is UNCHANGED: HeliosLauncher still installs the full
modset from distribution.json. Docs (CLAUDE.md, plan/04/05/14, runtime)
updated; plan/04-packwiz.md stubbed as superseded by plan/04-mods.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:34:38 +02:00
parent 90c3be7bb5
commit 27237bc7c1
108 changed files with 647 additions and 369 deletions

View File

@@ -1,7 +1,7 @@
# Ulicraft Server — Project Context
> Self-hosted modded Minecraft (NeoForge 1.21.1) with self-hosted auth/skins
> (Drasl), a packwiz modpack, avatar rendering, a guest landing page, and uptime
> (Drasl), a distribution-fed modpack, avatar rendering, a guest landing page, and uptime
> monitoring. Public, internet-present deployment behind the host's nginx + TLS.
**`plan/` is the source of truth for the design.** Start at `plan/00-overview.md`.
@@ -30,18 +30,17 @@ Internet ── host nginx (TLS, LE certs, HSTS) ──► caddy (127.0.0.1:8880
│ routes by Host:
${BASE_DOMAIN} ─► /srv/www landing + /launcher/ downloads
auth.${BASE_DOMAIN} ─► drasl (Yggdrasil API + web UI)
pack.${BASE_DOMAIN} ─► packwiz files + /custom/ jars
avatar.${BASE_DOMAIN} ─► nmsr (skin/avatar renderer, Drasl-backed)
status.${BASE_DOMAIN} ─► uptime-kuma (status page + monitors)
distribution.${BASE} ─► static site from ANOTHER repo (DISTRIBUTION_WEB_ROOT)
minecraft :25565 (+ 24454/udp Simple Voice Chat)
└ ONLINE_MODE=false, -javaagent authlib-injector → http://auth.${BASE_DOMAIN}/authlib-injector
└ mods via PACKWIZ_URL → http://pack.${BASE_DOMAIN}/pack.toml
└ mods from ./server-mods volume (/mods, REMOVE_OLD_MODS) — see plan/04-mods.md
mc-backup ── RCON → minecraft (6h interval, prune 14d, world-only)
```
Bring-up: render configs → build landing → fetch launcher → `docker compose up -d --build`.
Bring-up: render configs → sync server mods → build landing → fetch launcher → `docker compose up -d --build`.
See `plan/12-build-order.md` and `plan/14-deploy.md`. Deploy to prod host `cochi`
via the `deploy` skill.
@@ -55,8 +54,11 @@ via the `deploy` skill.
- **Manual curation** (user choice, flagged expensive) — if it gets painful,
fork-and-trim Leaking Kitchen Sink is still on the table (closest vibe match).
- **itzg/minecraft-server** base image — de-facto standard, auto-installs NeoForge.
- **packwiz** for the modpack — `PACKWIZ_URL` drives server mod install; clients
import the same `pack.toml`. Jars come from the upstream CDN per `.pw.toml`.
- **Distribution-fed mod volume (replaced packwiz)** the modset's source of
truth is the HeliosLauncher distribution repo (`$DISTRIBUTION_WEB_ROOT`).
Clients install the full set via `distribution.json`. The SERVER loads a
filtered subset (`both`/`server`) from a mounted `./server-mods` volume
(`REMOVE_OLD_MODS=TRUE`). See `plan/04-mods.md`.
## Critical Gotchas
@@ -81,10 +83,17 @@ Mods are picky about exact minor versions. Compose pins `NEOFORGE_VERSION:
"21.1.209"`. **Verify against https://projects.neoforged.net/neoforged/neoforge
before deploying.** Never `"latest"` for a stable server.
### Mod source preference
Prefer **Modrinth over CurseForge** (`packwiz modrinth add <slug>`). CF needs
`CF_API_KEY` and is flakier. Tag client-only mods `side = "client"` so the server
doesn't pull and crash on them.
### Mod source + server filtering
The full modset lives in the distribution repo (`$DISTRIBUTION_WEB_ROOT/servers/
ulicraft-1.21.1/forgemods/required/*.jar`), managed by Nebula. Clients get
everything via `distribution.json`. The server runs a **filtered subset**:
`tooling/classify-mods.py` reads each jar's `neoforge.mods.toml` with tomllib and
seeds `mods-sides.json` (`client|server|both`, default `both`), then
`tooling/sync-server-mods.sh` copies the `both`/`server` jars into `./server-mods`
(mounted at `/mods`). Hard client-only mods (sodium/iris) classify as `client`
automatically; cosmetic-client mods that declare `BOTH` stay `both` until you
hand-edit `mods-sides.json` to `client`. No Modrinth/packwiz, no network at
classify/sync time. See `plan/04-mods.md`.
### Memory sizing
~50100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
@@ -93,9 +102,11 @@ doesn't pull and crash on them.
## Config (.env)
`BASE_DOMAIN`, `RCON_PASSWORD`, `CADDY_HTTP_PORT`/`CADDY_HTTP_BIND`,
`DISTRIBUTION_WEB_ROOT`, the Let's Encrypt block (`LE_EMAIL`, `LE_SUBDOMAINS`,
`OVH_*`), optional `CF_API_KEY`. Rendered configs (drasl, nmsr, packwiz) come
from `*.tmpl` via `tooling/render-config.sh` (substitutes `${BASE_DOMAIN}` only).
`DISTRIBUTION_WEB_ROOT` (source of the modset + caddy `distribution.` mount), the
Let's Encrypt block (`LE_EMAIL`, `LE_SUBDOMAINS`, `OVH_*`). Rendered configs
(drasl, nmsr) come from `*.tmpl` via `tooling/render-config.sh` (substitutes
`${BASE_DOMAIN}` only). Server mods are synced (not rendered) by
`tooling/sync-server-mods.sh`.
## Client Distribution (guests)
@@ -105,7 +116,9 @@ mirrored at apex `/launcher/`. Per guest:
2. Add an authlib-injector account, URL
`https://auth.${BASE_DOMAIN}/authlib-injector` (register/login at
`https://auth.${BASE_DOMAIN}`).
3. Import the pack `https://pack.${BASE_DOMAIN}/pack.toml`.
3. The launcher installs the modpack from the distribution (`distribution.json`).
(NOTE: packwiz/`pack.` removed — the landing join step still references a
packwiz URL and needs reworking around the launcher/distribution flow.)
4. Connect to `${BASE_DOMAIN}` (`:25565`).
## Open / Pending Work