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:
172
plan/04-mods.md
Normal file
172
plan/04-mods.md
Normal file
@@ -0,0 +1,172 @@
|
||||
# 04 — Mods (custom volume, distribution-fed)
|
||||
|
||||
> **Supersedes `04-packwiz.md`.** packwiz is being removed. This describes the
|
||||
> replacement: the server loads mods from a filtered local volume; clients keep
|
||||
> getting the full set from the HeliosLauncher `distribution.json`.
|
||||
|
||||
## Why drop packwiz
|
||||
|
||||
- The packwiz index tracked only **9** mods while the real modset (76 jars) lives
|
||||
in the distribution repo — packwiz was half-built and stale.
|
||||
- Manual packwiz curation (`.tmpl` render → `refresh` → re-hash) is the flagged
|
||||
pain point, and a single missing file (`custom/.gitkeep`) crashlooped the
|
||||
server (404 → installer aborts → minecraft never starts).
|
||||
- The launcher (HeliosLauncher) already distributes mods to clients via a
|
||||
Nebula-generated `distribution.json`. That is the canonical client mod list.
|
||||
The server just needs the same jars, minus client-only ones.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ulicraft-distribution/dist (another repo, = $DISTRIBUTION_WEB_ROOT)
|
||||
servers/ulicraft-1.21.1/
|
||||
forgemods/required/*.jar ← 76 jars, Nebula-managed (SOURCE OF TRUTH)
|
||||
distribution.json ← generated by Nebula; CLIENT install list
|
||||
│ │
|
||||
│ caddy distribution. vhost │ HeliosLauncher reads it →
|
||||
▼ ▼ installs ALL listed mods (client)
|
||||
guests' launcher ◄───────────────────────────────────
|
||||
|
||||
ulicraft-server (this repo)
|
||||
mods-sides.json ← { "<jar>": "client|server|both" } (committed, editable)
|
||||
tooling/classify-mods ← tomllib: seeds/updates mods-sides.json from jar manifests
|
||||
tooling/sync-server-mods ← copies both+server jars from $DIST → ./server-mods/
|
||||
server-mods/ (gitignored) ──mount──► minecraft /mods (REMOVE_OLD_MODS=TRUE)
|
||||
```
|
||||
|
||||
- **Client distribution: unchanged.** HeliosLauncher installs everything in
|
||||
`distribution.json`. No packwiz, no client zip.
|
||||
- **Server: filtered subset.** Only `both` + `server` jars are copied into the
|
||||
mounted `server-mods/`. `client`-tagged jars never reach the server.
|
||||
- **Single source of jars:** `$DISTRIBUTION_WEB_ROOT/.../forgemods/required/`.
|
||||
This repo stores no jars — only `mods-sides.json`.
|
||||
|
||||
## Classification — tomllib only (no Modrinth)
|
||||
|
||||
`tooling/classify-mods.py` reads each jar's `META-INF/neoforge.mods.toml` with
|
||||
Python `tomllib` and derives a side from the **`minecraft`/`neoforge` dependency
|
||||
`side`** (the field modders use to declare a mod's environment):
|
||||
|
||||
| manifest signal | derived side |
|
||||
|---|---|
|
||||
| `minecraft` (or `neoforge`) dep `side = "CLIENT"` | `client` |
|
||||
| dep `side = "SERVER"` | `server` |
|
||||
| dep `side = "BOTH"`, unspecified, or no manifest | `both` (default) |
|
||||
|
||||
Output is **merged** into `mods-sides.json`: existing (human-edited) entries are
|
||||
**preserved**; only new/unknown jars get a derived default. So the file is a
|
||||
one-time seed + permanent manual override surface. New jars default to `both`
|
||||
(your chosen default) unless their manifest says otherwise.
|
||||
|
||||
### Accuracy + the manual step (important)
|
||||
|
||||
tomllib reliably catches **hard client-only** mods — ones that declare their
|
||||
`minecraft`/`neoforge` dep as `CLIENT` (e.g. `sodium`, `iris`). Those are exactly
|
||||
the mods that refuse/break on a dedicated server, so the must-exclude set is
|
||||
handled automatically.
|
||||
|
||||
It will **not** auto-exclude client-*cosmetic* mods that declare `BOTH` in their
|
||||
manifest (e.g. `BetterF3`, `entityculling`, `sodium-extra`, `entity_texture_features`,
|
||||
`entity_model_features`, `ImmediatelyFast`, `drippyloadingscreen`, `melody`,
|
||||
`welcomescreen`, `Controlling`, `MouseTweaks`, `Searchables`, `tagtooltips`,
|
||||
`JustEnoughResources`, `TravelersTitles`, `BridgingMod`, `ponderjs`,
|
||||
`yeetusexperimentus`). These load on the server harmlessly (wasted RAM, no crash)
|
||||
unless you mark them `client` in `mods-sides.json`. **That manual curation is the
|
||||
"filter manually" step** — `classify-mods` seeds, you trim cosmetic-client mods.
|
||||
|
||||
Also worth an explicit decision in the override file:
|
||||
- `CustomSkinLoader` — server doesn't need it (skins resolve via Drasl/authlib);
|
||||
set `client` to keep it off the server.
|
||||
|
||||
## `mods-sides.json` format
|
||||
|
||||
```json
|
||||
{
|
||||
"jei-1.21.1-neoforge-19.27.0.340.jar": "both",
|
||||
"sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar": "client",
|
||||
"ftb-essentials-neoforge-2101.1.9.jar": "both",
|
||||
"rightclickharvest-neoforge-4.6.1+1.21.1.jar": "server"
|
||||
}
|
||||
```
|
||||
|
||||
Key = exact jar filename (matches `forgemods/required/*.jar`). Value ∈
|
||||
`client | server | both`. Hand-edit freely; `classify-mods` won't clobber it.
|
||||
|
||||
## Tooling
|
||||
|
||||
### `tooling/classify-mods.py` (dev-time, no network)
|
||||
1. Enumerate `$DISTRIBUTION_WEB_ROOT/servers/ulicraft-1.21.1/forgemods/required/*.jar`.
|
||||
2. For each jar not already in `mods-sides.json`: parse manifest → derive side
|
||||
(table above) → add entry.
|
||||
3. Report: counts per side, and **which jars were defaulted to `both`** (so you
|
||||
know what to review). Never overwrites existing entries.
|
||||
4. Optionally warn on entries in `mods-sides.json` whose jar no longer exists.
|
||||
|
||||
### `tooling/sync-server-mods.sh` (deploy-time, offline) — replaces `render-pack.sh`
|
||||
1. Read `mods-sides.json`.
|
||||
2. `mkdir -p server-mods`; for each `both`/`server` jar, copy from
|
||||
`$DISTRIBUTION_WEB_ROOT/.../forgemods/required/` → `server-mods/`.
|
||||
3. Remove any `server-mods/*.jar` not currently selected (keeps it authoritative;
|
||||
`REMOVE_OLD_MODS=TRUE` in the container also enforces this on `/data/mods`).
|
||||
4. **Halt** (non-zero exit) if a selected jar is missing from `$DIST`, or if
|
||||
`mods-sides.json` is absent — cautious, no silent partial sync.
|
||||
|
||||
## docker-compose.yml changes
|
||||
|
||||
```yaml
|
||||
# remove:
|
||||
# PACKWIZ_URL: ...
|
||||
# extra_hosts: pack.${BASE_DOMAIN}:host-gateway (auth. stays — authlib)
|
||||
environment:
|
||||
MODS_FILE: "" # ensure unset
|
||||
REMOVE_OLD_MODS: "TRUE" # mounted folder is authoritative
|
||||
volumes:
|
||||
- mc_data:/data
|
||||
- ./runtime:/extras:ro
|
||||
- ./server-mods:/mods:ro # itzg auto-syncs /mods → /data/mods
|
||||
```
|
||||
|
||||
`depends_on: caddy` can drop (server no longer fetches `pack.` at boot); keep
|
||||
`drasl` (authlib). `pack.` caddy vhost + the `./pack`/`./custom` mounts become
|
||||
dead — remove in the same pass.
|
||||
|
||||
## Deploy flow (updated `plan/14`)
|
||||
|
||||
```
|
||||
git pull --ff-only
|
||||
tooling/render-config.sh # drasl/nmsr/etc. (unchanged)
|
||||
tooling/sync-server-mods.sh # NEW — replaces render-pack.sh
|
||||
tooling/fetch-authlib.sh
|
||||
docker compose down --remove-orphans
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
`$DISTRIBUTION_WEB_ROOT` must be present on the host (already is — caddy mounts
|
||||
it). `classify-mods.py` is a dev step run when the modset changes; it is **not**
|
||||
in the deploy path (keeps deploy offline + deterministic).
|
||||
|
||||
## Decommission checklist (on execution)
|
||||
|
||||
- [ ] `tooling/classify-mods.py` + generate initial `mods-sides.json` (review it).
|
||||
- [ ] `tooling/sync-server-mods.sh`.
|
||||
- [ ] compose: drop `PACKWIZ_URL`, `pack.` extra_host, `./pack`/`./custom` mounts,
|
||||
`pack.` caddy vhost; add `server-mods` mount + `REMOVE_OLD_MODS`.
|
||||
- [ ] delete `tooling/render-pack.sh`; remove its call from docs/deploy.
|
||||
- [ ] delete `pack/` (pack.toml, index.toml, `*.pw.toml*`, `.packwizignore`,
|
||||
CustomSkinLoader templates if duplicated in DIST) and `custom/` (jars now
|
||||
sourced from `$DIST`).
|
||||
- [ ] `.gitignore`: drop packwiz lines; add `server-mods/`.
|
||||
- [ ] supersede `plan/04-packwiz.md`; update `05-minecraft.md`, `14-deploy.md`,
|
||||
`CLAUDE.md` (mod source section), and the landing join steps if they still
|
||||
reference `pack.toml`.
|
||||
- [ ] resolves the host-local divergence around `pack/` ([[cochi-host-local-divergence]]).
|
||||
|
||||
## Open questions / risks
|
||||
|
||||
- **Cosmetic-client mods** load on the server unless manually set `client` — RAM
|
||||
waste, not a crash. Curate `mods-sides.json` over time.
|
||||
- **No manifest** in a jar → defaults `both`. Acceptable; review the defaulted
|
||||
list `classify-mods` prints.
|
||||
- **Nebula regen** may rename jars (version bumps) → `mods-sides.json` keys go
|
||||
stale; `classify-mods` re-seeds new names as `both`, and `sync` warns on
|
||||
missing-from-DIST. Re-curate on big mod updates.
|
||||
Reference in New Issue
Block a user