Compare commits
2
Commits
0bf42d5797
...
b6b02398da
| 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.
|
||||
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:
|
||||
# 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.
|
||||
LE_EMAIL=you@example.com
|
||||
# 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_CK can be blank on first run — acme.sh prints an auth URL, then paste it.
|
||||
OVH_END_POINT=ovh-eu
|
||||
|
||||
@@ -17,6 +17,11 @@ server-mods/
|
||||
# vendored binaries
|
||||
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
|
||||
www/
|
||||
landing/node_modules/
|
||||
|
||||
@@ -12,7 +12,9 @@ with `plan/` or the code, those win.
|
||||
|
||||
- **Players**: 4–10 friends
|
||||
- **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)
|
||||
- **Auth/skins**: Drasl (Yggdrasil-compatible), **password login** — no OIDC
|
||||
- **DNS**: handled OUTSIDE this repo; point `${BASE_DOMAIN}` + subdomains at the host
|
||||
@@ -131,7 +133,9 @@ while every other check passed.
|
||||
|
||||
### Memory sizing
|
||||
~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)
|
||||
|
||||
@@ -157,13 +161,12 @@ mirrored at apex `/launcher/`. Per guest:
|
||||
## Open / Pending Work
|
||||
|
||||
### Roadmap — mod migration follow-ups (post packwiz→04-mods)
|
||||
- [ ] **Curate `mods-sides.json` cosmetic-client mods.** tomllib auto-flagged 11
|
||||
hard client-only mods; the other 65 default `both`, including client-cosmetic
|
||||
ones (BetterF3, entityculling, MouseTweaks, ponderjs, fancymenu, drippy, melody,
|
||||
welcomescreen, Searchables, JustEnoughResources, TravelersTitles, yeetus…) that
|
||||
load harmlessly on the server but waste RAM. Hand-edit them to `client` so
|
||||
`sync-server-mods.sh` drops them from `./server-mods`. (Also review `appleskin`,
|
||||
flagged `client` — flip to `both` if server-side food data wanted.)
|
||||
- [ ] **Curate `mods-sides.json` cosmetic-client mods.** Pack is now **164 jars**
|
||||
(2026-07-20): **31 `client` / 133 `both`**. The `both` set still contains
|
||||
client-cosmetic mods that load harmlessly on the server but waste RAM — hand-edit
|
||||
them to `client` so `sync-server-mods.sh` drops them from `./server-mods`.
|
||||
(Also review `appleskin`, flagged `client` — flip to `both` if server-side food
|
||||
data wanted.)
|
||||
### Landing rework + mod list (PLANNED — see `plan/18-landing-rework.md`)
|
||||
Full design settled across WS1–WS6; all tasks unchecked, ready to execute.
|
||||
- [ ] **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).
|
||||
|
||||
- [ ] **WS6 — Mod shortlist + list component (`plan/17-mod-shortlist.md`).**
|
||||
Light gap doc: current 76-jar pack is vanilla+ QoL/perf; **empty** on tech &
|
||||
magic, thin worldgen, no map; orphan deps `ponderjs`/`Necronomicon`. Landing
|
||||
mod-list component = 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.).
|
||||
Gap doc part **DONE**: pack filled out to **164 jars** (2026-07-20) — Create
|
||||
ecosystem (tech), Ars Nouveau + ars_creo (magic), full YUNG's + Moog's +
|
||||
TerraBlender (worldgen); `ponderjs` orphan resolved by Create. Still open: **no
|
||||
map mod** (Xaero's), `Necronomicon` orphan (no Iron's Spells), and 164 > the
|
||||
stated 50–100 ceiling. Still to build: landing mod-list component =
|
||||
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
|
||||
htpasswd login, writable, rendered `:ro` config. See `plan/20-files.md`.
|
||||
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/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/70-generators.caddy:/etc/caddy/conf.d/70-generators.caddy:ro
|
||||
- ./www:/srv/www:ro
|
||||
- ./launcher:/srv/launcher:ro
|
||||
# Static site from ANOTHER repo (absolute host path in .env).
|
||||
@@ -170,6 +171,36 @@ services:
|
||||
aliases:
|
||||
- "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
|
||||
# player profiles from Drasl over mcnet. caddy proxies avatar. -> nmsr:8080.
|
||||
nmsr:
|
||||
|
||||
@@ -34,7 +34,7 @@ limit_req_zone $binary_remote_addr zone=files_admin:10m rate=6r/m;
|
||||
server {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
+6
-4
@@ -6,7 +6,8 @@
|
||||
|
||||
## 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.
|
||||
- Manual packwiz curation (`.tmpl` render → `refresh` → re-hash) is the flagged
|
||||
pain point, and a single missing file (`custom/.gitkeep`) crashlooped the
|
||||
@@ -20,7 +21,7 @@
|
||||
```
|
||||
ulicraft-distribution/dist (another repo, = $DISTRIBUTION_WEB_ROOT)
|
||||
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
|
||||
│ │
|
||||
│ 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,
|
||||
CustomSkinLoader, entity_model_features, entity_texture_features, tagtooltips).
|
||||
|
||||
**Current state: 24 `client` / 52 `both`.** The 52 may still hide a client-only
|
||||
mod that loaded without crashing; if a future restart trips another
|
||||
**State at first cutover: 24 `client` / 52 `both`. Current (2026-07-20, post
|
||||
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
|
||||
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
|
||||
|
||||
+48
-33
@@ -1,14 +1,19 @@
|
||||
# Mod shortlist + landing mod-list component (WS6)
|
||||
|
||||
> **Status: PLANNED — not started.** Two separable deliverables: (1) a curated
|
||||
> kitchen-sink gap doc (this 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`).
|
||||
> **Status: gap doc DONE (pack filled out 2026-07-20), landing component still
|
||||
> PLANNED.** Two separable deliverables: (1) a curated kitchen-sink gap doc (this
|
||||
> 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
|
||||
|
||||
- Current pack (76 jars, `mods-sides.json`) is a polished **vanilla+ QoL/perf** base,
|
||||
**not yet kitchen-sink**. Strong perf + QoL + storage + FTB + voice; **empty** on
|
||||
tech and magic, **thin** on worldgen, **no** map mod.
|
||||
- The pack is now **kitchen-sink** — **164 jars** (`mods-sides.json`: 133 `both`,
|
||||
31 `client`), up from the 76-jar vanilla+ base this doc originally described.
|
||||
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/
|
||||
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
|
||||
@@ -17,48 +22,58 @@
|
||||
from the jars — a different dataset from this shortlist. Flat alphabetical, pretty
|
||||
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,
|
||||
lithium, spark.
|
||||
lithium, modernfix, spark.
|
||||
- **QoL:** JEI, Jade(+addons), JER, JEProfessions, appleskin, MouseTweaks, Controlling,
|
||||
Searchables, inventoryessentials, polymorph, rightclickharvest, simplemagnets,
|
||||
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).
|
||||
- **Farm/decor/build:** FarmersDelight, FramedBlocks, handcrafted.
|
||||
- **Farm/decor/build:** FarmersDelight, handcrafted, rechiseled, amendments,
|
||||
immersive_paintings, etched, emotecraft.
|
||||
- **FTB utils:** chunks, essentials, library, teams, ultimine.
|
||||
- **Voice:** Simple Voice Chat (`voicechat`).
|
||||
- **Scripting/guide:** KubeJS(+rhino, +delight), ponderjs, Patchouli.
|
||||
- **Libs:** architectury, balm, cloth-config, configured, curios, resourcefullib,
|
||||
supermartijn642 core/config, jamlib, baguettelib, titanium, utilitarian, YungsApi,
|
||||
Necronomicon, yacl.
|
||||
- **Scripting/guide:** KubeJS(+rhino, +delight, +create), lootjs, toolsjs, ponderjs,
|
||||
Patchouli.
|
||||
- **Server-custom:** `ulicraftmod` (this group's own mod).
|
||||
- **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
|
||||
Energistics 2, Thermal Series, Powah. (Pick a subset; Create is the anchor.)
|
||||
- [ ] **Magic — empty.** Candidates: Ars Nouveau, Botania, Iron's Spells 'n Spellbooks,
|
||||
Eidolon.
|
||||
- [ ] **Worldgen — thin** (only `YungsApi` + `ExplorersCompass`). Candidates: Terralith,
|
||||
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.
|
||||
- [ ] **Map — none.** Candidate: Xaero's Minimap + Worldmap. (`Ping-Wheel` and
|
||||
`ftb-chunks`' claim map do not replace a minimap.)
|
||||
- [ ] **Server-side perf (optional):** C2ME. (`modernfix` is in.)
|
||||
- [ ] Optional second magic pillar (Botania / Iron's Spells) — Ars alone covers the
|
||||
vibe; only add if the pack budget allows.
|
||||
|
||||
## 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.
|
||||
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)
|
||||
|
||||
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
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"count": 76,
|
||||
"count": 164,
|
||||
"mods": [
|
||||
{ "id": "jei", "name": "Just Enough Items (JEI)", "version": "19.27.0.340",
|
||||
"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
|
||||
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:
|
||||
- [ ] `tooling/build-modlist.py` (jar parse + logo extract + `mods.json`).
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
| `status.${BASE_DOMAIN}` | `50-status.caddy` | `uptime-kuma:3001` |
|
||||
| `files.${BASE_DOMAIN}` | `60-files.caddy` | `filestash:8334` |
|
||||
| `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