Packwiz-Abdelpak-hosting-kit/packwiz-setup.sh

587 lines
26 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
MCPORT="25565" # Minecraft port, shown in the guides
GUIDES=1 # write the player setup guides
FORCE=0 # move an existing pack aside and rebuild it
die() { echo "error: $*" >&2; exit 1; }
info() { echo ">>> $*"; }
warn() { echo " warning: $*" >&2; }
usage() { awk 'NR>1 && /^#/ {sub(/^# ?/,""); print; next} NR>1 {exit}' "$0"; 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 ;;
-p|--port) MCPORT="${2:-}"; shift 2 ;;
--no-guides) GUIDES=0; shift ;;
--force) FORCE=1; 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%/}"
# Minecraft assumes 25565, so only show a port when it is not that — a
# needless ":25565" in the address is a classic source of player confusion.
if [ "$MCPORT" = "25565" ]; then
SERVER_DISPLAY="$SERVER_ADDR"
else
SERVER_DISPLAY="$SERVER_ADDR:$MCPORT"
fi
[ -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 ----
if [ -e "$DIR/pack.toml" ]; then
if (( FORCE )); then
# Moved aside rather than deleted: the pack dir may hold hand-edited
# .pw.toml files or a git history, and a rebuild is not worth losing them.
bak="$DIR.bak.$(date +%Y%m%d%H%M%S)"
info "existing pack found — moving it to $bak"
mv "$DIR" "$bak"
else
die "$DIR/pack.toml already exists — refusing to overwrite
Pass --force to move it aside and rebuild, or 'packwiz init --reinit' there."
fi
fi
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_DISPLAY</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_DISPLAY</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