Compare commits

...
2 Commits
Author SHA1 Message Date
oier b6b02398da feat(generators): offline data pack generators at generators.
Add generators.${BASE_DOMAIN}: a pinned fork of misode.github.io served
with its own mcmeta + vanilla-mcdoc mirror, so it works with zero internet
at the LAN party. Fork lives in ulicraft-generators (branch ulicraft),
pinned at upstream 2bfe5522.

The app is 100% client-side: every data fetch comes from the player's
browser, not from cochi. So offline capability is entirely about which
origins the bundle refers to, plus a mirror to serve them from. The vhost
sets Content-Security-Policy with connect-src 'self' as ENFORCEMENT: an
external origin that sneaks back in (or returns via an upstream merge)
then breaks loudly in prod, on an ordinary day, instead of silently at the
venue with no internet to debug it.

sync-generators-data.sh mirrors ~109M: the 1.21.1 summary/data/atlas tags,
assets-tiny (assets minus hundreds of MB of .ogg — hence no sounds
generator), vanilla-mcdoc symbols.json, and summary/versions/data.min.json,
which comes from a BRANCH head rather than a version tag and runs during
Spyglass init. It verifies a sentinel file per source instead of trusting a
clean clone exit, since a renamed upstream path would otherwise sync
"successfully" and only fail in the browser at the party.

The container serves the bundle and the mirror on one origin, so there is
no CORS, no second cert and no second DNS name.

DEPLOY ORDER MATTERS. GENERATORS_REPO must be set in cochi's .env BEFORE
pulling this: compose uses ${GENERATORS_REPO:?...}, and an unset required
variable makes docker compose fail to parse the file — taking down every
service including Minecraft, not just this one. Full runbook and the rest
of the prerequisites: plan/21-generators.md.

Verified offline in real Chromium with every non-origin request aborted:
zero off-origin attempts, zero same-origin failures, zero console errors,
generators rendering schemas and live JSON.
2026-07-21 18:59:01 +02:00
oierandClaude Opus 4.8 7164371d34 docs(mods): sync docs to the 164-jar kitchen-sink pack
The pack grew 76 -> 164 jars (133 both / 31 client) without any doc
update. Create + ~18 addons filled the tech gap, ars_nouveau + ars_creo
the magic gap, and the full YUNG's/Moog's/TerraBlender set the worldgen
gap, which also resolved the ponderjs orphan.

- 17-mod-shortlist: rewritten around the current pack; gap doc marked
  done, remaining gaps narrowed to map mod, Necronomicon orphan, C2ME.
- 04-mods: jar counts updated; the 24/52 cutover split is now labelled
  historical alongside the current 31/133.
- CLAUDE.md: WS6 partly done, stale "65 default both" line dropped, and
  the ~50-100 mod ceiling flagged as knowingly exceeded in both places.
  MAX_MEMORY 10G was sized for that old assumption - noted for re-check.
- 18-landing-rework: mods.json count 76 -> 164, "50+" stat tile noted
  stale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:29:43 +02:00
12 changed files with 500 additions and 55 deletions

No files matched your search

