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.
This commit is contained in:
1 parent
7164371d34
commit
b6b02398da
8 files changed
+422
-2
No files matched your search
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