diff --git a/.gitignore b/.gitignore index 2050418..ab3f85f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,15 +6,13 @@ runtime/authlib-injector.jar # rendered configs (from tooling/render-config.sh) drasl/config/config.toml nmsr/config.toml -# packwiz pack files rendered from pack/**/*.tmpl by tooling/render-pack.sh -# (domain-dependent build output); source of truth is the .tmpl + custom/ jars. -pack/mods/*.pw.toml -pack/index.toml -pack/CustomSkinLoader/CustomSkinLoader.json + +# server mod subset synced from the distribution repo by +# tooling/sync-server-mods.sh (source of truth is mods-sides.json + $DIST jars) +server-mods/ # vendored binaries launcher/ -pack/mods-files/ # landing page: generated build output + deps www/ diff --git a/CLAUDE.md b/CLAUDE.md index 39a45f3..7e2270f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `). 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 ~50–100 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 diff --git a/caddy/Caddyfile b/caddy/Caddyfile index eba7263..b25d2a6 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -1,5 +1,5 @@ # Base Caddy config. Vhosts live in conf.d/*.caddy snippets: -# conf.d/00-core.caddy auth + packwiz +# conf.d/00-core.caddy auth # conf.d/10-static.caddy apex landing + launcher downloads # conf.d/30-distribution.caddy static site from another repo # conf.d/40-avatar.caddy nmsr skin/avatar renderer diff --git a/caddy/conf.d/00-core.caddy b/caddy/conf.d/00-core.caddy index b58407d..d97ca74 100644 --- a/caddy/conf.d/00-core.caddy +++ b/caddy/conf.d/00-core.caddy @@ -1,9 +1,4 @@ -# Core ingress — always mounted. Drasl auth + packwiz pack metadata. +# Core ingress — always mounted. Drasl auth. http://auth.{$BASE_DOMAIN} { reverse_proxy drasl:25585 } - -http://pack.{$BASE_DOMAIN} { - root * /srv/pack - file_server browse -} diff --git a/custom/BetterF3-11.0.3-NeoForge-1.21.1.jar b/custom/BetterF3-11.0.3-NeoForge-1.21.1.jar deleted file mode 100644 index 5589011..0000000 Binary files a/custom/BetterF3-11.0.3-NeoForge-1.21.1.jar and /dev/null differ diff --git a/custom/BridgingMod-2.6.2+1.21.1.neoforge-release.jar b/custom/BridgingMod-2.6.2+1.21.1.neoforge-release.jar deleted file mode 100644 index 1d422b4..0000000 Binary files a/custom/BridgingMod-2.6.2+1.21.1.neoforge-release.jar and /dev/null differ diff --git a/custom/Controlling-neoforge-1.21.1-19.0.5.jar b/custom/Controlling-neoforge-1.21.1-19.0.5.jar deleted file mode 100644 index e8d0868..0000000 Binary files a/custom/Controlling-neoforge-1.21.1-19.0.5.jar and /dev/null differ diff --git a/custom/CustomSkinLoader_Universal-14.28.jar b/custom/CustomSkinLoader_Universal-14.28.jar deleted file mode 100644 index 5d77af4..0000000 Binary files a/custom/CustomSkinLoader_Universal-14.28.jar and /dev/null differ diff --git a/custom/ExplorersCompass-1.21.1-3.4.0-neoforge.jar b/custom/ExplorersCompass-1.21.1-3.4.0-neoforge.jar deleted file mode 100644 index daab5cd..0000000 Binary files a/custom/ExplorersCompass-1.21.1-3.4.0-neoforge.jar and /dev/null differ diff --git a/custom/FarmersDelight-1.21.1-1.3.2.jar b/custom/FarmersDelight-1.21.1-1.3.2.jar deleted file mode 100644 index 2fef351..0000000 Binary files a/custom/FarmersDelight-1.21.1-1.3.2.jar and /dev/null differ diff --git a/custom/FramedBlocks-10.6.0.jar b/custom/FramedBlocks-10.6.0.jar deleted file mode 100644 index 3a54aeb..0000000 Binary files a/custom/FramedBlocks-10.6.0.jar and /dev/null differ diff --git a/custom/ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar b/custom/ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar deleted file mode 100644 index fba292a..0000000 Binary files a/custom/ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar and /dev/null differ diff --git a/custom/Jade-1.21.1-NeoForge-15.10.5.jar b/custom/Jade-1.21.1-NeoForge-15.10.5.jar deleted file mode 100644 index 8113653..0000000 Binary files a/custom/Jade-1.21.1-NeoForge-15.10.5.jar and /dev/null differ diff --git a/custom/JadeAddons-1.21.1-NeoForge-6.1.0.jar b/custom/JadeAddons-1.21.1-NeoForge-6.1.0.jar deleted file mode 100644 index 09b2398..0000000 Binary files a/custom/JadeAddons-1.21.1-NeoForge-6.1.0.jar and /dev/null differ diff --git a/custom/JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar b/custom/JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar deleted file mode 100644 index 0634bb0..0000000 Binary files a/custom/JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar and /dev/null differ diff --git a/custom/JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar b/custom/JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar deleted file mode 100644 index 0e86f12..0000000 Binary files a/custom/JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar and /dev/null differ diff --git a/custom/MouseTweaks-neoforge-mc1.21-2.26.1.jar b/custom/MouseTweaks-neoforge-mc1.21-2.26.1.jar deleted file mode 100644 index 1623784..0000000 Binary files a/custom/MouseTweaks-neoforge-mc1.21-2.26.1.jar and /dev/null differ diff --git a/custom/Necronomicon-NeoForge-1.6.0+1.21.jar b/custom/Necronomicon-NeoForge-1.6.0+1.21.jar deleted file mode 100644 index 394687c..0000000 Binary files a/custom/Necronomicon-NeoForge-1.6.0+1.21.jar and /dev/null differ diff --git a/custom/Patchouli-1.21.1-93-NEOFORGE.jar b/custom/Patchouli-1.21.1-93-NEOFORGE.jar deleted file mode 100644 index 610c901..0000000 Binary files a/custom/Patchouli-1.21.1-93-NEOFORGE.jar and /dev/null differ diff --git a/custom/Searchables-neoforge-1.21.1-1.0.2.jar b/custom/Searchables-neoforge-1.21.1-1.0.2.jar deleted file mode 100644 index 3e97e55..0000000 Binary files a/custom/Searchables-neoforge-1.21.1-1.0.2.jar and /dev/null differ diff --git a/custom/TravelersTitles-1.21.1-NeoForge-5.1.3.jar b/custom/TravelersTitles-1.21.1-NeoForge-5.1.3.jar deleted file mode 100644 index 4ee8a66..0000000 Binary files a/custom/TravelersTitles-1.21.1-NeoForge-5.1.3.jar and /dev/null differ diff --git a/custom/YungsApi-1.21.1-NeoForge-5.1.6.jar b/custom/YungsApi-1.21.1-NeoForge-5.1.6.jar deleted file mode 100644 index 859d5e2..0000000 Binary files a/custom/YungsApi-1.21.1-NeoForge-5.1.6.jar and /dev/null differ diff --git a/custom/appleskin-neoforge-mc1.21-3.0.9.jar b/custom/appleskin-neoforge-mc1.21-3.0.9.jar deleted file mode 100644 index 4028d08..0000000 Binary files a/custom/appleskin-neoforge-mc1.21-3.0.9.jar and /dev/null differ diff --git a/custom/architectury-13.0.8-neoforge.jar b/custom/architectury-13.0.8-neoforge.jar deleted file mode 100644 index eb846ef..0000000 Binary files a/custom/architectury-13.0.8-neoforge.jar and /dev/null differ diff --git a/custom/baguettelib-1.21.1-NeoForge-2.0.3.jar b/custom/baguettelib-1.21.1-NeoForge-2.0.3.jar deleted file mode 100644 index 2def853..0000000 Binary files a/custom/baguettelib-1.21.1-NeoForge-2.0.3.jar and /dev/null differ diff --git a/custom/balm-neoforge-1.21.1-21.0.58.jar b/custom/balm-neoforge-1.21.1-21.0.58.jar deleted file mode 100644 index 3e6472e..0000000 Binary files a/custom/balm-neoforge-1.21.1-21.0.58.jar and /dev/null differ diff --git a/custom/cardboardboxes-1.21.1-0.1.2.jar b/custom/cardboardboxes-1.21.1-0.1.2.jar deleted file mode 100644 index 4868320..0000000 Binary files a/custom/cardboardboxes-1.21.1-0.1.2.jar and /dev/null differ diff --git a/custom/cloth-config-15.0.140-neoforge.jar b/custom/cloth-config-15.0.140-neoforge.jar deleted file mode 100644 index 8775b30..0000000 Binary files a/custom/cloth-config-15.0.140-neoforge.jar and /dev/null differ diff --git a/custom/comforts-neoforge-9.0.5+1.21.1.jar b/custom/comforts-neoforge-9.0.5+1.21.1.jar deleted file mode 100644 index b28e247..0000000 Binary files a/custom/comforts-neoforge-9.0.5+1.21.1.jar and /dev/null differ diff --git a/custom/configured-neoforge-1.21.1-2.6.3.jar b/custom/configured-neoforge-1.21.1-2.6.3.jar deleted file mode 100644 index 0eeadf9..0000000 Binary files a/custom/configured-neoforge-1.21.1-2.6.3.jar and /dev/null differ diff --git a/custom/corpse-neoforge-1.21.1-1.1.13.jar b/custom/corpse-neoforge-1.21.1-1.1.13.jar deleted file mode 100644 index 4a8b68f..0000000 Binary files a/custom/corpse-neoforge-1.21.1-1.1.13.jar and /dev/null differ diff --git a/custom/corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar b/custom/corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar deleted file mode 100644 index 84b322c..0000000 Binary files a/custom/corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar and /dev/null differ diff --git a/custom/curios-neoforge-9.5.1+1.21.1.jar b/custom/curios-neoforge-9.5.1+1.21.1.jar deleted file mode 100644 index bdd9486..0000000 Binary files a/custom/curios-neoforge-9.5.1+1.21.1.jar and /dev/null differ diff --git a/custom/drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar b/custom/drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar deleted file mode 100644 index fa80149..0000000 Binary files a/custom/drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar and /dev/null differ diff --git a/custom/entity_model_features-3.2.4-1.21-neoforge.jar b/custom/entity_model_features-3.2.4-1.21-neoforge.jar deleted file mode 100644 index ee704a7..0000000 Binary files a/custom/entity_model_features-3.2.4-1.21-neoforge.jar and /dev/null differ diff --git a/custom/entity_texture_features_1.21-neoforge-7.1.jar b/custom/entity_texture_features_1.21-neoforge-7.1.jar deleted file mode 100644 index 0d08799..0000000 Binary files a/custom/entity_texture_features_1.21-neoforge-7.1.jar and /dev/null differ diff --git a/custom/entityculling-neoforge-1.10.2-mc1.21.1.jar b/custom/entityculling-neoforge-1.10.2-mc1.21.1.jar deleted file mode 100644 index b9fe8d5..0000000 Binary files a/custom/entityculling-neoforge-1.10.2-mc1.21.1.jar and /dev/null differ diff --git a/custom/fancymenu_neoforge_3.9.1_MC_1.21.1.jar b/custom/fancymenu_neoforge_3.9.1_MC_1.21.1.jar deleted file mode 100644 index 6a50f54..0000000 Binary files a/custom/fancymenu_neoforge_3.9.1_MC_1.21.1.jar and /dev/null differ diff --git a/custom/fastleafdecay-35.jar b/custom/fastleafdecay-35.jar deleted file mode 100644 index 09390e5..0000000 Binary files a/custom/fastleafdecay-35.jar and /dev/null differ diff --git a/custom/ferritecore-7.0.3-neoforge.jar b/custom/ferritecore-7.0.3-neoforge.jar deleted file mode 100644 index a2d5384..0000000 Binary files a/custom/ferritecore-7.0.3-neoforge.jar and /dev/null differ diff --git a/custom/ftb-chunks-neoforge-2101.1.17.jar b/custom/ftb-chunks-neoforge-2101.1.17.jar deleted file mode 100644 index 7325763..0000000 Binary files a/custom/ftb-chunks-neoforge-2101.1.17.jar and /dev/null differ diff --git a/custom/ftb-essentials-neoforge-2101.1.9.jar b/custom/ftb-essentials-neoforge-2101.1.9.jar deleted file mode 100644 index e10b627..0000000 Binary files a/custom/ftb-essentials-neoforge-2101.1.9.jar and /dev/null differ diff --git a/custom/ftb-library-neoforge-2101.1.31.jar b/custom/ftb-library-neoforge-2101.1.31.jar deleted file mode 100644 index efb14d1..0000000 Binary files a/custom/ftb-library-neoforge-2101.1.31.jar and /dev/null differ diff --git a/custom/ftb-teams-neoforge-2101.1.10.jar b/custom/ftb-teams-neoforge-2101.1.10.jar deleted file mode 100644 index ab07249..0000000 Binary files a/custom/ftb-teams-neoforge-2101.1.10.jar and /dev/null differ diff --git a/custom/ftb-ultimine-neoforge-2101.1.14.jar b/custom/ftb-ultimine-neoforge-2101.1.14.jar deleted file mode 100644 index 0bf1860..0000000 Binary files a/custom/ftb-ultimine-neoforge-2101.1.14.jar and /dev/null differ diff --git a/custom/functionalstorage-1.21.1-1.5.7.jar b/custom/functionalstorage-1.21.1-1.5.7.jar deleted file mode 100644 index 197903d..0000000 Binary files a/custom/functionalstorage-1.21.1-1.5.7.jar and /dev/null differ diff --git a/custom/handcrafted-neoforge-1.21.1-4.0.3.jar b/custom/handcrafted-neoforge-1.21.1-4.0.3.jar deleted file mode 100644 index 8c10a92..0000000 Binary files a/custom/handcrafted-neoforge-1.21.1-4.0.3.jar and /dev/null differ diff --git a/custom/inventoryessentials-neoforge-1.21.1-21.1.15.jar b/custom/inventoryessentials-neoforge-1.21.1-21.1.15.jar deleted file mode 100644 index edbb4b6..0000000 Binary files a/custom/inventoryessentials-neoforge-1.21.1-21.1.15.jar and /dev/null differ diff --git a/custom/iris-neoforge-1.8.12+mc1.21.1.jar b/custom/iris-neoforge-1.8.12+mc1.21.1.jar deleted file mode 100644 index edb687d..0000000 Binary files a/custom/iris-neoforge-1.8.12+mc1.21.1.jar and /dev/null differ diff --git a/custom/jamlib-neoforge-1.3.6+1.21.1.jar b/custom/jamlib-neoforge-1.3.6+1.21.1.jar deleted file mode 100644 index 8907518..0000000 Binary files a/custom/jamlib-neoforge-1.3.6+1.21.1.jar and /dev/null differ diff --git a/custom/jei-1.21.1-neoforge-19.27.0.340.jar b/custom/jei-1.21.1-neoforge-19.27.0.340.jar deleted file mode 100644 index 8ff59a0..0000000 Binary files a/custom/jei-1.21.1-neoforge-19.27.0.340.jar and /dev/null differ diff --git a/custom/konkrete_neoforge_1.9.9_MC_1.21.jar b/custom/konkrete_neoforge_1.9.9_MC_1.21.jar deleted file mode 100644 index 789db30..0000000 Binary files a/custom/konkrete_neoforge_1.9.9_MC_1.21.jar and /dev/null differ diff --git a/custom/kubejs-neoforge-2101.7.2-build.368.jar b/custom/kubejs-neoforge-2101.7.2-build.368.jar deleted file mode 100644 index 909587a..0000000 Binary files a/custom/kubejs-neoforge-2101.7.2-build.368.jar and /dev/null differ diff --git a/custom/kubejsdelight-1.1.6.jar b/custom/kubejsdelight-1.1.6.jar deleted file mode 100644 index d92765e..0000000 Binary files a/custom/kubejsdelight-1.1.6.jar and /dev/null differ diff --git a/custom/lithium-neoforge-0.15.3+mc1.21.1.jar b/custom/lithium-neoforge-0.15.3+mc1.21.1.jar deleted file mode 100644 index d7722eb..0000000 Binary files a/custom/lithium-neoforge-0.15.3+mc1.21.1.jar and /dev/null differ diff --git a/custom/lootr-neoforge-1.21.1-1.11.37.120.jar b/custom/lootr-neoforge-1.21.1-1.11.37.120.jar deleted file mode 100644 index 0afe5b9..0000000 Binary files a/custom/lootr-neoforge-1.21.1-1.11.37.120.jar and /dev/null differ diff --git a/custom/melody_neoforge_1.0.10_MC_1.21.jar b/custom/melody_neoforge_1.0.10_MC_1.21.jar deleted file mode 100644 index 64ceeed..0000000 Binary files a/custom/melody_neoforge_1.0.10_MC_1.21.jar and /dev/null differ diff --git a/custom/morefunctionalstorage-1.2.3.jar b/custom/morefunctionalstorage-1.2.3.jar deleted file mode 100644 index dc718d0..0000000 Binary files a/custom/morefunctionalstorage-1.2.3.jar and /dev/null differ diff --git a/custom/polymorph-neoforge-1.1.0+1.21.1.jar b/custom/polymorph-neoforge-1.1.0+1.21.1.jar deleted file mode 100644 index db2ea5a..0000000 Binary files a/custom/polymorph-neoforge-1.1.0+1.21.1.jar and /dev/null differ diff --git a/custom/ponderjs-neoforge-1.21.1-2.4.0.jar b/custom/ponderjs-neoforge-1.21.1-2.4.0.jar deleted file mode 100644 index c723d22..0000000 Binary files a/custom/ponderjs-neoforge-1.21.1-2.4.0.jar and /dev/null differ diff --git a/custom/resourcefullib-neoforge-1.21-3.0.12.jar b/custom/resourcefullib-neoforge-1.21-3.0.12.jar deleted file mode 100644 index 7ff520d..0000000 Binary files a/custom/resourcefullib-neoforge-1.21-3.0.12.jar and /dev/null differ diff --git a/custom/rhino-2101.2.7-build.85.jar b/custom/rhino-2101.2.7-build.85.jar deleted file mode 100644 index 386f868..0000000 Binary files a/custom/rhino-2101.2.7-build.85.jar and /dev/null differ diff --git a/custom/rightclickharvest-neoforge-4.6.1+1.21.1.jar b/custom/rightclickharvest-neoforge-4.6.1+1.21.1.jar deleted file mode 100644 index e79b208..0000000 Binary files a/custom/rightclickharvest-neoforge-4.6.1+1.21.1.jar and /dev/null differ diff --git a/custom/simplemagnets-1.1.12c-neoforge-mc1.21.jar b/custom/simplemagnets-1.1.12c-neoforge-mc1.21.jar deleted file mode 100644 index f923789..0000000 Binary files a/custom/simplemagnets-1.1.12c-neoforge-mc1.21.jar and /dev/null differ diff --git a/custom/sodium-extra-neoforge-0.8.7+mc1.21.1.jar b/custom/sodium-extra-neoforge-0.8.7+mc1.21.1.jar deleted file mode 100644 index a681eff..0000000 Binary files a/custom/sodium-extra-neoforge-0.8.7+mc1.21.1.jar and /dev/null differ diff --git a/custom/sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar b/custom/sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar deleted file mode 100644 index ff18c69..0000000 Binary files a/custom/sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar and /dev/null differ diff --git a/custom/sophisticatedbackpacks-1.21.1-3.25.55.1852.jar b/custom/sophisticatedbackpacks-1.21.1-3.25.55.1852.jar deleted file mode 100644 index 50b1446..0000000 Binary files a/custom/sophisticatedbackpacks-1.21.1-3.25.55.1852.jar and /dev/null differ diff --git a/custom/sophisticatedcore-1.21.1-1.4.49.1976.jar b/custom/sophisticatedcore-1.21.1-1.4.49.1976.jar deleted file mode 100644 index ecf4afd..0000000 Binary files a/custom/sophisticatedcore-1.21.1-1.4.49.1976.jar and /dev/null differ diff --git a/custom/spark-1.10.124-neoforge.jar b/custom/spark-1.10.124-neoforge.jar deleted file mode 100644 index d5fc090..0000000 Binary files a/custom/spark-1.10.124-neoforge.jar and /dev/null differ diff --git a/custom/supermartijn642configlib-1.1.8-neoforge-mc1.21.jar b/custom/supermartijn642configlib-1.1.8-neoforge-mc1.21.jar deleted file mode 100644 index 648062d..0000000 Binary files a/custom/supermartijn642configlib-1.1.8-neoforge-mc1.21.jar and /dev/null differ diff --git a/custom/supermartijn642corelib-1.1.21-neoforge-mc1.21.jar b/custom/supermartijn642corelib-1.1.21-neoforge-mc1.21.jar deleted file mode 100644 index db68908..0000000 Binary files a/custom/supermartijn642corelib-1.1.21-neoforge-mc1.21.jar and /dev/null differ diff --git a/custom/tagtooltips-neoforge-1.21.1-1.2.0.jar b/custom/tagtooltips-neoforge-1.21.1-1.2.0.jar deleted file mode 100644 index 06d4fd2..0000000 Binary files a/custom/tagtooltips-neoforge-1.21.1-1.2.0.jar and /dev/null differ diff --git a/custom/titanium-1.21-4.0.43.jar b/custom/titanium-1.21-4.0.43.jar deleted file mode 100644 index a714504..0000000 Binary files a/custom/titanium-1.21-4.0.43.jar and /dev/null differ diff --git a/custom/torchmaster-neoforge-1.21.1-21.1.9.jar b/custom/torchmaster-neoforge-1.21.1-21.1.9.jar deleted file mode 100644 index ff6df04..0000000 Binary files a/custom/torchmaster-neoforge-1.21.1-21.1.9.jar and /dev/null differ diff --git a/custom/trashcans-1.0.18c-neoforge-mc1.21.jar b/custom/trashcans-1.0.18c-neoforge-mc1.21.jar deleted file mode 100644 index 32379a1..0000000 Binary files a/custom/trashcans-1.0.18c-neoforge-mc1.21.jar and /dev/null differ diff --git a/custom/utilitarian-1.21.1-0.19.2.jar b/custom/utilitarian-1.21.1-0.19.2.jar deleted file mode 100644 index eeafc48..0000000 Binary files a/custom/utilitarian-1.21.1-0.19.2.jar and /dev/null differ diff --git a/custom/voicechat-neoforge-1.21.1-2.6.18.jar b/custom/voicechat-neoforge-1.21.1-2.6.18.jar deleted file mode 100644 index 5b63c79..0000000 Binary files a/custom/voicechat-neoforge-1.21.1-2.6.18.jar and /dev/null differ diff --git a/custom/welcomescreen-neoforge-1.0.0-1.21.1.jar b/custom/welcomescreen-neoforge-1.0.0-1.21.1.jar deleted file mode 100644 index f974e74..0000000 Binary files a/custom/welcomescreen-neoforge-1.0.0-1.21.1.jar and /dev/null differ diff --git a/custom/yeetusexperimentus-neoforge-87.0.0.jar b/custom/yeetusexperimentus-neoforge-87.0.0.jar deleted file mode 100644 index f10ead8..0000000 Binary files a/custom/yeetusexperimentus-neoforge-87.0.0.jar and /dev/null differ diff --git a/custom/yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar b/custom/yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar deleted file mode 100644 index 562bb4f..0000000 Binary files a/custom/yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar and /dev/null differ diff --git a/docker-compose.yml b/docker-compose.yml index 74a274d..6e14a93 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,9 +3,9 @@ # ────────────────────────────────────────────────────────────────── # Single, unified compose. One file holds the whole stack: # drasl auth + skins (Yggdrasil), internal only -# minecraft the server (itzg/NEOFORGE), installs mods via packwiz +# minecraft the server (itzg/NEOFORGE), mods from ./server-mods volume # mc-backup world backups every 6h via RCON -# caddy internal ingress — routes auth./pack./apex/launcher/avatar. +# caddy internal ingress — routes auth./apex/launcher/avatar. # /distribution. by Host header (conf.d/*.caddy) # nmsr skin/avatar renderer at avatar.${BASE_DOMAIN} # uptime-kuma status monitoring at status.${BASE_DOMAIN} @@ -16,7 +16,7 @@ # Production TLS terminates at the HOST's nginx in front of caddy # (nginx/ulicraft-caddy.conf.tmpl + Let's Encrypt); caddy is published on a # localhost-only port (CADDY_HTTP_BIND/CADDY_HTTP_PORT in .env). Containers reach -# the stack's own names internally via caddy's mcnet aliases (auth./pack.). +# the stack's own names internally via caddy's mcnet alias (auth.). # ────────────────────────────────────────────────────────────────── services: @@ -71,8 +71,12 @@ services: ENFORCE_SECURE_PROFILE: "FALSE" # required for authlib-injector on 1.21+ ALLOW_FLIGHT: "TRUE" # many tech mods need this - # ── Mod source: packwiz pack served by caddy (alias pack.) ── - PACKWIZ_URL: "http://pack.${BASE_DOMAIN}/pack.toml" + # ── Mod source: filtered subset of the distribution modset ── + # Jars are synced into ./server-mods by tooling/sync-server-mods.sh + # (both/server entries from mods-sides.json) and mounted at /mods below; + # itzg auto-syncs /mods → /data/mods. REMOVE_OLD_MODS makes that mirror + # authoritative so removed mods get pruned. See plan/04-mods.md. + REMOVE_OLD_MODS: "TRUE" # ── RCON for remote admin ─────────────────────────────────── ENABLE_RCON: "TRUE" @@ -82,16 +86,15 @@ services: TZ: "Europe/Madrid" volumes: - mc_data:/data - - ./runtime:/extras:ro # authlib-injector.jar lives here + - ./runtime:/extras:ro # authlib-injector.jar lives here + - ./server-mods:/mods:ro # itzg auto-syncs /mods → /data/mods depends_on: - - drasl - - caddy # pack./auth. must resolve at boot + - drasl # authlib (auth.) must resolve at boot # Containers don't inherit the host's /etc/hosts; the LAN resolver returns a - # dead address for the public names. Pin auth./pack. to the host so HTTPS to - # them lands on the host's nginx (valid LE cert) -> caddy -> service. + # dead address for the public names. Pin auth. to the host so HTTPS to it + # lands on the host's nginx (valid LE cert) -> caddy -> drasl. extra_hosts: - "auth.${BASE_DOMAIN}:host-gateway" - - "pack.${BASE_DOMAIN}:host-gateway" networks: - mcnet @@ -114,7 +117,7 @@ services: - mcnet # Internal ingress. Routes every vhost by Host header (conf.d/*.caddy): - # auth. -> drasl, pack. -> packwiz files, apex -> landing + /launcher, + # auth. -> drasl, apex -> landing + /launcher, # avatar. -> nmsr, status. -> uptime-kuma, distribution. -> static site. # Published on a localhost-only port; the host's nginx terminates TLS in # front of it (nginx/ulicraft-caddy.conf.tmpl). @@ -135,13 +138,6 @@ services: - ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro - ./www:/srv/www:ro - ./launcher:/srv/launcher:ro - - ./pack:/srv/pack:ro - # Custom (locally-hosted) mod jars live OUTSIDE ./pack so packwiz refresh - # doesn't index them as direct files. Served at pack.${BASE_DOMAIN}/custom/. - # Nested under the :ro /srv/pack mount — the mountpoint pack/custom/ must - # pre-exist on the host (tracked via pack/custom/.gitkeep) or Docker can't - # mkdir it in the read-only parent. - - ./custom:/srv/pack/custom:ro # Static site from ANOTHER repo (absolute host path in .env). - ${DISTRIBUTION_WEB_ROOT:?DISTRIBUTION_WEB_ROOT unset in .env}:/srv/distribution:ro networks: @@ -149,7 +145,6 @@ services: # Containers resolve the stack's own public names to caddy internally. aliases: - "auth.${BASE_DOMAIN}" - - "pack.${BASE_DOMAIN}" # Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls # player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080. diff --git a/landing/src/data/site.ts b/landing/src/data/site.ts index cb8af1f..7d6705d 100644 --- a/landing/src/data/site.ts +++ b/landing/src/data/site.ts @@ -43,7 +43,13 @@ export const site = { // Requires CORSAllowOrigins to include the apex in drasl config.toml. draslApiUrl: `https://auth.${BASE_DOMAIN}/drasl/api/v2`, - // packwiz modpack entrypoint (public HTTPS, pack. subdomain). + // TODO: packwiz removed — the `pack.` vhost no longer exists, so this URL 404s. + // The modpack is now distributed to clients by HeliosLauncher via the + // distribution.json (server loads a filtered subset from ./server-mods/; see + // plan/04-mods.md). The join flow still references this in the launcher step + // and needs to be redesigned around the launcher/distribution flow. Kept + // (non-empty) only so the Astro build + the CopyChip in [...lang].astro don't + // break; do not treat as live. packwizUrl: `https://pack.${BASE_DOMAIN}/pack.toml`, // NMSR avatar renderer root. Player heads come from OUR Drasl skins, not diff --git a/mods-sides.json b/mods-sides.json new file mode 100644 index 0000000..1570bfe --- /dev/null +++ b/mods-sides.json @@ -0,0 +1,78 @@ +{ + "BetterF3-11.0.3-NeoForge-1.21.1.jar": "both", + "BridgingMod-2.6.2+1.21.1.neoforge-release.jar": "client", + "Controlling-neoforge-1.21.1-19.0.5.jar": "client", + "CustomSkinLoader_Universal-14.28.jar": "client", + "ExplorersCompass-1.21.1-3.4.0-neoforge.jar": "both", + "FarmersDelight-1.21.1-1.3.2.jar": "both", + "FramedBlocks-10.6.0.jar": "both", + "ImmediatelyFast-NeoForge-1.6.10+1.21.1.jar": "client", + "Jade-1.21.1-NeoForge-15.10.5.jar": "both", + "JadeAddons-1.21.1-NeoForge-6.1.0.jar": "both", + "JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar": "both", + "JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar": "both", + "MouseTweaks-neoforge-mc1.21-2.26.1.jar": "both", + "Necronomicon-NeoForge-1.6.0+1.21.jar": "both", + "Patchouli-1.21.1-93-NEOFORGE.jar": "both", + "Searchables-neoforge-1.21.1-1.0.2.jar": "both", + "TravelersTitles-1.21.1-NeoForge-5.1.3.jar": "both", + "YungsApi-1.21.1-NeoForge-5.1.6.jar": "both", + "appleskin-neoforge-mc1.21-3.0.9.jar": "client", + "architectury-13.0.8-neoforge.jar": "both", + "baguettelib-1.21.1-NeoForge-2.0.3.jar": "both", + "balm-neoforge-1.21.1-21.0.58.jar": "both", + "cardboardboxes-1.21.1-0.1.2.jar": "both", + "cloth-config-15.0.140-neoforge.jar": "both", + "comforts-neoforge-9.0.5+1.21.1.jar": "both", + "configured-neoforge-1.21.1-2.6.3.jar": "both", + "corpse-neoforge-1.21.1-1.1.13.jar": "both", + "corpsecurioscompat-1.21.1-NeoForge-4.0.1.jar": "both", + "curios-neoforge-9.5.1+1.21.1.jar": "both", + "drippyloadingscreen_neoforge_3.1.2_MC_1.21.1.jar": "both", + "entity_model_features-3.2.4-1.21-neoforge.jar": "client", + "entity_texture_features_1.21-neoforge-7.1.jar": "client", + "entityculling-neoforge-1.10.2-mc1.21.1.jar": "both", + "fancymenu_neoforge_3.9.1_MC_1.21.1.jar": "both", + "fastleafdecay-35.jar": "both", + "ferritecore-7.0.3-neoforge.jar": "both", + "ftb-chunks-neoforge-2101.1.17.jar": "both", + "ftb-essentials-neoforge-2101.1.9.jar": "both", + "ftb-library-neoforge-2101.1.31.jar": "both", + "ftb-teams-neoforge-2101.1.10.jar": "both", + "ftb-ultimine-neoforge-2101.1.14.jar": "both", + "functionalstorage-1.21.1-1.5.7.jar": "both", + "handcrafted-neoforge-1.21.1-4.0.3.jar": "both", + "inventoryessentials-neoforge-1.21.1-21.1.15.jar": "both", + "iris-neoforge-1.8.12+mc1.21.1.jar": "client", + "jamlib-neoforge-1.3.6+1.21.1.jar": "both", + "jei-1.21.1-neoforge-19.27.0.340.jar": "both", + "konkrete_neoforge_1.9.9_MC_1.21.jar": "both", + "kubejs-neoforge-2101.7.2-build.368.jar": "both", + "kubejsdelight-1.1.6.jar": "both", + "lithium-neoforge-0.15.3+mc1.21.1.jar": "both", + "lootr-neoforge-1.21.1-1.11.37.120.jar": "both", + "melody_neoforge_1.0.10_MC_1.21.jar": "both", + "morefunctionalstorage-1.2.3.jar": "both", + "polymorph-neoforge-1.1.0+1.21.1.jar": "both", + "ponderjs-neoforge-1.21.1-2.4.0.jar": "both", + "resourcefullib-neoforge-1.21-3.0.12.jar": "both", + "rhino-2101.2.7-build.85.jar": "both", + "rightclickharvest-neoforge-4.6.1+1.21.1.jar": "both", + "simplemagnets-1.1.12c-neoforge-mc1.21.jar": "both", + "sodium-extra-neoforge-0.8.7+mc1.21.1.jar": "client", + "sodium-neoforge-0.8.12-alpha.4+mc1.21.1.jar": "client", + "sophisticatedbackpacks-1.21.1-3.25.55.1852.jar": "both", + "sophisticatedcore-1.21.1-1.4.49.1976.jar": "both", + "spark-1.10.124-neoforge.jar": "both", + "supermartijn642configlib-1.1.8-neoforge-mc1.21.jar": "both", + "supermartijn642corelib-1.1.21-neoforge-mc1.21.jar": "both", + "tagtooltips-neoforge-1.21.1-1.2.0.jar": "client", + "titanium-1.21-4.0.43.jar": "both", + "torchmaster-neoforge-1.21.1-21.1.9.jar": "both", + "trashcans-1.0.18c-neoforge-mc1.21.jar": "both", + "utilitarian-1.21.1-0.19.2.jar": "both", + "voicechat-neoforge-1.21.1-2.6.18.jar": "both", + "welcomescreen-neoforge-1.0.0-1.21.1.jar": "both", + "yeetusexperimentus-neoforge-87.0.0.jar": "both", + "yet_another_config_lib_v3-3.8.2+1.21.1-neoforge.jar": "both" +} diff --git a/pack/.packwizignore b/pack/.packwizignore deleted file mode 100644 index c15a823..0000000 --- a/pack/.packwizignore +++ /dev/null @@ -1,3 +0,0 @@ -# Template sources for pack metadata/config — rendered to their real files by -# tooling/render-pack.sh. packwiz must NOT index the templates themselves. -*.tmpl diff --git a/pack/CustomSkinLoader/CustomSkinLoader.json.tmpl b/pack/CustomSkinLoader/CustomSkinLoader.json.tmpl deleted file mode 100644 index 539817c..0000000 --- a/pack/CustomSkinLoader/CustomSkinLoader.json.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{ - "enable": true, - "loadlist": [ - { - "name": "Ulicraft Skins", - "type": "MojangAPI", - "apiRoot": "https://auth.${BASE_DOMAIN}/account/", - "sessionRoot": "https://auth.${BASE_DOMAIN}/session/" - } - ] -} diff --git a/pack/README.md b/pack/README.md deleted file mode 100644 index 306a9d9..0000000 --- a/pack/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ulicraft modpack (packwiz) - -This directory is the **single source of truth** for the modpack (MC 1.21.1, NeoForge 21.1.209). Curate mods from here with `packwiz modrinth add ` (prefer Modrinth over CurseForge); each mod produces a `mods/.pw.toml` metadata file referencing the upstream CDN. **Client-only mods must be tagged `side = "client"`** in their `.pw.toml` so the itzg server does not pull and crash on them. After adding/removing mods run `packwiz refresh` to recompute `index.toml`. Clients and the itzg server fetch jars from the upstream CDN referenced in each `.pw.toml`. Curation is an ongoing manual task — no mods are added yet. diff --git a/pack/custom/.gitkeep b/pack/custom/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/pack/mods/balm-neoforge-1-21-1-21-0-58.pw.toml.tmpl b/pack/mods/balm-neoforge-1-21-1-21-0-58.pw.toml.tmpl deleted file mode 100644 index 57da439..0000000 --- a/pack/mods/balm-neoforge-1-21-1-21-0-58.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "balm-neoforge-1.21.1-21.0.58" -filename = "balm-neoforge-1.21.1-21.0.58.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/balm-neoforge-1.21.1-21.0.58.jar" -hash-format = "sha256" -hash = "aa2463747bad4c2351869226ea7bce7839510dfd2ac387f8d1588b1ce9309c67" diff --git a/pack/mods/customskinloader-universal-14-28.pw.toml.tmpl b/pack/mods/customskinloader-universal-14-28.pw.toml.tmpl deleted file mode 100644 index 1903db8..0000000 --- a/pack/mods/customskinloader-universal-14-28.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "CustomSkinLoader_Universal-14.28" -filename = "CustomSkinLoader_Universal-14.28.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/CustomSkinLoader_Universal-14.28.jar" -hash-format = "sha256" -hash = "0008fb0a91dd1e84d52162cfe96addb5f6aed3b76d426ea7305d7624e31f1894" diff --git a/pack/mods/inventoryessentials-neoforge-1-21-1-21-1-15.pw.toml.tmpl b/pack/mods/inventoryessentials-neoforge-1-21-1-21-1-15.pw.toml.tmpl deleted file mode 100644 index 99ba2aa..0000000 --- a/pack/mods/inventoryessentials-neoforge-1-21-1-21-1-15.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "inventoryessentials-neoforge-1.21.1-21.1.15" -filename = "inventoryessentials-neoforge-1.21.1-21.1.15.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/inventoryessentials-neoforge-1.21.1-21.1.15.jar" -hash-format = "sha256" -hash = "b7a5c5526a45256551543419ba2b5d54e56c585d76a645a8cea8cb8ea843ae2c" diff --git a/pack/mods/jade-1-21-1-neoforge-15-10-5.pw.toml.tmpl b/pack/mods/jade-1-21-1-neoforge-15-10-5.pw.toml.tmpl deleted file mode 100644 index c6a810a..0000000 --- a/pack/mods/jade-1-21-1-neoforge-15-10-5.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "Jade-1.21.1-NeoForge-15.10.5" -filename = "Jade-1.21.1-NeoForge-15.10.5.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/Jade-1.21.1-NeoForge-15.10.5.jar" -hash-format = "sha256" -hash = "067bb4b007e1d6f6b79f0afe99c91252aa825472b99a76d33a60d24442f9e92d" diff --git a/pack/mods/jadeaddons-1-21-1-neoforge-6-1-0.pw.toml.tmpl b/pack/mods/jadeaddons-1-21-1-neoforge-6-1-0.pw.toml.tmpl deleted file mode 100644 index 3d40e43..0000000 --- a/pack/mods/jadeaddons-1-21-1-neoforge-6-1-0.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "JadeAddons-1.21.1-NeoForge-6.1.0" -filename = "JadeAddons-1.21.1-NeoForge-6.1.0.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/JadeAddons-1.21.1-NeoForge-6.1.0.jar" -hash-format = "sha256" -hash = "5f242668ad710092964d4d9afda8a94fa2401665ea13eea67d55d07e4dff8a64" diff --git a/pack/mods/jei-1-21-1-neoforge-19-27-0-340.pw.toml.tmpl b/pack/mods/jei-1-21-1-neoforge-19-27-0-340.pw.toml.tmpl deleted file mode 100644 index 72eda59..0000000 --- a/pack/mods/jei-1-21-1-neoforge-19-27-0-340.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "jei-1.21.1-neoforge-19.27.0.340" -filename = "jei-1.21.1-neoforge-19.27.0.340.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/jei-1.21.1-neoforge-19.27.0.340.jar" -hash-format = "sha256" -hash = "8aaf547432f1b4958239b036356b910692fe40f858c3073d996f56bbf7c99826" diff --git a/pack/mods/justenoughprofessions-neoforge-1-21-1-4-0-5.pw.toml.tmpl b/pack/mods/justenoughprofessions-neoforge-1-21-1-4-0-5.pw.toml.tmpl deleted file mode 100644 index 833671d..0000000 --- a/pack/mods/justenoughprofessions-neoforge-1-21-1-4-0-5.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "JustEnoughProfessions-neoforge-1.21.1-4.0.5" -filename = "JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/JustEnoughProfessions-neoforge-1.21.1-4.0.5.jar" -hash-format = "sha256" -hash = "b320b47f1791f56df9e1a05138f0e312e6e54541945ff15f2f1fd398cd1d4d9d" diff --git a/pack/mods/justenoughresources-neoforge-1-21-1-1-6-0-17.pw.toml.tmpl b/pack/mods/justenoughresources-neoforge-1-21-1-1-6-0-17.pw.toml.tmpl deleted file mode 100644 index 35350db..0000000 --- a/pack/mods/justenoughresources-neoforge-1-21-1-1-6-0-17.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "JustEnoughResources-NeoForge-1.21.1-1.6.0.17" -filename = "JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/JustEnoughResources-NeoForge-1.21.1-1.6.0.17.jar" -hash-format = "sha256" -hash = "7a47d69b5530704690d5a5f0726cd74a2d6c57df17dd4a8589c4ab5212f91460" diff --git a/pack/mods/mousetweaks-neoforge-mc1-21-2-26-1.pw.toml.tmpl b/pack/mods/mousetweaks-neoforge-mc1-21-2-26-1.pw.toml.tmpl deleted file mode 100644 index 991cca6..0000000 --- a/pack/mods/mousetweaks-neoforge-mc1-21-2-26-1.pw.toml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -name = "MouseTweaks-neoforge-mc1.21-2.26.1" -filename = "MouseTweaks-neoforge-mc1.21-2.26.1.jar" -side = "both" - -[download] -url = "http://pack.${BASE_DOMAIN}/custom/MouseTweaks-neoforge-mc1.21-2.26.1.jar" -hash-format = "sha256" -hash = "68e6f4201c5de97b77929a7215c9552495696ca6a3bf3ae4eacc34e135f6cc8b" diff --git a/pack/pack.toml b/pack/pack.toml deleted file mode 100644 index f663aec..0000000 --- a/pack/pack.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "Ulicraft" -author = "Oier" -version = "1.0.0" -pack-format = "packwiz:1.1.0" - -[index] -file = "index.toml" -hash-format = "sha256" -hash = "9bca0b1a4434050835cab46116d7c14aafec971ff5a95a1ee0c4232c8bcbc355" - -[versions] -minecraft = "1.21.1" -neoforge = "21.1.209" diff --git a/pack/servers.dat b/pack/servers.dat deleted file mode 100644 index 509f486..0000000 Binary files a/pack/servers.dat and /dev/null differ diff --git a/plan/04-mods.md b/plan/04-mods.md new file mode 100644 index 0000000..34ad82c --- /dev/null +++ b/plan/04-mods.md @@ -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 ← { "": "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. diff --git a/plan/04-packwiz.md b/plan/04-packwiz.md index 99d4f8a..42e54fb 100644 --- a/plan/04-packwiz.md +++ b/plan/04-packwiz.md @@ -1,37 +1,7 @@ -# packwiz — modpack source of truth +# packwiz — modpack source of truth (SUPERSEDED) -## Summary - -`packwiz` (installed at `~/go/bin/packwiz`) authors the modpack. The `./pack` -directory is the **single source of truth** for mods, consumed by both: -- the **server** (itzg `PACKWIZ_URL`), and -- **clients** (Fjord launcher imports the pack / packwiz-installer). - -Caddy serves `./pack` at `pack.ulicraft.net`. packwiz emits **metadata** -(`pack.toml`, `index.toml`, `mods/*.pw.toml`); the `.pw.toml` files reference -Modrinth/CF **CDN URLs**, so server and clients fetch the jars from those CDNs. -The stack assumes the internet is present. - -## Behaviors confirmed - -- **Prune**: packwiz-installer tracks a manifest and **removes** client mods no - longer in the index on re-run. (Server side via itzg likewise re-syncs.) -- **Download source**: jars come from the CDN URLs in `.pw.toml`, not the pack - host (which serves only metadata). -- **itzg side filtering**: itzg downloads **server-side mods only**. Client-only - mods MUST be tagged `side = "client"` (not `both`) or the server may pull and - crash on them. - -## NeoForge pinning - -`pack.toml` pins MC `1.21.1` + NeoForge `21.1.x`. Verify exact NeoForge build -against projects.neoforged.net before locking. - -## Tasks - -- [ ] `packwiz init` → `./pack` (MC 1.21.1, modloader neoforge, pinned version) -- [ ] Curate mod shortlist (perf/tech/magic/storage/QoL/worldgen/map/voice) -- [ ] Tag client-only mods `side = "client"` -- [ ] `packwiz refresh`; verify `index.toml` committed (clients fetch it) -- [ ] Point server `PACKWIZ_URL=http://pack.ulicraft.net/pack.toml` -- [ ] Test install end-to-end on a fresh client +> **Superseded by [`04-mods.md`](04-mods.md).** packwiz has been removed. +> The server now loads a filtered mod subset from a local `server-mods/` volume +> (synced from the distribution repo by `tooling/sync-server-mods.sh`); clients +> still get the full set from the HeliosLauncher `distribution.json`. +> See `04-mods.md` for the current design. diff --git a/plan/05-minecraft.md b/plan/05-minecraft.md index 870805b..be2f796 100644 --- a/plan/05-minecraft.md +++ b/plan/05-minecraft.md @@ -3,19 +3,24 @@ ## Summary `itzg/minecraft-server:java21`, NeoForge 1.21.1, offline-mode + authlib-injector -pointing at drasl. Mods via `PACKWIZ_URL`. itzg installs MC + NeoForge + -libraries + server mods on first boot and re-syncs on restart; the stack assumes -the internet is present, so `TYPE=NEOFORGE` boots normally every time. +pointing at drasl. Mods come from a local `./server-mods` volume (mounted at +`/mods`), NOT packwiz — see `04-mods.md`. itzg installs MC + NeoForge + libraries +on first boot and auto-syncs `/mods` → `/data/mods` every boot; the stack assumes +the internet is present (for the NeoForge installer), so `TYPE=NEOFORGE` boots +normally every time. ## Server config ``` TYPE=NEOFORGE, VERSION=1.21.1, NEOFORGE_VERSION=21.1.x -PACKWIZ_URL=http://pack.${BASE_DOMAIN}/pack.toml +REMOVE_OLD_MODS=TRUE # ./server-mods is the authoritative mod set +volume ./server-mods:/mods:ro ``` -First boot installs MC + NeoForge + libraries + server mods into the `mc_data` -volume. Subsequent boots re-sync the pack against `PACKWIZ_URL`. +`./server-mods` is populated by `tooling/sync-server-mods.sh` (the `both`/`server` +subset of `mods-sides.json`, copied from `$DISTRIBUTION_WEB_ROOT`). First boot +installs MC + NeoForge + libraries into the `mc_data` volume; every boot itzg +mirrors `/mods` into `/data/mods` and prunes removed ones (`REMOVE_OLD_MODS`). ## Auth / config @@ -33,8 +38,9 @@ RCON enabled for mc-backup. ## Tasks - [ ] Compose: authlib URL `http://auth.${BASE_DOMAIN}/authlib-injector` -- [ ] Compose: `PACKWIZ_URL=http://pack.${BASE_DOMAIN}/pack.toml` +- [ ] Compose: mount `./server-mods:/mods:ro` + `REMOVE_OLD_MODS=TRUE` +- [ ] Run `tooling/sync-server-mods.sh` before bring-up (populates `./server-mods`) - [ ] Mount `./runtime` for authlib-injector.jar at `/extras` -- [ ] `depends_on`: drasl + caddy (pack served before server installs) +- [ ] `depends_on`: drasl (authlib must resolve at boot) - [ ] Confirm `ENFORCE_SECURE_PROFILE=FALSE` ↔ drasl `SignPublicKeys=false` - [ ] Verify a client joins with their drasl skin diff --git a/plan/14-deploy.md b/plan/14-deploy.md index bb33b28..ffb9c29 100644 --- a/plan/14-deploy.md +++ b/plan/14-deploy.md @@ -18,7 +18,8 @@ Production host for the Ulicraft stack. ssh cochi cd /home/ubuntu/mc/ulicraft-server-v1 git pull --ff-only -tooling/render-config.sh # re-render drasl/nmsr/pack configs from .env +tooling/render-config.sh # re-render drasl/nmsr configs from .env +tooling/sync-server-mods.sh # sync ./server-mods from $DISTRIBUTION_WEB_ROOT docker compose down --remove-orphans docker compose up -d --build ``` @@ -30,12 +31,16 @@ ssh cochi 'set -e cd /home/ubuntu/mc/ulicraft-server-v1 git pull --ff-only tooling/render-config.sh + tooling/sync-server-mods.sh docker compose down --remove-orphans docker compose up -d --build' ``` -`render-config.sh` re-renders configs (and `render-pack.sh` for the packwiz -pack) from `.env` before the stack comes back up. +`render-config.sh` re-renders the drasl/nmsr configs from `.env`; +`sync-server-mods.sh` mirrors the `both`/`server` mod subset (from +`mods-sides.json`) out of `$DISTRIBUTION_WEB_ROOT` into `./server-mods` before +the stack comes back up. `$DISTRIBUTION_WEB_ROOT` must resolve on the host (it +also backs the caddy `distribution.` mount). ### Landing rebuild (not part of `compose`) @@ -52,6 +57,31 @@ docker compose restart drasl # apply CORS / RateLimit / RequireInvite `down`/`up` is not needed for a landing-only or flag-only change — rebuild `www/` and restart `drasl`. +## Known host-local divergence on `cochi` (uncommitted) + +`cochi` carries deliberate local edits that are **not in the repo**, so +`git pull --ff-only` **aborts** ("local changes would be overwritten") whenever +an incoming commit also touches `docker-compose.yml`. As of 2026-06-09 the host +has, uncommitted: + +- `docker-compose.yml` — internal URLs forced `http`→`https` + (`JVM_OPTS` authlib-injector), for the in-container HTTPS path. (The old + `PACKWIZ_URL` divergence is moot — packwiz was removed; see `04-mods.md`.) +- `dnsmasq/` (new), `caddy/caddy-root-ca.crt`, `landing/pnpm-workspace.yaml` — + internal DNS + private CA so containers resolve/trust `https://auth.` etc. +- `landing/package.json` modified. (The former `pack/` divergence is resolved — + `pack/` and `custom/` were deleted with the packwiz removal.) + +**Do not steamroll it.** Before a deploy that edits `docker-compose.yml`, either: +1. **Converge** — commit the host edits to `main` + push, then a clean ff-pull + deploys (preferred — ends the divergence). The https/dnsmasq/CA setup belongs + in the repo. +2. **Stash–pull–pop** — `git stash` → `git pull --ff-only` → `git stash pop`. + Non-destructive; safe only when the incoming and host edits are in different + regions of the file. Stop on a pop conflict. + +Never `git reset --hard` / discard on the host without confirming first. + ## What survives a hard restart Named volumes persist — world and auth data are safe: @@ -70,7 +100,8 @@ restart (a few minutes); they reconnect once `minecraft` is healthy again. `CADDY_HTTP_BIND`, `DISTRIBUTION_WEB_ROOT`, `REGISTRATION_MODE` (`invite`|`open`, defaults `invite`), and the Let's Encrypt block (`render-config.sh` halts on an unset `BASE_DOMAIN` or an invalid `REGISTRATION_MODE`). -- Docker + compose plugin, `packwiz`, `envsubst`, `git`. +- Docker + compose plugin, `envsubst`, `jq` (used by `sync-server-mods.sh`; + falls back to `python3` if absent), `git`. - Host nginx + Let's Encrypt certs installed once — see `15-letsencrypt.md`. - **First deploy only** — online prep (build landing, fetch launcher, fetch authlib into `runtime/`). Not needed for routine redeploys. @@ -78,7 +109,9 @@ restart (a few minutes); they reconnect once `minecraft` is healthy again. ## Verify ```bash -docker compose ps # caddy, drasl, minecraft, mc-backup, nmsr, uptime-kuma up +docker compose ps # caddy, drasl, minecraft, mc-backup, nmsr, mc-status, uptime-kuma up +# self-hosted live status feed (plan/15-mc-status.md): +curl -fsS "https://$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/api/mcstatus/v2/status/java/$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)" | jq .online docker compose logs -f minecraft # watch for "Done" curl -fsS "https://auth.$(grep '^BASE_DOMAIN=' .env | cut -d= -f2)/" >/dev/null && echo "auth ok" ``` diff --git a/runtime/README.md b/runtime/README.md index e723fda..629a7e2 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -8,5 +8,6 @@ Runtime drop-ins mounted read-only into the Minecraft container at `/extras`. latest from https://github.com/yushijinhun/authlib-injector/releases and verifies it's a valid jar). Gitignored. -Mods are no longer listed here; the pack is served via packwiz -(`PACKWIZ_URL`, see plan/04-packwiz.md). +Mods are not listed here; the server loads a filtered subset from the +`./server-mods` volume (synced from the distribution repo by +`tooling/sync-server-mods.sh` — see plan/04-mods.md). diff --git a/tooling/add-custom-mod.sh b/tooling/add-custom-mod.sh deleted file mode 100755 index de9aa46..0000000 --- a/tooling/add-custom-mod.sh +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env bash -# add-custom-mod.sh — add LOCAL jar(s) to the packwiz pack as "external files". -# -# Implements the "Other external files" flow from -# https://packwiz.infra.link/tutorials/creating/adding-mods/ : a .pw.toml -# that points at a stable download URL + sha256, then refresh. Here the metadata -# is written as a TEMPLATE (.pw.toml.tmpl) with a ${BASE_DOMAIN} -# placeholder and rendered by render-config.sh, so the domain is never frozen. -# -# Since the jars are local (no public URL), we HOST them on the caddy pack -# server: each jar is copied to the top-level ./custom/ dir (OUTSIDE ./pack, so -# packwiz refresh won't index it as a direct file). Caddy bind-mounts ./custom -# at /srv/pack/custom, so it is served at -# http://pack.${BASE_DOMAIN}/custom/ -# which is exactly what minecraft fetches via PACKWIZ_URL on install. -# -# Usage: -# tooling/add-custom-mod.sh [--name "Nice Name"] [--side S] [--force] -# tooling/add-custom-mod.sh [--side S] [--force] # all *.jar in dir -# # (--name not allowed) -# S = both|client|server (default both) -# -# Single-jar name defaults to the filename without .jar; folder mode derives -# each name from its filename. packwiz refresh runs ONCE at the end. -# Cautious: halts on missing tool/var, bad side, whitespace in a filename, an -# empty folder, or an existing metadata/custom jar (unless --force). -set -euo pipefail - -cd "$(dirname "$0")/.." # repo root - -# ---- helpers --------------------------------------------------------------- -log() { printf '\n\033[1;32m==>\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; } -die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; } - -slugify() { # lowercase, non-alnum -> hyphen, squeeze + trim hyphens - printf '%s' "$1" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's/[^a-z0-9]+/-/g; s/-+/-/g; s/^-//; s/-$//' -} - -# ---- args ------------------------------------------------------------------ -TARGET=""; NAME=""; SIDE="both"; FORCE=0 -while [ $# -gt 0 ]; do - case "$1" in - --name) NAME="${2:?--name needs a value}"; shift 2 ;; - --side) SIDE="${2:?--side needs a value}"; shift 2 ;; - --force) FORCE=1; shift ;; - -h|--help) sed -n '2,24p' "$0"; exit 0 ;; - -*) die "unknown flag: $1" ;; - *) [ -z "$TARGET" ] || die "only one jar or dir at a time (got extra: $1)"; TARGET="$1"; shift ;; - esac -done - -[ -n "$TARGET" ] || die "no jar/dir given. usage: tooling/add-custom-mod.sh [--side both|client|server]" -[ -e "$TARGET" ] || die "path not found: $TARGET" -case "$SIDE" in both|client|server) ;; *) die "invalid --side '$SIDE' (both|client|server)" ;; esac - -# ---- preflight ------------------------------------------------------------- -command -v packwiz >/dev/null 2>&1 || die "missing required tool: packwiz (https://packwiz.infra.link)" -command -v sha256sum >/dev/null 2>&1 || die "missing required tool: sha256sum" -command -v envsubst >/dev/null 2>&1 || die "missing required tool: envsubst (gettext)" -[ -f .env ] || die ".env not found (copy .env.example and fill it)" -# shellcheck disable=SC1091 -set -a; . ./.env; set +a -: "${BASE_DOMAIN:?BASE_DOMAIN unset in .env}" -[ -f pack/pack.toml ] || die "pack/pack.toml not found — run from a packwiz pack (see plan/04-packwiz.md)" - -# ---- collect jars ---------------------------------------------------------- -declare -a JARS=() -if [ -d "$TARGET" ]; then - [ -z "$NAME" ] || die "--name not allowed with a folder (names are derived per-jar)" - shopt -s nullglob - for f in "$TARGET"/*.jar; do JARS+=("$f"); done - shopt -u nullglob - [ "${#JARS[@]}" -gt 0 ] || die "no .jar files in folder: $TARGET" -else - JARS=("$TARGET") -fi - -mkdir -p custom pack/mods - -# ---- per-jar processing (no refresh; refresh once at the end) -------------- -add_one() { # $1 = jar path. Uses NAME (single-jar only), SIDE, FORCE, BASE_DOMAIN. - local jar="$1" filename name slug meta dest url hash - [ -f "$jar" ] || die "not a file: $jar" - filename="$(basename -- "$jar")" - case "$filename" in *.jar) ;; *) die "not a .jar: $filename" ;; esac - case "$filename" in *[[:space:]]*) die "filename has whitespace: '$filename' — rename it (URL-unsafe)" ;; esac - - name="$NAME"; [ -n "$name" ] || name="${filename%.jar}" - slug="$(slugify "$name")" - [ -n "$slug" ] || die "could not derive a slug from name '$name'" - - # Metadata is a TEMPLATE: the download URL keeps a literal ${BASE_DOMAIN} - # placeholder, rendered to the real .pw.toml by render-config.sh at deploy so - # the domain is never frozen into a committed file. - meta="pack/mods/${slug}.pw.toml.tmpl" - dest="custom/${filename}" - url_tmpl='http://pack.${BASE_DOMAIN}/custom/'"${filename}" - - if [ "$FORCE" -ne 1 ]; then - [ -e "$meta" ] && die "metadata exists: $meta (use --force to overwrite)" - [ -e "$dest" ] && die "hosted jar exists: $dest (use --force to overwrite)" - fi - - hash="$(sha256sum -- "$jar" | cut -d' ' -f1)" - # Skip the copy when the source already IS the hosted file (regen-in-place, - # e.g. `add-custom-mod.sh custom --force` to rewrite metadata). - [ "$dest" -ef "$jar" ] || cp -- "$jar" "$dest" - cat > "$meta" < http://pack.${BASE_DOMAIN}/custom/${filename} (${SIDE}, sha256 ${hash:0:12}…)" -} - -log "adding ${#JARS[@]} jar(s), side=${SIDE}" -for j in "${JARS[@]}"; do add_one "$j"; done - -# ---- render + refresh once ------------------------------------------------- -# render-pack.sh renders all pack templates (BASE_DOMAIN only) + rebuilds the -# packwiz index. Single source of pack-render logic, shared with render-config. -log "rendering metadata + refreshing index" -tooling/render-pack.sh -log "done — ${#JARS[@]} mod(s) added to the pack" -echo " commit: custom/*.jar + pack/mods/*.pw.toml.tmpl (rendered .pw.toml/index are gitignored)" diff --git a/tooling/classify-mods.py b/tooling/classify-mods.py new file mode 100755 index 0000000..62d5a68 --- /dev/null +++ b/tooling/classify-mods.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# classify-mods.py — seed/update mods-sides.json from jar manifests (tomllib only). +# +# Enumerates the distribution's server-required jars and, for each jar NOT already +# a key in mods-sides.json, reads META-INF/neoforge.mods.toml and derives a side +# from the minecraft/neoforge dependency `side`: +# side == "CLIENT" -> client +# side == "SERVER" -> server +# "BOTH" / unspecified / no manifest / parse error -> both (the default) +# Existing entries are PRESERVED verbatim (never clobbers human edits); only new +# jars are added. No network calls. Pure stdlib (Python 3.11+ for tomllib). +# +# Source dir resolution (first match wins): +# 1. CLI arg: classify-mods.py +# 2. env MODS_DIST_DIR +# 3. $DISTRIBUTION_WEB_ROOT from .env / environment +# The path may point at either the dist ROOT (…/distribution[/dist]) or directly +# at the …/servers/ulicraft-1.21.1/forgemods/required dir — both are accepted. + +import glob +import json +import os +import sys +import zipfile + +try: + import tomllib +except ModuleNotFoundError: + sys.exit("classify-mods.py requires Python 3.11+ (tomllib is stdlib)") + +REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +SIDES_JSON = os.path.join(REPO_ROOT, "mods-sides.json") +REQUIRED_SUBPATH = os.path.join("servers", "ulicraft-1.21.1", "forgemods", "required") +MANIFEST = "META-INF/neoforge.mods.toml" + + +def read_env_dist_root(): + """Read DISTRIBUTION_WEB_ROOT from environment, else from repo .env.""" + val = os.environ.get("DISTRIBUTION_WEB_ROOT") + if val: + return val + env_path = os.path.join(REPO_ROOT, ".env") + if os.path.isfile(env_path): + with open(env_path, encoding="utf-8") as fh: + for line in fh: + line = line.strip() + if line.startswith("DISTRIBUTION_WEB_ROOT="): + return line.split("=", 1)[1].strip().strip('"').strip("'") + return None + + +def resolve_required_dir(): + """Resolve the forgemods/required dir from override or .env. Halt if missing.""" + src = None + origin = None + if len(sys.argv) > 1 and sys.argv[1]: + src, origin = sys.argv[1], "CLI arg" + elif os.environ.get("MODS_DIST_DIR"): + src, origin = os.environ["MODS_DIST_DIR"], "MODS_DIST_DIR env" + else: + src = read_env_dist_root() + origin = "$DISTRIBUTION_WEB_ROOT" + if not src: + sys.exit( + "no source dir: pass a path, set MODS_DIST_DIR, or DISTRIBUTION_WEB_ROOT in .env" + ) + candidates = [os.path.join(src, REQUIRED_SUBPATH), src] + for cand in candidates: + if os.path.isdir(cand) and os.path.basename(os.path.normpath(cand)) == "required": + return cand, origin + for cand in candidates: + if os.path.isdir(cand): + return cand, origin + sys.exit( + f"forgemods/required dir not found (from {origin}={src!r}). " + f"Tried: {', '.join(candidates)}" + ) + + +def derive_side(jar_path): + """Return client|server|both from the jar's minecraft/neoforge dep side.""" + try: + with zipfile.ZipFile(jar_path) as z: + data = z.read(MANIFEST) + manifest = tomllib.loads(data.decode("utf-8")) + except (KeyError, zipfile.BadZipFile, tomllib.TOMLDecodeError, OSError, UnicodeDecodeError): + return "both" + deps = manifest.get("dependencies", {}) + if not isinstance(deps, dict): + return "both" + decided = None + for dep_list in deps.values(): + if not isinstance(dep_list, list): + continue + for d in dep_list: + if not isinstance(d, dict): + continue + if d.get("modId") in ("minecraft", "neoforge"): + side = d.get("side") + if side == "CLIENT": + return "client" + if side == "SERVER": + return "server" + # BOTH or unspecified — keep looking; a later CLIENT/SERVER wins. + decided = "both" + return decided or "both" + + +def main(): + required_dir, origin = resolve_required_dir() + jars = sorted(glob.glob(os.path.join(required_dir, "*.jar"))) + if not jars: + sys.exit(f"no jars found in {required_dir} (from {origin})") + + if os.path.isfile(SIDES_JSON): + with open(SIDES_JSON, encoding="utf-8") as fh: + sides = json.load(fh) + if not isinstance(sides, dict): + sys.exit(f"{SIDES_JSON} is not a JSON object") + else: + sides = {} + + existing_count = len(sides) + added = {} + defaulted_both = [] # newly-added jars that fell back to "both" + for jar in jars: + name = os.path.basename(jar) + if name in sides: + continue + side = derive_side(jar) + sides[name] = side + added[name] = side + if side == "both": + defaulted_both.append(name) + + # Stable, human-diffable output. + ordered = {k: sides[k] for k in sorted(sides)} + with open(SIDES_JSON, "w", encoding="utf-8") as fh: + json.dump(ordered, fh, indent=2, ensure_ascii=False) + fh.write("\n") + + # Warn about stale keys (in mods-sides.json but no jar in the dist dir). + present = {os.path.basename(j) for j in jars} + stale = sorted(k for k in sides if k not in present) + + counts = {"client": 0, "server": 0, "both": 0} + for v in sides.values(): + if v in counts: + counts[v] += 1 + + print(f"source: {required_dir} (from {origin})") + print(f"jars in dist: {len(jars)} | pre-existing entries: {existing_count} | added: {len(added)}") + print(f"sides totals — both: {counts['both']} client: {counts['client']} server: {counts['server']}") + if added: + print(f"\nnewly classified ({len(added)}):") + for name, side in sorted(added.items()): + print(f" {side:6} {name}") + if defaulted_both: + print(f"\nDEFAULTED to 'both' ({len(defaulted_both)}) — review for cosmetic-client mods:") + for name in defaulted_both: + print(f" {name}") + if stale: + print(f"\nWARNING — {len(stale)} key(s) in mods-sides.json have no jar in the dist dir:") + for name in stale: + print(f" {name}") + print(f"\nwrote {SIDES_JSON}") + + +if __name__ == "__main__": + main() diff --git a/tooling/render-config.sh b/tooling/render-config.sh index 0776f0b..c913321 100755 --- a/tooling/render-config.sh +++ b/tooling/render-config.sh @@ -29,6 +29,6 @@ render() { # $1=template $2=output render drasl/config/config.toml.tmpl drasl/config/config.toml render nmsr/config.toml.tmpl nmsr/config.toml -# pack templates (mods/*.pw.toml.tmpl, CustomSkinLoader, …) + packwiz index. -# Delegated to render-pack.sh (BASE_DOMAIN only — single source of pack render). -tooling/render-pack.sh +# Server mods are no longer rendered here — they come from the distribution repo +# via tooling/sync-server-mods.sh (run separately in the deploy flow). See +# plan/04-mods.md. diff --git a/tooling/render-pack.sh b/tooling/render-pack.sh deleted file mode 100755 index e419043..0000000 --- a/tooling/render-pack.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# render-pack.sh — render every pack template (pack/**/*.tmpl) by injecting -# ${BASE_DOMAIN}, then rebuild the packwiz index. -# -# Pack templates are domain-dependent build output (download URLs, skin API -# roots, …). This renderer needs ONLY ${BASE_DOMAIN}, so it can run while -# curating mods before the full deploy config exists. -# Both tooling/render-config.sh (deploy) and tooling/add-custom-mod.sh call it. -set -euo pipefail - -cd "$(dirname "$0")/.." # repo root - -[ -f .env ] && { set -a; . ./.env; set +a; } -: "${BASE_DOMAIN:?BASE_DOMAIN unset (set in .env)}" -command -v envsubst >/dev/null 2>&1 || { echo "missing required tool: envsubst (gettext)" >&2; exit 1; } -command -v packwiz >/dev/null 2>&1 || { echo "missing required tool: packwiz" >&2; exit 1; } -[ -f pack/pack.toml ] || { echo "pack/pack.toml not found" >&2; exit 1; } - -shopt -s globstar nullglob -tmpls=(pack/**/*.tmpl) -shopt -u globstar nullglob - -for t in "${tmpls[@]}"; do - # Substitute ONLY ${BASE_DOMAIN} so any other literal $-syntax is preserved. - envsubst '${BASE_DOMAIN}' < "$t" > "${t%.tmpl}" - echo "rendered ${t%.tmpl}" -done - -# index.toml is gitignored build output; seed a minimal one so packwiz refresh -# (which won't bootstrap a missing index) has something to populate. -[ -f pack/index.toml ] || printf 'hash-format = "sha256"\n' > pack/index.toml -( cd pack && packwiz refresh ) -echo "packwiz index refreshed (${#tmpls[@]} template(s))" diff --git a/tooling/sync-server-mods.sh b/tooling/sync-server-mods.sh new file mode 100755 index 0000000..1dc4b7a --- /dev/null +++ b/tooling/sync-server-mods.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# sync-server-mods.sh — mirror the server's mod subset into ./server-mods/. +# Replaces the old render-pack.sh (packwiz). Copies every jar mapped `both` or +# `server` in mods-sides.json from the distribution's forgemods/required dir into +# ./server-mods/, and removes any stray server-mods/*.jar not in the selection +# (authoritative mirror). The minecraft container mounts ./server-mods at /mods +# (REMOVE_OLD_MODS=TRUE), so /data/mods ends up matching exactly. +# +# Cautious by design: halts (non-zero) on a missing mods-sides.json or a selected +# jar absent from the source dir — never a silent partial sync. +# +# Source dir resolution (first match wins): +# 1. CLI arg: sync-server-mods.sh +# 2. env MODS_DIST_DIR +# 3. $DISTRIBUTION_WEB_ROOT from .env +# The path may point at the dist ROOT (…/distribution[/dist]) or directly at +# …/servers/ulicraft-1.21.1/forgemods/required — both are accepted. +set -euo pipefail + +cd "$(dirname "$0")/.." # repo root + +[ -f .env ] && { set -a; . ./.env; set +a; } + +REQUIRED_SUBPATH="servers/ulicraft-1.21.1/forgemods/required" +SIDES_JSON="mods-sides.json" +DEST="server-mods" + +# ── Resolve source dir ────────────────────────────────────────────── +if [ "${1:-}" != "" ]; then + SRC="$1"; ORIGIN="CLI arg" +elif [ "${MODS_DIST_DIR:-}" != "" ]; then + SRC="$MODS_DIST_DIR"; ORIGIN="MODS_DIST_DIR env" +elif [ "${DISTRIBUTION_WEB_ROOT:-}" != "" ]; then + SRC="$DISTRIBUTION_WEB_ROOT"; ORIGIN="\$DISTRIBUTION_WEB_ROOT" +else + echo "no source dir: pass a path, set MODS_DIST_DIR, or DISTRIBUTION_WEB_ROOT in .env" >&2 + exit 1 +fi + +if [ -d "$SRC/$REQUIRED_SUBPATH" ]; then + REQ="$SRC/$REQUIRED_SUBPATH" +elif [ "$(basename "$SRC")" = "required" ] && [ -d "$SRC" ]; then + REQ="$SRC" +else + echo "forgemods/required dir not found (from $ORIGIN=$SRC)" >&2 + echo "tried: $SRC/$REQUIRED_SUBPATH and $SRC" >&2 + exit 1 +fi + +[ -f "$SIDES_JSON" ] || { echo "$SIDES_JSON not found — run tooling/classify-mods.py first" >&2; exit 1; } + +# ── Selected jars (both|server) ───────────────────────────────────── +select_jars() { + if command -v jq >/dev/null 2>&1; then + jq -r 'to_entries[] | select(.value == "both" or .value == "server") | .key' "$SIDES_JSON" + else + python3 - "$SIDES_JSON" <<'PY' +import json, sys +with open(sys.argv[1], encoding="utf-8") as fh: + sides = json.load(fh) +for name, side in sides.items(): + if side in ("both", "server"): + print(name) +PY + fi +} + +mapfile -t SELECTED < <(select_jars | sort) +[ "${#SELECTED[@]}" -gt 0 ] || { echo "no jars selected (both/server) in $SIDES_JSON — nothing to sync" >&2; exit 1; } + +# ── Halt if any selected jar is missing from the source ───────────── +missing=() +for jar in "${SELECTED[@]}"; do + [ -f "$REQ/$jar" ] || missing+=("$jar") +done +if [ "${#missing[@]}" -gt 0 ]; then + echo "ERROR: ${#missing[@]} selected jar(s) missing from source ($REQ):" >&2 + printf ' %s\n' "${missing[@]}" >&2 + echo "refusing to do a partial sync — fix mods-sides.json or the distribution." >&2 + exit 1 +fi + +# ── Mirror: copy selected, prune everything else ──────────────────── +mkdir -p "$DEST" + +# Prune stray jars not in the current selection. +declare -A keep +for jar in "${SELECTED[@]}"; do keep["$jar"]=1; done +pruned=0 +shopt -s nullglob +for existing in "$DEST"/*.jar; do + base="$(basename "$existing")" + if [ -z "${keep[$base]:-}" ]; then + rm -f -- "$existing" + pruned=$((pruned + 1)) + fi +done +shopt -u nullglob + +# Copy selection (cp -p preserves mtime so unchanged jars don't re-touch). +copied=0 +for jar in "${SELECTED[@]}"; do + cp -p -- "$REQ/$jar" "$DEST/$jar" + copied=$((copied + 1)) +done + +echo "source: $REQ (from $ORIGIN)" +echo "synced $copied jar(s) into $DEST/ (pruned $pruned stray)."