+16 -1
View File
@@ -95,6 +95,21 @@ FILES_SECRET_KEY=
# server-side; players never see it. openssl rand -hex 24. # server-side; players never see it. openssl rand -hex 24.
FILES_LOCAL_SECRET= FILES_LOCAL_SECRET=
# ── generators.${BASE_DOMAIN} — offline Minecraft data pack generators ────
# A pinned fork of misode.github.io, built from source and served with its own
# data mirror so it works with ZERO internet at the LAN party. The app is 100%
# client-side, so the mirror is what makes it offline-capable — not the fact
# that the host has no WAN. Full design: plan/21-generators.md.
# Absolute host path to the ulicraft-generators checkout (the fork). This is the
# docker build context; the fork carries its own Dockerfile + Caddyfile.
GENERATORS_REPO=/home/oier/projects/mc-mods/ulicraft-group/ulicraft-generators
# Where tooling/sync-generators-data.sh puts the ~107M mcmeta + vanilla-mcdoc
# mirror. Gitignored, fetched deliberately with WAN, mounted read-only.
# Must be re-synced (with a matching MC_REF) if the pinned MC version moves.
GENERATORS_DATA_DIR=./generators-data
# Only needed if using CurseForge-exclusive mods: # Only needed if using CurseForge-exclusive mods:
# CF_API_KEY=your-curseforge-api-key # CF_API_KEY=your-curseforge-api-key
@@ -102,7 +117,7 @@ FILES_LOCAL_SECRET=
# Only needed to issue real TLS certs. DNS-01 needs no inbound ports. # Only needed to issue real TLS certs. DNS-01 needs no inbound ports.
LE_EMAIL=you@example.com LE_EMAIL=you@example.com
# space-separated subdomains; apex ${BASE_DOMAIN} is always included # space-separated subdomains; apex ${BASE_DOMAIN} is always included
LE_SUBDOMAINS=auth distribution www avatar status files LE_SUBDOMAINS=auth distribution www avatar status files generators
# OVH API creds (DNS zone write). Create at https://eu.api.ovh.com/createToken/ # OVH API creds (DNS zone write). Create at https://eu.api.ovh.com/createToken/
# OVH_CK can be blank on first run — acme.sh prints an auth URL, then paste it. # OVH_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
OVH_END_POINT=ovh-eu OVH_END_POINT=ovh-eu
+5
View File
@@ -17,6 +17,11 @@ server-mods/
# vendored binaries # vendored binaries
launcher/ launcher/
# offline data mirror for generators.${BASE_DOMAIN} (~107M of mcmeta +
# vanilla-mcdoc), synced by tooling/sync-generators-data.sh. Needs WAN, so it is
# fetched deliberately before the party — never at the venue.
generators-data/
# landing page: generated build output + deps # landing page: generated build output + deps
www/ www/
landing/node_modules/ landing/node_modules/
+21 -15
View File
@@ -12,7 +12,9 @@ with `plan/` or the code, those win.
- **Players**: 4–10 friends - **Players**: 4–10 friends
- **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL) - **Modpack vibe**: Kitchen-sink (tech + magic + exploration + QoL)
- **Pack approach**: Manually curated (~50–100 mods), NOT a forked existing pack - **Pack approach**: Manually curated, NOT a forked existing pack. Original target
~50–100 mods; **actual is 164 jars** (2026-07-20, kitchen-sink expansion) —
ceiling knowingly exceeded, watch RAM
- **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge) - **Minecraft version**: 1.21.1, **NeoForge** (not classic Forge)
- **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC - **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
- **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host - **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
@@ -131,7 +133,9 @@ while every other check passed.
### Memory sizing ### Memory sizing
~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`, ~50–100 mods, 8 players, 1.21.1: `INIT_MEMORY: 4G`, `MAX_MEMORY: 10G`,
`USE_AIKAR_FLAGS: TRUE`. `USE_AIKAR_FLAGS: TRUE`. **The pack is now 164 jars / 133 server-side** (Create
ecosystem + Ars Nouveau + full worldgen) — sized for the old 50–100 assumption, so
`MAX_MEMORY` may need a bump. Re-check heap usage under load before adding more.
## Config (.env) ## Config (.env)
@@ -157,13 +161,12 @@ mirrored at apex `/launcher/`. Per guest:
## Open / Pending Work ## Open / Pending Work
### Roadmap — mod migration follow-ups (post packwiz→04-mods) ### Roadmap — mod migration follow-ups (post packwiz→04-mods)
- [ ] **Curate `mods-sides.json` cosmetic-client mods.** tomllib auto-flagged 11 - [ ] **Curate `mods-sides.json` cosmetic-client mods.** Pack is now **164 jars**
hard client-only mods; the other 65 default `both`, including client-cosmetic (2026-07-20): **31 `client` / 133 `both`**. The `both` set still contains
ones (BetterF3, entityculling, MouseTweaks, ponderjs, fancymenu, drippy, melody, client-cosmetic mods that load harmlessly on the server but waste RAM — hand-edit
welcomescreen, Searchables, JustEnoughResources, TravelersTitles, yeetus…) that them to `client` so `sync-server-mods.sh` drops them from `./server-mods`.
load harmlessly on the server but waste RAM. Hand-edit them to `client` so (Also review `appleskin`, flagged `client` — flip to `both` if server-side food
`sync-server-mods.sh` drops them from `./server-mods`. (Also review `appleskin`, data wanted.)
flagged `client` — flip to `both` if server-side food data wanted.)
### Landing rework + mod list (PLANNED — see `plan/18-landing-rework.md`) ### Landing rework + mod list (PLANNED — see `plan/18-landing-rework.md`)
Full design settled across WS1–WS6; all tasks unchecked, ready to execute. Full design settled across WS1–WS6; all tasks unchecked, ready to execute.
- [ ] **WS1 — Join flow off packwiz.** Homepage = UlicraftLauncher, 3 steps - [ ] **WS1 — Join flow off packwiz.** Homepage = UlicraftLauncher, 3 steps
@@ -181,12 +184,15 @@ Full design settled across WS1–WS6; all tasks unchecked, ready to execute.
- [ ] **WS5 — Footer status link** to `status.${BASE_DOMAIN}` (nav unchanged). - [ ] **WS5 — Footer status link** to `status.${BASE_DOMAIN}` (nav unchanged).
- [ ] **WS6 — Mod shortlist + list component (`plan/17-mod-shortlist.md`).** - [ ] **WS6 — Mod shortlist + list component (`plan/17-mod-shortlist.md`).**
Light gap doc: current 76-jar pack is vanilla+ QoL/perf; **empty** on tech & Gap doc part **DONE**: pack filled out to **164 jars** (2026-07-20) — Create
magic, thin worldgen, no map; orphan deps `ponderjs`/`Necronomicon`. Landing ecosystem (tech), Ars Nouveau + ars_creo (magic), full YUNG's + Moog's +
mod-list component = auto-generated `mods.json` + extracted logos from the TerraBlender (worldgen); `ponderjs` orphan resolved by Create. Still open: **no
distribution forgemods (`tooling/build-modlist.py`), flat alphabetical, pretty map mod** (Xaero's), `Necronomicon` orphan (no Iron's Spells), and 164 > the
names, no categories/links; feeds the "50+" stat tile. stated 50–100 ceiling. Still to build: landing mod-list component =
- [ ] Server-side perf mods (C2ME, etc.). auto-generated `mods.json` + extracted logos from the distribution forgemods
(`tooling/build-modlist.py`), flat alphabetical, pretty names, no
categories/links; feeds the "50+" stat tile.
- [ ] Server-side perf mods (C2ME, etc. — `modernfix` already in the pack).
- [x] **Filestash file share** (`files.`, 2026-07-14) — live in prod. One shared - [x] **Filestash file share** (`files.`, 2026-07-14) — live in prod. One shared
htpasswd login, writable, rendered `:ro` config. See `plan/20-files.md`. htpasswd login, writable, rendered `:ro` config. See `plan/20-files.md`.
Follow-ups: add the Uptime Kuma HTTP monitor for `files.`; the share is Follow-ups: add the Uptime Kuma HTTP monitor for `files.`; the share is
+36
View File
@@ -0,0 +1,36 @@
# Generators — a pinned, self-contained fork of misode.github.io (Minecraft
# data pack generators) at generators.${BASE_DOMAIN}. Full design: plan/21-generators.md.
#
# The container serves BOTH the app bundle and the ~107M offline data mirror
# (mcmeta + vanilla-mcdoc) that tooling/sync-generators-data.sh produces, so
# the whole thing is single-origin by construction.
#
# WHY THAT MATTERS: this app is 100% client-side. Every data fetch comes from
# the PLAYER'S BROWSER, not from this host. Blocking the container's egress
# proves nothing, and "cochi has no WAN at the party" protects nothing — a
# single leftover upstream URL simply fails in the browser, at the venue, with
# no internet available to debug it.
http://generators.{$BASE_DOMAIN} {
# The CSP is the ENFORCEMENT mechanism for offline operation, not decoration.
#
# `connect-src 'self'` is the load-bearing directive: it hard-blocks any
# fetch/XHR to an origin we do not serve. If an upstream URL survives the
# fork patch — or a future upstream merge reintroduces one — it breaks
# LOUDLY in prod, on an ordinary day with internet available to fix it,
# instead of silently at the party. This is the same lesson as the filestash
# `http://https://` incident: a 200 on `/` proves the server is up, never
# that the app works.
#
# script-src/style-src stay permissive ON PURPOSE for now. Spyglass and
# deepslate may need wasm/eval and this has NOT been characterised yet;
# tightening them is a post-party task. Loosening `connect-src` is not —
# that one is the whole point.
header {
Content-Security-Policy "default-src 'self'; connect-src 'self'; img-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self'; worker-src 'self' blob:; base-uri 'self'; form-action 'self'"
# The mirror is pinned to one MC version and changes only when
# sync-generators-data.sh is re-run, so let browsers keep it. This is
# also what makes a second visit cheap on a LAN with no upstream.
?Cache-Control "public, max-age=604800"
}
reverse_proxy generators:80
}
+31
View File
@@ -160,6 +160,7 @@ services:
- ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro - ./caddy/conf.d/40-avatar.caddy:/etc/caddy/conf.d/40-avatar.caddy:ro
- ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro - ./caddy/conf.d/50-status.caddy:/etc/caddy/conf.d/50-status.caddy:ro
- ./caddy/conf.d/60-files.caddy:/etc/caddy/conf.d/60-files.caddy:ro - ./caddy/conf.d/60-files.caddy:/etc/caddy/conf.d/60-files.caddy:ro
- ./caddy/conf.d/70-generators.caddy:/etc/caddy/conf.d/70-generators.caddy:ro
- ./www:/srv/www:ro - ./www:/srv/www:ro
- ./launcher:/srv/launcher:ro - ./launcher:/srv/launcher:ro
# Static site from ANOTHER repo (absolute host path in .env). # Static site from ANOTHER repo (absolute host path in .env).
@@ -170,6 +171,36 @@ services:
aliases: aliases:
- "auth.${BASE_DOMAIN}" - "auth.${BASE_DOMAIN}"
# Minecraft data pack generators at generators.${BASE_DOMAIN} — a pinned fork
# of misode.github.io, built from source. caddy proxies generators. -> :80.
# Full design + the offline rationale: plan/21-generators.md.
#
# Built from the FORK's own checkout (GENERATORS_REPO), not from ./docker/,
# because the fork is not published yet — unlike nmsr, its Dockerfile cannot
# git-clone a pinned upstream ref. The fork carries its own Dockerfile+Caddyfile.
#
# The container serves the app AND the data mirror on one origin. That is the
# entire offline strategy: this app is 100% client-side, so every fetch comes
# from the player's browser. At the party there is no internet — anything
# off-origin is simply dead, which is why caddy/conf.d/70-generators.caddy
# enforces `connect-src 'self'`.
generators:
build:
context: ${GENERATORS_REPO:?GENERATORS_REPO unset in .env (path to the ulicraft-generators checkout)}
args:
# Must match the mcmeta ref mirrored into GENERATORS_DATA_DIR and the
# MIRRORED_VERSION pin in the fork's src/app/services/Versions.ts.
LATEST_VERSION: "1.21.1"
image: ulicraft/generators:local
container_name: generators
restart: unless-stopped
volumes:
# ~107M mcmeta + vanilla-mcdoc mirror from tooling/sync-generators-data.sh.
# Read-only: it is a fetched artifact, never something the app writes.
- ${GENERATORS_DATA_DIR:-./generators-data}:/srv/data:ro
networks:
- mcnet
# Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls # Avatar/skin renderer — built from source (docker/nmsr/Dockerfile), pulls
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080. # player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.
nmsr: nmsr:
+24 -1
View File
@@ -34,7 +34,7 @@ limit_req_zone $binary_remote_addr zone=files_admin:10m rate=6r/m;
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN} files.${BASE_DOMAIN}; server_name ${BASE_DOMAIN} www.${BASE_DOMAIN} auth.${BASE_DOMAIN} distribution.${BASE_DOMAIN} avatar.${BASE_DOMAIN} status.${BASE_DOMAIN} files.${BASE_DOMAIN} generators.${BASE_DOMAIN};
return 301 https://$host$request_uri; return 301 https://$host$request_uri;
} }
@@ -111,6 +111,29 @@ server {
} }
} }
# Data pack generators — a pinned, self-contained fork of misode.github.io.
# Serves its own mcmeta/vanilla-mcdoc mirror from the same origin, so it works
# with ZERO internet at party time (the app is 100% client-side: every data
# fetch comes from the player's browser, not from this host). caddy sets the
# CSP that enforces that — see caddy/conf.d/70-generators.caddy.
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name generators.${BASE_DOMAIN};
ssl_certificate ${APP_DIR}/certs/generators.${BASE_DOMAIN}/cert.pem;
ssl_certificate_key ${APP_DIR}/certs/generators.${BASE_DOMAIN}/key.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://ulicraft_caddy;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server { server {
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
+6 -4
View File
@@ -6,7 +6,8 @@
## Why drop packwiz ## Why drop packwiz
- The packwiz index tracked only **9** mods while the real modset (76 jars) lives - The packwiz index tracked only **9** mods while the real modset (76 jars at the
time of cutover; 164 as of 2026-07-20) lives
in the distribution repo — packwiz was half-built and stale. in the distribution repo — packwiz was half-built and stale.
- Manual packwiz curation (`.tmpl` render → `refresh` → re-hash) is the flagged - Manual packwiz curation (`.tmpl` render → `refresh` → re-hash) is the flagged
pain point, and a single missing file (`custom/.gitkeep`) crashlooped the pain point, and a single missing file (`custom/.gitkeep`) crashlooped the
@@ -20,7 +21,7 @@
``` ```
ulicraft-distribution/dist (another repo, = $DISTRIBUTION_WEB_ROOT) ulicraft-distribution/dist (another repo, = $DISTRIBUTION_WEB_ROOT)
servers/ulicraft-1.21.1/ servers/ulicraft-1.21.1/
forgemods/required/*.jar ← 76 jars, Nebula-managed (SOURCE OF TRUTH) forgemods/required/*.jar ← 164 jars (2026-07-20), Nebula-managed (SOURCE OF TRUTH)
distribution.json ← generated by Nebula; CLIENT install list distribution.json ← generated by Nebula; CLIENT install list
│ │ │ │
│ caddy distribution. vhost │ HeliosLauncher reads it → │ caddy distribution. vhost │ HeliosLauncher reads it →
@@ -196,8 +197,9 @@ welcomescreen, yeetusexperimentus` — on top of the 11 tomllib auto-caught
(sodium, sodium-extra, iris, ImmediatelyFast, appleskin, Controlling, BridgingMod, (sodium, sodium-extra, iris, ImmediatelyFast, appleskin, Controlling, BridgingMod,
CustomSkinLoader, entity_model_features, entity_texture_features, tagtooltips). CustomSkinLoader, entity_model_features, entity_texture_features, tagtooltips).
**Current state: 24 `client` / 52 `both`.** The 52 may still hide a client-only **State at first cutover: 24 `client` / 52 `both`. Current (2026-07-20, post
mod that loaded without crashing; if a future restart trips another tech/magic/worldgen expansion): 31 `client` / 133 `both`.** The `both` set may
still hide a client-only mod that loaded without crashing; if a future restart trips another
`invalid dist DEDICATED_SERVER`, flip that one jar to `client` and re-sync — same `invalid dist DEDICATED_SERVER`, flip that one jar to `client` and re-sync — same
one-line loop. When excluding, prefer leaf mods; if a `both` mod hard-depends on one-line loop. When excluding, prefer leaf mods; if a `both` mod hard-depends on
the one you exclude you'll get a `Missing mandatory dependency` instead — put it the one you exclude you'll get a `Missing mandatory dependency` instead — put it
+48 -33
View File
@@ -1,14 +1,19 @@
# Mod shortlist + landing mod-list component (WS6) # Mod shortlist + landing mod-list component (WS6)
> **Status: PLANNED — not started.** Two separable deliverables: (1) a curated > **Status: gap doc DONE (pack filled out 2026-07-20), landing component still
> kitchen-sink gap doc (this file), (2) an auto-generated `mods.json` + logos that > PLANNED.** Two separable deliverables: (1) a curated kitchen-sink gap doc (this
> feed a flat mod-list component on the landing page (see also `18-landing-rework.md`). > file), (2) an auto-generated `mods.json` + logos that feed a flat mod-list
> component on the landing page (see also `18-landing-rework.md`).
## TL;DR ## TL;DR
- Current pack (76 jars, `mods-sides.json`) is a polished **vanilla+ QoL/perf** base, - The pack is now **kitchen-sink** — **164 jars** (`mods-sides.json`: 133 `both`,
**not yet kitchen-sink**. Strong perf + QoL + storage + FTB + voice; **empty** on 31 `client`), up from the 76-jar vanilla+ base this doc originally described.
tech and magic, **thin** on worldgen, **no** map mod. Tech (Create + ~18 addons), magic (Ars Nouveau + ars_creo) and worldgen (full
YUNG's set + Moog's structures + TerraBlender) all landed. **Map is still the
only open gap.**
- **164 jars is over the stated 50–100 ceiling** in `CLAUDE.md`. Accepted for now;
it moves the RAM/stability bar (`INIT_MEMORY 4G`/`MAX_MEMORY 10G` — watch it).
- Jars live in the **external distribution repo** (`$DISTRIBUTION_WEB_ROOT/servers/ - Jars live in the **external distribution repo** (`$DISTRIBUTION_WEB_ROOT/servers/
ulicraft-1.21.1/forgemods/required/*.jar`, Nebula-managed). Adding mods happens ulicraft-1.21.1/forgemods/required/*.jar`, Nebula-managed). Adding mods happens
there, **not** in this repo. This doc is a shortlist only; depth = **light** (names there, **not** in this repo. This doc is a shortlist only; depth = **light** (names
@@ -17,48 +22,58 @@
from the jars — a different dataset from this shortlist. Flat alphabetical, pretty from the jars — a different dataset from this shortlist. Flat alphabetical, pretty
names, extracted logos, no categories, no Modrinth/CF links. names, extracted logos, no categories, no Modrinth/CF links.
## Current pack — what's already in (76 jars) ## Current pack — what's in (164 jars)
- **Tech — Create ecosystem (the anchor):** create, createaddition, createdeco,
create_connected, copycats, create-central-kitchen, create-enchantment-industry,
create-integrated-farming, create_jetpack(+curios), create_mechanical_chicken,
create_mechanical_extruder, create_mechanical_spawner, mechanical_cow,
mechanicals, createsifter, createultimine, createornithopterglider,
CreateDragonsPlus, sliceanddice, sophisticatedbackpackscreateintegration,
kubejs-create. Non-Create tech: laserio, enderrf, EnderStorage, melter,
mob_grinding_utils, buildinggadgets2, charginggadgets, titanium, CodeChickenLib.
- **Magic:** ars_nouveau, ars_creo (Ars×Create bridge).
- **Worldgen/exploration:** full YUNG's set (BetterCaves, CaveBiomes, Bridges,
BetterDungeons, BetterMineshafts, BetterStrongholds, BetterDesertTemples,
BetterJungleTemples, BetterOceanMonuments, BetterWitchHuts,
BetterNetherFortresses, BetterEndIsland), Moog's (Voyager, End, Nether,
MissingVillages, moogs_structures), TerraBlender, ExplorersCompass,
world_dimension_nexus, HangGlider, fishingoverhaul.
- **Perf:** sodium, sodium-extra, iris, immediatelyfast, entityculling, ferritecore, - **Perf:** sodium, sodium-extra, iris, immediatelyfast, entityculling, ferritecore,
lithium, spark. lithium, modernfix, spark.
- **QoL:** JEI, Jade(+addons), JER, JEProfessions, appleskin, MouseTweaks, Controlling, - **QoL:** JEI, Jade(+addons), JER, JEProfessions, appleskin, MouseTweaks, Controlling,
Searchables, inventoryessentials, polymorph, rightclickharvest, simplemagnets, Searchables, inventoryessentials, polymorph, rightclickharvest, simplemagnets,
fastleafdecay, torchmaster, trashcans, corpse(+curioscompat), lootr, comforts, fastleafdecay, torchmaster, trashcans, corpse(+curioscompat), lootr, comforts,
BetterF3, tagtooltips, cardboardboxes. BetterF3, tagtooltips, cardboardboxes, EasyAnvils, EasyMagic, easydisenchanting,
easy-villagers, easy-piglins, EasierSleeping, solcarrot, Neat, Ping-Wheel,
moreoverlays, ToastControl, ExtremeSoundMuffler, colorfulhearts, UsefulSlime.
- **Storage:** sophisticatedbackpacks(+core), functionalstorage(+more). - **Storage:** sophisticatedbackpacks(+core), functionalstorage(+more).
- **Farm/decor/build:** FarmersDelight, FramedBlocks, handcrafted. - **Farm/decor/build:** FarmersDelight, handcrafted, rechiseled, amendments,
immersive_paintings, etched, emotecraft.
- **FTB utils:** chunks, essentials, library, teams, ultimine. - **FTB utils:** chunks, essentials, library, teams, ultimine.
- **Voice:** Simple Voice Chat (`voicechat`). - **Voice:** Simple Voice Chat (`voicechat`).
- **Scripting/guide:** KubeJS(+rhino, +delight), ponderjs, Patchouli. - **Scripting/guide:** KubeJS(+rhino, +delight, +create), lootjs, toolsjs, ponderjs,
- **Libs:** architectury, balm, cloth-config, configured, curios, resourcefullib, Patchouli.
supermartijn642 core/config, jamlib, baguettelib, titanium, utilitarian, YungsApi, - **Server-custom:** `ulicraftmod` (this group's own mod).
Necronomicon, yacl. - **Libs:** architectury, balm, cloth-config, configured, curios, caelus, geckolib,
kotlinforforge, moonlight, Placebo, PuzzlesLib, resourcefullib, CreativeCore,
supermartijn642 core/config, jamlib, baguettelib, txnilib, fzzy_config,
titanium, utilitarian, YungsApi, Necronomicon, yacl, konkrete.
## Gaps vs kitchen-sink (tech + magic + exploration + QoL) ## Remaining gaps
- [ ] **Tech — empty.** Candidates: Create, Mekanism, Immersive Engineering, Applied - [ ] **Map — none.** Candidate: Xaero's Minimap + Worldmap. (`Ping-Wheel` and
Energistics 2, Thermal Series, Powah. (Pick a subset; Create is the anchor.) `ftb-chunks`' claim map do not replace a minimap.)
- [ ] **Magic — empty.** Candidates: Ars Nouveau, Botania, Iron's Spells 'n Spellbooks, - [ ] **Server-side perf (optional):** C2ME. (`modernfix` is in.)
Eidolon. - [ ] Optional second magic pillar (Botania / Iron's Spells) — Ars alone covers the
- [ ] **Worldgen — thin** (only `YungsApi` + `ExplorersCompass`). Candidates: Terralith, vibe; only add if the pack budget allows.
Tectonic, YUNG's structure set (Better Dungeons/Mineshafts/Strongholds…),
Repurposed Structures, William Wythers' Overhauled Overworld.
- [ ] **Map — none.** Candidate: Xaero's Minimap + Worldmap.
- [ ] **Server-side perf (optional):** ModernFix, C2ME.
## Orphaned dependencies (cleanup) ## Orphaned dependencies
- [ ] **`ponderjs`** present but **no Create** — ponder is Create's. Add Create or drop. - [x] **`ponderjs`** — resolved, Create is in the pack.
- [ ] **`Necronomicon`** present but **no Iron's Spells** — it's Iron's Spells' lib. - [ ] **`Necronomicon`** present but **no Iron's Spells** — it's Iron's Spells' lib.
Add Iron's Spells or drop. Add Iron's Spells or drop.
## Curation tradeoff
Adding the tech+magic+worldgen anchors pushes 76 → 100+ jars, near the stated
50–100 ceiling, and raises the RAM/stability bar (`INIT_MEMORY 4G`/`MAX_MEMORY 10G`).
Curate deliberately; verify each candidate's NeoForge **1.21.1** build + the
`21.1.233` loader pin at add-time.
## Execution (external repo, when ready) ## Execution (external repo, when ready)
1. Add jars in the distribution repo (Nebula `forgemods/required/`), keep the NeoForge 1. Add jars in the distribution repo (Nebula `forgemods/required/`), keep the NeoForge
+3 -1
View File
@@ -158,7 +158,7 @@ offline) reads `$DISTRIBUTION_WEB_ROOT/servers/ulicraft-1.21.1/forgemods/require
```json ```json
{ {
"schemaVersion": 1, "schemaVersion": 1,
"count": 76, "count": 164,
"mods": [ "mods": [
{ "id": "jei", "name": "Just Enough Items (JEI)", "version": "19.27.0.340", { "id": "jei", "name": "Just Enough Items (JEI)", "version": "19.27.0.340",
"authors": "mezz", "description": "…", "logo": "/mod-logos/jei.png" } "authors": "mezz", "description": "…", "logo": "/mod-logos/jei.png" }
@@ -168,6 +168,8 @@ offline) reads `$DISTRIBUTION_WEB_ROOT/servers/ulicraft-1.21.1/forgemods/require
`ModList.astro` (new) imports `mods.json` → flat grid (logo + name + version). The `ModList.astro` (new) imports `mods.json` → flat grid (logo + name + version). The
homepage stat tile (`site.ts` `stats[0].key "50+"`) is fed from `mods.json.count`. homepage stat tile (`site.ts` `stats[0].key "50+"`) is fed from `mods.json.count`.
(The hardcoded `"50+"` fallback is stale — the pack is 164 jars as of 2026-07-20;
bump it to `"150+"` when wiring the tile.)
Tasks: Tasks:
- [ ] `tooling/build-modlist.py` (jar parse + logo extract + `mods.json`). - [ ] `tooling/build-modlist.py` (jar parse + logo extract + `mods.json`).
+1
View File
@@ -14,6 +14,7 @@
| `status.${BASE_DOMAIN}` | `50-status.caddy` | `uptime-kuma:3001` | | `status.${BASE_DOMAIN}` | `50-status.caddy` | `uptime-kuma:3001` |
| `files.${BASE_DOMAIN}` | `60-files.caddy` | `filestash:8334` | | `files.${BASE_DOMAIN}` | `60-files.caddy` | `filestash:8334` |
| `distribution.${BASE_DOMAIN}` | `30-distribution.caddy` | static `/srv/distribution` (other repo) | | `distribution.${BASE_DOMAIN}` | `30-distribution.caddy` | static `/srv/distribution` (other repo) |
| `generators.${BASE_DOMAIN}` | `70-generators.caddy` | `generators:80` (offline misode fork + data mirror) |
--- ---
+168
View File
@@ -0,0 +1,168 @@
# 21 — Generators (`generators.${BASE_DOMAIN}`)
> Minecraft data pack generators — a pinned, offline-only fork of
> [misode.github.io](https://github.com/misode/misode.github.io), served with
> its own data mirror so it works with **zero internet** at the LAN party.
## Why self-host at all
Upstream is public, free and CDN-hosted, so a plain mirror would buy nothing.
Three things justify this:
1. **Offline.** At the party the machine is present but the network is not.
2. **Version pin.** The generators can never drift ahead of a 1.21.1 server.
3. **Modded schemas.** `public/mcdoc/` is the extension point for mods (see
*C-lite / C-full* below).
## The one fact that drives every decision
**The app is 100% client-side.** Every data fetch comes from the *player's
browser*, not from cochi. So:
- "cochi has no WAN at the party" protects nothing — a leftover upstream URL
just fails in the browser, at the venue, with no internet to debug it.
- Blocking the container's egress proves nothing either.
- The **only** things that make it offline-capable are (a) which origins the
bundle refers to, and (b) the mirror we serve them from.
That is why the CSP is enforcement, not decoration — see *Verification*.
## Architecture
```
host nginx (TLS, LE cert for generators.) ─► caddy ─► generators:80
│ (caddy in-container)
caddy/conf.d/70-generators.caddy ── sets Content-Security-Policy
│
generators container: /srv/app ← built Preact/Vite bundle (from the fork)
/srv/data ← ~109M mirror, bind-mounted :ro
```
One origin serves both the app and its data. No CORS, no second cert, no second
DNS name.
| Piece | Where |
|---|---|
| Fork | `git.fosil.eu/oier/ulicraft-generators`, branch `ulicraft`, `GENERATORS_REPO` in `.env` |
| Dockerfile + Caddyfile | **in the fork** (it is unpublished upstream, so unlike `nmsr` it cannot clone a pinned ref) |
| Mirror sync | `tooling/sync-generators-data.sh` → `GENERATORS_DATA_DIR` (default `./generators-data`, gitignored) |
| Stack vhost | `caddy/conf.d/70-generators.caddy` |
| TLS vhost | `nginx/ulicraft-caddy.conf.tmpl` |
## The mirror (~109M)
mcmeta publishes per-version snapshots as **tags** (~4000 of them), not branches.
| Source | Ref | Size |
|---|---|---|
| `misode/mcmeta` | tag `1.21.1-summary` | 31M |
| `misode/mcmeta` | tag `1.21.1-data` | 34M |
| `misode/mcmeta` | tag `1.21.1-assets-tiny` → served **as** `1.21.1-assets` | 36M |
| `misode/mcmeta` | tag `1.21.1-atlas` | 3.5M |
| `misode/mcmeta` | branch `summary`, `versions/data.min.json` only | 172K |
| `SpyglassMC/vanilla-mcdoc` | branch `generated`, `symbols.json` | 2.4M |
Three non-obvious points:
- **`assets-tiny` is assets minus the `.ogg` sounds** (hundreds of MB). It is
served at the `-assets` path because that is what the app requests. Sound
*playback* would 404, which is why the fork deletes the sounds generator.
- **`summary/versions/data.min.json` comes from the branch head, not a tag**
(`DataFetcher.ts:205`), and it runs during Spyglass init. Miss it and the app
dead-ends offline while looking perfectly healthy online.
- The **directory layout mirrors upstream's URL shape** (`/mcmeta/<ref>-<type>/…`).
That is what keeps the fork patch to a couple of constants.
The sync script verifies a **sentinel file per source** rather than trusting a
clean `git clone` exit — a renamed upstream path would otherwise "sync
successfully" and die in the browser at the party.
## Fork diff (pinned at upstream `2bfe5522`)
| Change | File |
|---|---|
| `mcmetaUrl` → `/mcmeta`, `vanillaMcdocUrl` → `/vanilla-mcdoc` | `services/DataFetcher.ts` |
| Removed jsdelivr fallback | `services/DataFetcher.ts` |
| Removed hardcoded immersive_weathering raw.githubusercontent URL | `services/DataFetcher.ts` |
| Version locked to `1.21` | `services/Versions.ts` (`MIRRORED_VERSION`) |
| Switcher filters the **view**, not the data | `components/VersionSwitcher.tsx` |
| Routes `/sounds`, `/changelog`, `/versions`, `/whats-new` deleted | `App.tsx`, `pages/Home.tsx` |
| Share disabled (write API, unmirrorable) | `services/Sharing.ts` |
| gtag → no-op, EthicalAds/Giscus/contributors removed, footer + `SOURCE_REPO_URL` | `index.html`, `Home.tsx`, `Footer.tsx`, `Utils.ts` |
### Version lock — the trap
`checkVersion()` compares versions **by index** (`config.versions.findIndex`).
Deleting entries from `config.versions` / `VersionIds` makes a removed
`minVersion` resolve to `-1`, so `-1 <= version` passes and generators that do
**not** exist in 1.21 silently render. **Filter the view, never the data.**
### Upstream policy: pin and freeze
Do not chase `master`. Upstream ships 26.x while this server sits at 1.21.1, and
every merge risks reintroducing an external origin. Merges are a rare,
deliberate chore — and the CSP is what catches a regression.
## C-lite vs C-full (modded generators)
**Currently C-lite.** `public/mcdoc/create.mcdoc` and `neoforge.mcdoc` ship
upstream and cover the pack's two biggest datapack surfaces. `.mcdoc` files are
fetched **same-origin** from `/mcdoc/<dep>.mcdoc`, so adding a mod = drop a file
in `public/mcdoc/` + add entries to `src/config.json`.
**C-full** (autocomplete for the pack's own items/blocks) additionally needs:
- a registry dump from the live server — KubeJS is already in the pack and
already mounted, so a startup script can dump every registry;
- a patch to `fetchRegistries`, which currently **hardcodes the `minecraft:`
namespace** (`data[id].map(e => 'minecraft:' + e)`), so modded ids cannot
enter autocomplete from data alone;
- modded item icons would still be missing (the atlas is vanilla-only).
mcdoc is hand-written per mod (upstream's range: 19–1389 lines), so C-full needs
a **named shortlist**, never "the pack".
## Deploy
Prerequisites on cochi, in this order — **the first one can take the whole stack
down if skipped**:
1. **`GENERATORS_REPO` must be set in cochi's `.env`.** compose uses
`${GENERATORS_REPO:?…}`, and an unset required variable makes
`docker compose up` fail to parse the file — **every service, including
Minecraft**, not just this one.
2. Clone the fork on cochi at that path (branch `ulicraft`).
3. `tooling/sync-generators-data.sh` on cochi (~109M, **needs WAN**).
4. `generators` in `LE_SUBDOMAINS` → `tooling/issue-letsencrypt.sh`.
5. **Only then** `tooling/render-nginx.sh --install` (nginx fails its config
test if the cert file does not exist).
6. DNS `A generators.${BASE_DOMAIN}` — and the **party DNS** too.
Then the normal deploy (`deploy` skill): git pull + `docker compose up -d --build`.
## Verification
**A 200 on `/` proves the server is up, not that the app works.** This is an SPA
whose failure mode is a healthy-looking shell over dead data.
- `curl /mcmeta/1.21.1-summary/registries/data.min.json` — proves the mirror is
actually mounted. An Uptime Kuma monitor should probe **this**, not `/`.
- **Offline acceptance test**: a real browser with every non-origin request
blocked, walking the generators you intend to use. `tooling/`-adjacent harness
lives in the scratchpad; the check is: zero off-origin attempts, zero
same-origin failures, zero console errors, schemas actually rendered.
- The **CSP** (`connect-src 'self'`) is the standing enforcement: an external
origin that sneaks back in breaks loudly in prod, on an ordinary day, instead
of silently at the party.
`script-src`/`style-src` are deliberately permissive for now — Spyglass/deepslate
wasm+eval needs were not characterised under deadline. Tightening them is a
post-party task; loosening `connect-src` is not.
## Party-day notes
- HSTS is `max-age=31536000; includeSubDomains` on every vhost, so **there is no
HTTP fallback and no cert-warning click-through**. The cert must be valid.
- LE certs bind **names, not IPs**, so a party DNS override pointing
`generators.${BASE_DOMAIN}` at the LAN IP validates fine — provided the cert
is unexpired and was issued while WAN existed.
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
# sync-generators-data.sh — mirror every remote data source the generators app
# reads, so generators.${BASE_DOMAIN} works with ZERO internet at party time.
#
# The generators app (a pinned fork of misode.github.io) is 100% CLIENT-SIDE:
# every data fetch comes from the player's browser, not from this host. So
# "cochi has no WAN" protects nothing on its own — the URLs themselves have to
# point at our own origin. The fork rewrites two constants:
#
# DataFetcher.ts mcmetaUrl = '/mcmeta'
# vanillaMcdocUrl = '/vanilla-mcdoc'
#
# and it builds paths as `${mcmetaUrl}/${ref}-${type}/...`, so the directory
# layout below is not arbitrary — it mirrors upstream's URL shape exactly,
# which is what keeps the fork diff to two lines instead of a rewrite.
#
# Serves at:
# /mcmeta/<ref>-{summary,data,atlas,assets}/... per-version snapshots
# /mcmeta/summary/versions/data.min.json dynamic version list
# /vanilla-mcdoc/generated/symbols.json THE schema source
#
# mcmeta publishes per-version snapshots as TAGS, not branches (~4000 of them),
# hence `--branch <tag> --depth 1` below.
#
# Needs WAN. Run deliberately, before the party — never at the venue.
# Output dir is gitignored; only this script is tracked.
#
# Cautious by design: halts on any error, verifies each expected file exists
# rather than trusting a clean exit, and refuses to leave a half-synced tree in
# place. Idempotent — re-runs replace each source atomically.
set -euo pipefail
cd "$(dirname "$0")/.." # repo root
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; }
command -v git >/dev/null || die "git required"
# The MC version the server runs. mcmeta tags are <ref>-<type>; the generators
# UI is locked to the config.json entry {id: '1.21', ref: '1.21.1'}, so this
# MUST stay in step with that ref and with NEOFORGE/VERSION in docker-compose.
MC_REF="${MC_REF:-1.21.1}"
OUT="${GENERATORS_DATA_DIR:-generators-data}"
MCMETA_REPO="https://github.com/misode/mcmeta.git"
MCDOC_REPO="https://github.com/SpyglassMC/vanilla-mcdoc.git"
# Each entry: <mcmeta tag>:<directory we serve it as>.
#
# assets-tiny is assets MINUS the ~hundreds of MB of .ogg sound files. We serve
# it AT the `-assets` path because that is what the app requests (lang files,
# textures, models all live there). Consequence: sound PLAYBACK 404s, which is
# why the fork deletes the sounds generator from the UI. If you ever re-add it,
# swap this to the full `-assets` tag and re-measure the disk cost first.
SOURCES=(
"${MC_REF}-summary:${MC_REF}-summary"
"${MC_REF}-data:${MC_REF}-data"
"${MC_REF}-atlas:${MC_REF}-atlas"
"${MC_REF}-assets-tiny:${MC_REF}-assets"
)
# One file per source that MUST exist afterwards. A clean `git clone` exit does
# not prove the tag held what the app needs — a renamed path upstream would
# sync "successfully" and then die in the browser, offline, at the party.
declare -A SENTINEL=(
["${MC_REF}-summary"]="registries/data.min.json"
["${MC_REF}-data"]="data/minecraft/worldgen/biome/plains.json"
["${MC_REF}-atlas"]="all/atlas.png"
["${MC_REF}-assets"]="assets/minecraft/lang/en_us.json"
)
# Shallow-clone one ref into a temp dir, verify its sentinel, then swap it in.
# Never deletes the live copy until the new one has been verified.
sync_ref() {
local repo="$1" ref="$2" dest="$3" sentinel="$4"
local tmp="${dest}.tmp.$$"
rm -rf "$tmp"
git clone -q --depth 1 --branch "$ref" "$repo" "$tmp" \
|| die "clone failed: $repo @ $ref"
rm -rf "$tmp/.git"
[ -f "$tmp/$sentinel" ] || {
rm -rf "$tmp"
die "$ref synced but '$sentinel' is missing — upstream layout changed.
Refusing to install a tree the app cannot read. Investigate before retrying."
}
rm -rf "$dest"
mv "$tmp" "$dest"
printf ' %s -> %s (%s)\n' "$ref" "$dest" "$(du -sh "$dest" | cut -f1)"
}
log "mirroring generators data for MC ${MC_REF} -> ${OUT}/"
mkdir -p "$OUT/mcmeta"
for entry in "${SOURCES[@]}"; do
tag="${entry%%:*}"
dir="${entry##*:}"
log "mcmeta tag ${tag}"
sync_ref "$MCMETA_REPO" "$tag" "$OUT/mcmeta/$dir" "${SENTINEL[$dir]}"
done
# The dynamic version list. NOT a version tag — DataFetcher.ts:205 fetches it
# from the live `summary` BRANCH head, and it runs during Spyglass init. Miss
# this one and the app dead-ends offline while looking perfectly healthy online.
# It is a single file, so fetch it directly rather than cloning the whole
# branch (which is another ~31M of the LATEST version's summary, all unused).
log "mcmeta branch summary (versions/data.min.json only)"
command -v curl >/dev/null || die "curl required"
mkdir -p "$OUT/mcmeta/summary/versions"
curl -fsSL "https://raw.githubusercontent.com/misode/mcmeta/summary/versions/data.min.json" \
-o "$OUT/mcmeta/summary/versions/data.min.json.tmp" \
|| die "failed to fetch summary/versions/data.min.json"
# Verify it parses as JSON before installing it — a captive-portal HTML error
# page would download with a clean exit code and break Spyglass init offline.
python3 -c "import json,sys; json.load(open(sys.argv[1]))" \
"$OUT/mcmeta/summary/versions/data.min.json.tmp" \
|| die "summary/versions/data.min.json is not valid JSON — refusing to install it"
mv "$OUT/mcmeta/summary/versions/data.min.json.tmp" \
"$OUT/mcmeta/summary/versions/data.min.json"
printf ' summary/versions/data.min.json (%s)\n' \
"$(du -h "$OUT/mcmeta/summary/versions/data.min.json" | cut -f1)"
# The schema source. Without it there are no schemas, no validation, no
# generator — this is the single most load-bearing file in the mirror.
#
# The app requests /vanilla-mcdoc/generated/symbols.json. In the upstream URL
# that decomposes as <repo>/<ref>/<path> = vanilla-mcdoc / branch `generated` /
# `symbols.json` — `generated` is a BRANCH NAME, not a directory, and
# symbols.json sits at the branch root. So the clone lands in a directory named
# after the branch to reproduce the same URL shape locally.
log "vanilla-mcdoc branch generated"
sync_ref "$MCDOC_REPO" "generated" "$OUT/vanilla-mcdoc/generated" "symbols.json"
log "done — $(du -sh "$OUT" | cut -f1) in ${OUT}/"
echo " mount it read-only into the generators container (docker-compose.yml)"
echo " verify offline BEFORE the party: block all non-origin requests in"
echo " DevTools and walk the generators you actually intend to use."