Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b02398da | ||
|
|
7164371d34 |
No files matched your search
+16
-1
@@ -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
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Executable
+141
@@ -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."
|
||||||
Reference in new issue
Block a user