565 lines
25 KiB
Bash
Executable File
565 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Set up packwiz and initialise a NeoForge modpack, sourcing every mod from an
|
|
# HTTP mirror of a local mods directory rather than Modrinth/CurseForge.
|
|
# Everything lives on the ZFS share at /minecraft.
|
|
#
|
|
# ./packwiz-setup.sh -n "My Pack" -a themiro -u https://games.abdelbaki.eu/mods
|
|
#
|
|
# ./packwiz-setup.sh -n "My Pack" -a themiro -m 1.21.1 -f 21.1.72 \
|
|
# -M /minecraft/mods -u http://192.168.200.41:8080/mods
|
|
#
|
|
# The mirror must already be serving those jars when this runs — packwiz
|
|
# fetches each URL to compute its hash. Run as your normal user.
|
|
|
|
set -euo pipefail
|
|
|
|
NAME=""
|
|
AUTHOR=""
|
|
MCVER="1.21.1"
|
|
NFVER="" # empty = latest
|
|
MODSDIR=""
|
|
BASEURL=""
|
|
DIR=""
|
|
PACKVER="1.0.0"
|
|
SHARE="/minecraft" # ZFS dataset everything lives on
|
|
PACKROOT="" # defaults to $SHARE/packs
|
|
WEBROOT="" # defaults to $SHARE — docroot the site is served from
|
|
SITE_URL="" # defaults to the origin of $BASEURL
|
|
PACK_URL="" # defaults to $SITE_URL/packs/<slug>
|
|
SERVER_ADDR="" # defaults to the hostname of $SITE_URL
|
|
GUIDES=1 # write the player setup guides
|
|
|
|
die() { echo "error: $*" >&2; exit 1; }
|
|
info() { echo ">>> $*"; }
|
|
warn() { echo " warning: $*" >&2; }
|
|
usage() { sed -n '2,15p' "$0" | sed 's/^# \?//'; exit "${1:-0}"; }
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-n|--name) NAME="${2:-}"; shift 2 ;;
|
|
-a|--author) AUTHOR="${2:-}"; shift 2 ;;
|
|
-m|--mc) MCVER="${2:-}"; shift 2 ;;
|
|
-f|--neoforge) NFVER="${2:-}"; shift 2 ;;
|
|
-M|--mods-dir) MODSDIR="${2:-}"; shift 2 ;;
|
|
-u|--base-url) BASEURL="${2:-}"; shift 2 ;;
|
|
-v|--version) PACKVER="${2:-}"; shift 2 ;;
|
|
-d|--dir) DIR="${2:-}"; shift 2 ;;
|
|
-s|--share) SHARE="${2:-}"; shift 2 ;;
|
|
-r|--root) PACKROOT="${2:-}"; shift 2 ;;
|
|
-W|--webroot) WEBROOT="${2:-}"; shift 2 ;;
|
|
-U|--site-url) SITE_URL="${2:-}"; shift 2 ;;
|
|
-P|--pack-url) PACK_URL="${2:-}"; shift 2 ;;
|
|
-A|--server) SERVER_ADDR="${2:-}"; shift 2 ;;
|
|
--no-guides) GUIDES=0; shift ;;
|
|
-h|--help) usage 0 ;;
|
|
*) echo "unknown option: $1" >&2; usage 1 ;;
|
|
esac
|
|
done
|
|
|
|
[ "$(id -u)" -ne 0 ] || die "run this as your normal user, not root"
|
|
[ -n "$NAME" ] || die "missing -n <pack name>"
|
|
[ -n "$AUTHOR" ] || die "missing -a <author>"
|
|
|
|
# ----------------------------------------------------------------- share ----
|
|
|
|
# If the ZFS dataset isn't mounted, /minecraft is just an empty directory on
|
|
# root — writing a pack there would silently land on the 15G root filesystem
|
|
# instead of the pool. Refuse rather than fill up the wrong disk.
|
|
[ -d "$SHARE" ] || die "$SHARE does not exist"
|
|
if ! mountpoint -q "$SHARE"; then
|
|
die "$SHARE exists but nothing is mounted there — the ZFS dataset is not mounted.
|
|
Check 'zpool status' and 'zfs mount -a' before running this."
|
|
fi
|
|
|
|
fstype="$(findmnt -no FSTYPE "$SHARE" 2>/dev/null || true)"
|
|
[ "$fstype" = "zfs" ] || warn "$SHARE is $fstype, not zfs — continuing anyway"
|
|
|
|
[ -n "$PACKROOT" ] || PACKROOT="$SHARE/packs"
|
|
|
|
slug="$(printf '%s' "$NAME" | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-')"
|
|
[ -n "$slug" ] || die "pack name '$NAME' has no usable characters for a directory name"
|
|
[ -n "$DIR" ] || DIR="$PACKROOT/$slug"
|
|
|
|
# Fail on permissions now, with the fix, rather than midway through init.
|
|
mkdir -p "$PACKROOT" 2>/dev/null || true
|
|
[ -w "$PACKROOT" ] || die "cannot write to $PACKROOT
|
|
fix with: sudo install -d -o $USER -g $USER $PACKROOT"
|
|
|
|
# ----------------------------------------------------------------- mirror ---
|
|
|
|
# The mods dir defaults to the share; -u is what turns mirroring on.
|
|
[ -n "$BASEURL" ] && [ -z "$MODSDIR" ] && MODSDIR="$SHARE/mods"
|
|
if [ -n "$MODSDIR$BASEURL" ]; then
|
|
[ -n "$BASEURL" ] || die "-M given without -u <http base url>"
|
|
[ -d "$MODSDIR" ] || die "mods dir does not exist: $MODSDIR"
|
|
fi
|
|
BASEURL="${BASEURL%/}"
|
|
|
|
# ------------------------------------------------------------------ site ----
|
|
|
|
# Everything the guides need is derivable from the mirror URL, so one -u is
|
|
# usually enough; each piece can still be overridden individually.
|
|
[ -n "$SITE_URL" ] || [ -z "$BASEURL" ] || SITE_URL="$(awk -F/ '{print $1"//"$3}' <<<"$BASEURL")"
|
|
SITE_URL="${SITE_URL%/}"
|
|
|
|
if [ -n "$SITE_URL" ]; then
|
|
[ -n "$PACK_URL" ] || PACK_URL="$SITE_URL/packs/$slug"
|
|
[ -n "$SERVER_ADDR" ] || SERVER_ADDR="$(sed -e 's#^[a-z]*://##' -e 's#:.*##' <<<"$SITE_URL")"
|
|
fi
|
|
PACK_URL="${PACK_URL%/}"
|
|
|
|
[ -n "$WEBROOT" ] || WEBROOT="$SHARE"
|
|
|
|
if (( GUIDES )); then
|
|
if [ -z "$SITE_URL" ]; then
|
|
warn "no -u or -U given, so the guide URLs can't be derived — skipping guides"
|
|
GUIDES=0
|
|
elif [ ! -d "$WEBROOT" ]; then
|
|
die "webroot does not exist: $WEBROOT (set it with -W, or pass --no-guides)"
|
|
elif [ ! -w "$WEBROOT" ]; then
|
|
die "cannot write guides to $WEBROOT
|
|
fix with: sudo install -d -o $USER -g $USER $WEBROOT
|
|
or pass --no-guides"
|
|
fi
|
|
fi
|
|
|
|
# ------------------------------------------------------------------ deps ----
|
|
|
|
if ! command -v go >/dev/null; then
|
|
info "installing Go"
|
|
sudo dnf install -y golang >/dev/null || die "could not install golang"
|
|
fi
|
|
|
|
GOBIN="$(go env GOPATH)/bin"
|
|
|
|
if ! command -v packwiz >/dev/null && [ ! -x "$GOBIN/packwiz" ]; then
|
|
info "building packwiz (pulls from the Go module proxy, takes a minute)"
|
|
go install github.com/packwiz/packwiz@latest \
|
|
|| die "go install failed — check network access to proxy.golang.org"
|
|
fi
|
|
|
|
PACKWIZ="$(command -v packwiz || echo "$GOBIN/packwiz")"
|
|
[ -x "$PACKWIZ" ] || die "packwiz not found after install; expected $GOBIN/packwiz"
|
|
info "packwiz: $PACKWIZ"
|
|
|
|
if ! command -v packwiz >/dev/null; then
|
|
for rc in ~/.zshrc ~/.bashrc; do
|
|
[ -f "$rc" ] || continue
|
|
grep -q '/go/bin\|GOPATH/bin' "$rc" && continue
|
|
printf '\nexport PATH="$PATH:%s"\n' "$GOBIN" >> "$rc"
|
|
info "added $GOBIN to PATH in $rc"
|
|
done
|
|
export PATH="$PATH:$GOBIN"
|
|
fi
|
|
|
|
# ------------------------------------------------------------------ init ----
|
|
|
|
[ -e "$DIR/pack.toml" ] && die "$DIR/pack.toml already exists — refusing to overwrite
|
|
(run 'packwiz init --reinit' there if that's what you want)"
|
|
|
|
mkdir -p "$DIR"
|
|
cd "$DIR"
|
|
|
|
# The NeoForge flag names have moved between packwiz builds, so ask this
|
|
# binary what it supports instead of assuming.
|
|
help="$("$PACKWIZ" init --help 2>&1 || true)"
|
|
grep -q -- '--modloader' <<<"$help" || die "unexpected packwiz init interface; run '$PACKWIZ init' interactively"
|
|
|
|
args=(--name "$NAME" --author "$AUTHOR" --version "$PACKVER" --mc-version "$MCVER" --modloader neoforge)
|
|
|
|
if [ -n "$NFVER" ]; then
|
|
grep -q -- '--neoforge-version' <<<"$help" \
|
|
|| die "this packwiz build has no --neoforge-version flag; run '$PACKWIZ init' interactively"
|
|
args+=(--neoforge-version "$NFVER")
|
|
elif grep -q -- '--neoforge-latest' <<<"$help"; then
|
|
args+=(--neoforge-latest)
|
|
fi
|
|
|
|
info "initialising NeoForge pack for MC $MCVER in $(pwd)"
|
|
"$PACKWIZ" init "${args[@]}"
|
|
[ -f pack.toml ] || die "packwiz init did not produce a pack.toml"
|
|
|
|
# ---------------------------------------------------------------- mirror ----
|
|
|
|
if [ -n "$MODSDIR" ]; then
|
|
grep -q 'url' <<<"$("$PACKWIZ" --help 2>&1 || true)" \
|
|
|| die "this packwiz build has no 'url' subcommand — cannot add mods by URL"
|
|
|
|
# packwiz downloads each URL to hash it, so a mirror that isn't up yet
|
|
# produces a pile of confusing per-mod failures. Check once, up front.
|
|
info "checking mirror at $BASEURL"
|
|
probe="$(curl -so /dev/null -w '%{http_code}' --max-time 10 "$BASEURL/" || echo 000)"
|
|
case "$probe" in
|
|
2*|3*|40[34]) ;; # a listing may legitimately be forbidden
|
|
000) die "cannot reach $BASEURL — start the mirror before running this" ;;
|
|
*) warn "$BASEURL/ returned HTTP $probe; continuing, individual mods may fail" ;;
|
|
esac
|
|
|
|
shopt -s nullglob
|
|
jars=("$MODSDIR"/*.jar)
|
|
shopt -u nullglob
|
|
[ "${#jars[@]}" -gt 0 ] || die "no .jar files found in $MODSDIR"
|
|
|
|
info "adding ${#jars[@]} mods from the mirror"
|
|
failed=()
|
|
for jar in "${jars[@]}"; do
|
|
file="$(basename "$jar")"
|
|
|
|
# Mod name: filename without .jar and without a trailing version, since
|
|
# packwiz uses this as the .pw.toml basename and it should stay stable
|
|
# across version bumps.
|
|
modname="$(printf '%s' "${file%.jar}" | sed -E 's/-[0-9][0-9A-Za-z.+_-]*$//')"
|
|
[ -n "$modname" ] || modname="${file%.jar}"
|
|
|
|
# Percent-encode the filename for the URL; leave the base URL alone.
|
|
enc="$(printf '%s' "$file" | sed -e 's/%/%25/g' -e 's/ /%20/g' -e 's/+/%2B/g')"
|
|
|
|
if "$PACKWIZ" url add "$modname" "$BASEURL/$enc" >/dev/null 2>&1; then
|
|
printf ' + %s\n' "$modname"
|
|
else
|
|
printf ' ! %s (failed)\n' "$modname"
|
|
failed+=("$file")
|
|
fi
|
|
done
|
|
|
|
"$PACKWIZ" refresh
|
|
|
|
if [ "${#failed[@]}" -gt 0 ]; then
|
|
echo
|
|
warn "${#failed[@]} of ${#jars[@]} mods failed to add:"
|
|
printf ' %s\n' "${failed[@]}" >&2
|
|
warn "re-run those by hand to see the error, e.g."
|
|
warn " $PACKWIZ url add <name> $BASEURL/${failed[0]}"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- guides ----
|
|
|
|
# One page per OS at $WEBROOT/setup-<os>-packwiz.html, generated with the real
|
|
# pack URL and server address baked in so players never edit anything.
|
|
write_guide() {
|
|
local os="$1" out="$2"
|
|
local os_label install_intro install_cmd install_note java_note dl_intro dl_cmd dl_note
|
|
|
|
case "$os" in
|
|
windows)
|
|
os_label="Windows"
|
|
install_intro="From PowerShell:"
|
|
install_cmd="winget install --id PrismLauncher.PrismLauncher"
|
|
install_note="Or grab the installer from prismlauncher.org if you'd rather not use winget."
|
|
java_note="Prism can download and manage Java 21 for you — on first run it offers to, and you should let it."
|
|
dl_intro="Open PowerShell in that folder and run:"
|
|
dl_cmd="curl.exe -L -o packwiz-installer-bootstrap.jar https://github.com/packwiz/packwiz-installer-bootstrap/releases/latest/download/packwiz-installer-bootstrap.jar"
|
|
dl_note="Or open that URL in a browser and move the downloaded file in."
|
|
;;
|
|
linux)
|
|
os_label="Linux"
|
|
install_intro="Flatpak works on every distro and is the easiest to keep current:"
|
|
install_cmd="flatpak install flathub org.prismlauncher.PrismLauncher"
|
|
install_note="Fedora also has <code class=\"inline\">sudo dnf install prismlauncher</code>, and Arch ships it in extra."
|
|
java_note="The Flatpak bundles its own Java and ignores the system one — nothing to install. On a native package, Fedora: <code class=\"inline\">sudo dnf install java-21-openjdk</code>, Debian/Ubuntu: <code class=\"inline\">sudo apt install openjdk-21-jre</code>."
|
|
dl_intro="From a terminal in that folder:"
|
|
dl_cmd="curl -LO https://github.com/packwiz/packwiz-installer-bootstrap/releases/latest/download/packwiz-installer-bootstrap.jar"
|
|
dl_note="Flatpak instances live under <code class=\"inline\">~/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/</code>."
|
|
;;
|
|
*) die "unknown OS for guide: $os" ;;
|
|
esac
|
|
|
|
# Expanding heredoc: \$INST_JAVA and \$INST_MC_DIR are escaped so they reach
|
|
# the page literally — Prism substitutes those at launch, not this script.
|
|
cat > "$out" <<EOF
|
|
<title>$NAME — Setup on $os_label</title>
|
|
<style>
|
|
:root{--bg:#F4F5F8;--surface:#FFF;--sunk:#EAECF2;--border:#D8DCE6;--ink:#171A22;
|
|
--ink-soft:#4A5163;--muted:#6E7689;--accent:#3350D8;--accent-dim:#E6EAFB;
|
|
--warn:#B0652C;--warn-dim:#FBF0E4;--code-bg:#14161E;--code-ink:#DDE2F0;
|
|
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
|
--mono:ui-monospace,"SF Mono","JetBrains Mono","Cascadia Mono",Menlo,Consolas,monospace}
|
|
@media(prefers-color-scheme:dark){:root{--bg:#101219;--surface:#171A23;--sunk:#1E222D;
|
|
--border:#2B3040;--ink:#EDEFF5;--ink-soft:#B4BBCC;--muted:#838CA0;--accent:#7C92F5;
|
|
--accent-dim:#1C2340;--warn:#E0A063;--warn-dim:#2A2113;--code-bg:#0B0D14}}
|
|
:root[data-theme=dark]{--bg:#101219;--surface:#171A23;--sunk:#1E222D;--border:#2B3040;
|
|
--ink:#EDEFF5;--ink-soft:#B4BBCC;--muted:#838CA0;--accent:#7C92F5;--accent-dim:#1C2340;
|
|
--warn:#E0A063;--warn-dim:#2A2113;--code-bg:#0B0D14}
|
|
:root[data-theme=light]{--bg:#F4F5F8;--surface:#FFF;--sunk:#EAECF2;--border:#D8DCE6;
|
|
--ink:#171A22;--ink-soft:#4A5163;--muted:#6E7689;--accent:#3350D8;--accent-dim:#E6EAFB;
|
|
--warn:#B0652C;--warn-dim:#FBF0E4;--code-bg:#14161E}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);
|
|
font-size:16px;line-height:1.65;-webkit-font-smoothing:antialiased}
|
|
.wrap{max-width:46rem;margin:0 auto;padding:0 1.25rem 5rem}
|
|
.masthead{padding:3.5rem 0 2.5rem}
|
|
.eyebrow{font-family:var(--mono);font-size:.72rem;text-transform:uppercase;
|
|
letter-spacing:.14em;color:var(--muted);margin:0 0 .9rem}
|
|
h1{font-size:clamp(2rem,6vw,2.9rem);line-height:1.08;letter-spacing:-.025em;
|
|
font-weight:700;text-wrap:balance;margin:0 0 .85rem}
|
|
.lede{font-size:1.08rem;color:var(--ink-soft);margin:0 0 1.75rem;max-width:34rem}
|
|
.facts{display:flex;flex-wrap:wrap;gap:.5rem;padding:0;margin:0;list-style:none}
|
|
.facts li{display:flex;align-items:baseline;gap:.45rem;background:var(--surface);
|
|
border:1px solid var(--border);border-radius:2px;padding:.35rem .7rem;font-size:.85rem}
|
|
.facts b{font-family:var(--mono);font-size:.7rem;text-transform:uppercase;
|
|
letter-spacing:.1em;color:var(--muted);font-weight:500}
|
|
.facts span{font-family:var(--mono);font-size:.86rem}
|
|
.step{display:grid;grid-template-columns:2.4rem 1fr;gap:0 1rem;
|
|
padding-bottom:2.2rem;margin-bottom:2.2rem;border-bottom:1px solid var(--border)}
|
|
.num{font-family:var(--mono);font-size:.8rem;font-weight:600;color:var(--accent);
|
|
padding-top:.35rem;font-variant-numeric:tabular-nums}
|
|
.step h2{grid-column:2;font-size:1.3rem;line-height:1.25;letter-spacing:-.015em;
|
|
font-weight:650;margin:0 0 .6rem;text-wrap:balance}
|
|
.step .body{grid-column:2;display:flex;flex-direction:column;gap:.85rem}
|
|
.step p{margin:0}
|
|
.step ul{margin:0;padding-left:1.15rem;display:flex;flex-direction:column;gap:.35rem}
|
|
@media(max-width:34rem){.step{grid-template-columns:1fr;gap:0}
|
|
.step h2,.step .body{grid-column:1}.num{padding-top:0;margin-bottom:.2rem}}
|
|
.cmd{position:relative}
|
|
.cmd pre{margin:0;background:var(--code-bg);color:var(--code-ink);border-radius:3px;
|
|
padding:.85rem 3.2rem .85rem 1rem;overflow-x:auto;font-family:var(--mono);
|
|
font-size:.83rem;line-height:1.6}
|
|
.cmd code{font-family:inherit;white-space:pre}
|
|
.copy{position:absolute;top:.5rem;right:.5rem;font-family:var(--mono);font-size:.66rem;
|
|
text-transform:uppercase;letter-spacing:.08em;padding:.25rem .5rem;border:1px solid #333A4D;
|
|
border-radius:2px;background:transparent;color:#8E97AD;cursor:pointer}
|
|
.copy:hover{color:var(--code-ink);border-color:#59627A}
|
|
.copy:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
.copy[data-done="1"]{color:#7BD88F;border-color:#3D6B4A}
|
|
code.inline{font-family:var(--mono);font-size:.86em;background:var(--sunk);
|
|
border:1px solid var(--border);border-radius:2px;padding:.08em .34em;word-break:break-word}
|
|
.caption{font-size:.82rem;color:var(--muted);margin:0}
|
|
.note{border-left:2px solid var(--accent);background:var(--accent-dim);
|
|
padding:.8rem 1rem;border-radius:0 3px 3px 0;font-size:.92rem}
|
|
.note.warn{border-left-color:var(--warn);background:var(--warn-dim)}
|
|
.note b{display:block;font-family:var(--mono);font-size:.68rem;text-transform:uppercase;
|
|
letter-spacing:.1em;margin-bottom:.3rem;color:var(--accent);font-weight:600}
|
|
.note.warn b{color:var(--warn)}
|
|
.note p{margin:0}
|
|
.mech{background:var(--surface);border:1px solid var(--border);border-radius:3px;
|
|
padding:1.5rem;margin-bottom:2.2rem}
|
|
.mech h2{font-size:1.15rem;margin:0 0 1rem;letter-spacing:-.01em}
|
|
.flow{display:flex;flex-direction:column;margin:0;padding:0;list-style:none}
|
|
.flow li{display:grid;grid-template-columns:8.5rem 1fr;gap:.9rem;padding:.6rem 0;
|
|
border-top:1px solid var(--border);font-size:.9rem}
|
|
.flow li:first-child{border-top:0;padding-top:0}
|
|
.flow b{font-family:var(--mono);font-size:.75rem;color:var(--accent);
|
|
font-weight:500;word-break:break-all}
|
|
@media(max-width:32rem){.flow li{grid-template-columns:1fr;gap:.15rem}}
|
|
h2.sec{font-size:1.15rem;margin:0 0 .9rem;letter-spacing:-.01em}
|
|
details{border:1px solid var(--border);border-radius:3px;background:var(--surface);
|
|
margin-bottom:.5rem}
|
|
summary{cursor:pointer;padding:.75rem 1rem;font-weight:550;font-size:.95rem;
|
|
list-style:none;display:flex;align-items:center;gap:.6rem}
|
|
summary::-webkit-details-marker{display:none}
|
|
summary::before{content:"+";font-family:var(--mono);color:var(--accent);font-size:1rem;line-height:1}
|
|
details[open] summary::before{content:"\2212"}
|
|
summary:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
|
|
.answer{padding:0 1rem 1rem;display:flex;flex-direction:column;gap:.7rem;
|
|
font-size:.92rem;color:var(--ink-soft)}
|
|
.answer p{margin:0}
|
|
footer{margin-top:3rem;padding-top:1.25rem;border-top:1px solid var(--border);
|
|
font-size:.84rem;color:var(--muted)}
|
|
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
|
|
</style>
|
|
|
|
<div class="wrap">
|
|
<header class="masthead">
|
|
<p class="eyebrow">Prism Launcher · $os_label</p>
|
|
<h1>Join $NAME, and stay on it</h1>
|
|
<p class="lede">A one-time setup that makes Prism re-sync your mods from the
|
|
server every time you hit Launch. A mod gets added, you get it next session —
|
|
no reinstalls, no zip files in chat.</p>
|
|
<ul class="facts">
|
|
<li><b>MC</b> <span>$MCVER</span></li>
|
|
<li><b>Loader</b> <span>NeoForge</span></li>
|
|
<li><b>Java</b> <span>21</span></li>
|
|
<li><b>Server</b> <span>$SERVER_ADDR</span></li>
|
|
</ul>
|
|
</header>
|
|
|
|
<section class="step"><div class="num">01</div>
|
|
<h2>Install Prism Launcher</h2>
|
|
<div class="body">
|
|
<p>$install_intro</p>
|
|
<div class="cmd"><pre><code>$install_cmd</code></pre><button class="copy" type="button">Copy</button></div>
|
|
<p class="caption">$install_note</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step"><div class="num">02</div>
|
|
<h2>Get Java 21</h2>
|
|
<div class="body">
|
|
<p>Minecraft $MCVER on NeoForge needs Java 21. Check what you have:</p>
|
|
<div class="cmd"><pre><code>java -version</code></pre><button class="copy" type="button">Copy</button></div>
|
|
<p class="caption">$java_note</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step"><div class="num">03</div>
|
|
<h2>Create the instance</h2>
|
|
<div class="body">
|
|
<p>Click <b>Add Instance</b> and set it up as:</p>
|
|
<ul>
|
|
<li><b>Version</b> — $MCVER</li>
|
|
<li><b>Mod loader</b> — NeoForge, latest for $MCVER</li>
|
|
</ul>
|
|
<p>Create it, but <b>don't launch it yet</b>. Leave the mods folder empty —
|
|
the next two steps fill it, and anything you add by hand is removed on the
|
|
first sync.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step"><div class="num">04</div>
|
|
<h2>Drop in the installer</h2>
|
|
<div class="body">
|
|
<p>Select the instance, click <b>Folder</b> in the right-hand panel, then open
|
|
the <code class="inline">.minecraft</code> folder inside it.</p>
|
|
<p>$dl_intro</p>
|
|
<div class="cmd"><pre><code>$dl_cmd</code></pre><button class="copy" type="button">Copy</button></div>
|
|
<p class="caption">$dl_note</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step"><div class="num">05</div>
|
|
<h2>Wire up the automatic pull</h2>
|
|
<div class="body">
|
|
<p>This is the step that does the work. Right-click the instance →
|
|
<b>Edit</b> → <b>Settings</b> → <b>Custom commands</b>. Tick
|
|
<b>Custom commands</b>, then paste this into <b>Pre-launch command</b>:</p>
|
|
<div class="cmd"><pre><code>"\$INST_JAVA" -jar packwiz-installer-bootstrap.jar -g -s client $PACK_URL/pack.toml</code></pre><button class="copy" type="button">Copy</button></div>
|
|
<p>Every launch from now on, Prism runs that first: it reads the pack from the
|
|
server, downloads anything new, removes anything dropped, and only then starts
|
|
the game.</p>
|
|
<div class="note"><b>What the flags do</b>
|
|
<p><code class="inline">-g</code> skips the progress window — drop it the first
|
|
time so you can watch it work and see any errors.
|
|
<code class="inline">-s client</code> installs client-side mods only.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="step"><div class="num">06</div>
|
|
<h2>Launch and add the server</h2>
|
|
<div class="body">
|
|
<p>Hit <b>Launch</b>. The first run downloads the full mod set, so give it a
|
|
minute. At the title screen go to <b>Multiplayer</b> → <b>Add Server</b>:</p>
|
|
<div class="cmd"><pre><code>$SERVER_ADDR</code></pre><button class="copy" type="button">Copy</button></div>
|
|
<p>That's it. From here on just launch normally — updates arrive on their own.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="mech">
|
|
<h2>What happens on every launch</h2>
|
|
<ol class="flow">
|
|
<li><b>pack.toml</b><span>Fetched first. Names the index and its hash, so a changed pack is spotted immediately.</span></li>
|
|
<li><b>index.toml</b><span>Lists every mod entry with its hash.</span></li>
|
|
<li><b>mods/*.pw.toml</b><span>One per mod: where to download it, and the hash it must match.</span></li>
|
|
<li><b>your mods folder</b><span>Missing files downloaded, changed files replaced, removed files deleted.</span></li>
|
|
</ol>
|
|
</section>
|
|
|
|
<section>
|
|
<h2 class="sec">When it doesn't work</h2>
|
|
|
|
<details><summary>"Unable to access jarfile packwiz-installer-bootstrap.jar"</summary>
|
|
<div class="answer">
|
|
<p>The pre-launch command ran somewhere other than the folder holding the jar. Use the full path instead:</p>
|
|
<div class="cmd"><pre><code>"\$INST_JAVA" -jar "\$INST_MC_DIR/packwiz-installer-bootstrap.jar" -g -s client $PACK_URL/pack.toml</code></pre><button class="copy" type="button">Copy</button></div>
|
|
</div>
|
|
</details>
|
|
|
|
<details><summary>Game launches with no mods at all</summary>
|
|
<div class="answer">
|
|
<p>The pre-launch command didn't run. Check that <b>Custom commands</b> is
|
|
actually ticked in the instance settings — pasting the command without
|
|
enabling the checkbox is the usual cause.</p>
|
|
<p>Remove the <code class="inline">-g</code> flag and launch again; you'll see
|
|
the installer window and whatever it's complaining about.</p>
|
|
</div>
|
|
</details>
|
|
|
|
<details><summary>A hash doesn't match and the install stops</summary>
|
|
<div class="answer">
|
|
<p>A file on the server changed but the pack still lists the old hash. That's
|
|
the pack being out of date, not your machine — nothing to fix client-side.</p>
|
|
<p>Tell $AUTHOR to re-run the refresh script, then launch again.</p>
|
|
</div>
|
|
</details>
|
|
|
|
<details><summary>Crash mentioning class versions or UnsupportedClassVersionError</summary>
|
|
<div class="answer">
|
|
<p>Wrong Java. NeoForge for $MCVER needs Java 21, and an older one loads far
|
|
enough to crash confusingly.</p>
|
|
<p>In Prism: <b>Edit</b> → <b>Settings</b> → <b>Java</b> → <b>Auto-detect</b>, and pick a 21.</p>
|
|
</div>
|
|
</details>
|
|
|
|
<details><summary>Mods I added by hand keep disappearing</summary>
|
|
<div class="answer">
|
|
<p>Working as intended — the sync makes your mods folder match the server's
|
|
exactly, so anything not in the pack is removed each launch.</p>
|
|
<p>Want a personal client-side mod? Ask $AUTHOR to add it to the pack.</p>
|
|
</div>
|
|
</details>
|
|
</section>
|
|
|
|
<footer>Pack served from $SITE_URL · $NAME $PACKVER · If the pre-launch step
|
|
fails, the game still launches, with whatever mods it had last time.</footer>
|
|
</div>
|
|
|
|
<script>
|
|
document.querySelectorAll('.copy').forEach(function(b){
|
|
b.addEventListener('click',function(){
|
|
var c=b.parentElement.querySelector('code'); if(!c)return;
|
|
navigator.clipboard.writeText(c.textContent.trim()).then(function(){
|
|
b.textContent='Copied'; b.dataset.done='1';
|
|
setTimeout(function(){b.textContent='Copy';delete b.dataset.done;},1600);
|
|
}).catch(function(){
|
|
b.textContent='Ctrl+C';
|
|
setTimeout(function(){b.textContent='Copy';},1600);
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
EOF
|
|
}
|
|
|
|
if (( GUIDES )); then
|
|
info "writing player setup guides to $WEBROOT"
|
|
for os in windows linux; do
|
|
out="$WEBROOT/setup-$os-packwiz.html"
|
|
write_guide "$os" "$out"
|
|
chmod 644 "$out"
|
|
printf ' %s -> %s/setup-%s-packwiz.html\n' "$out" "$SITE_URL" "$os"
|
|
done
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- finish ----
|
|
|
|
cat > .gitignore <<'EOF'
|
|
# packwiz build output
|
|
/build/
|
|
EOF
|
|
|
|
if command -v git >/dev/null && [ ! -d .git ]; then
|
|
git init -q && git add -A && git commit -qm "Initial packwiz pack" \
|
|
&& info "git repo initialised with an initial commit"
|
|
fi
|
|
|
|
echo
|
|
info "done — pack is in $(pwd)"
|
|
cat <<EOF
|
|
|
|
The mods are referenced by URL against $BASEURL, with hashes pinned in the
|
|
index. If you replace a jar on the mirror, its hash changes and clients will
|
|
fail the check — re-run 'packwiz url add' for that mod to update it.
|
|
|
|
packwiz refresh # rebuild the index after manual edits
|
|
packwiz serve # serve pack.toml locally for testing
|
|
packwiz url add <name> <url> # add one more mod from the mirror
|
|
packwiz remove <name>
|
|
|
|
Clients need $(pwd) served at $PACK_URL — that is where
|
|
pack.toml, index.toml and mods/*.pw.toml are fetched from. Verify with:
|
|
|
|
curl -fsS $PACK_URL/pack.toml | head -3
|
|
EOF
|