diff --git a/README.md b/README.md
index 9872fe6..b22acc7 100644
--- a/README.md
+++ b/README.md
@@ -19,12 +19,13 @@ the stock MegaMek Swing client. See [TODO.md](TODO.md) for what's next and
## Constraint
**Minimal, upstreamable patches.** Everything builds against an unmodified
-release jar, the harness uses only public API, and the two patches that exist are
-latent MegaMek bugs rather than adaptations. A new MegaMek release should be a
+release jar, the harness uses only public API, and all four patches that exist
+are latent MegaMek bugs rather than adaptations. A new MegaMek release should be a
recompile plus at most a patch refresh, never a rebase.
-## Running the browser spike
+## Quickstart
+ ./scripts/bootstrap.sh # ~1.1GB of downloads, then builds everything
./spike-webswing/start.sh # blocks; Ctrl-C tears everything down
Open . You play TraineeA, Princess plays
@@ -55,6 +56,19 @@ out and Player Settings throws an NPE. Webswing Lite session settings do not hel
`maxClients: 1` refuses the second browser, and `CONTINUE_FOR_USER` +
`allowStealSession` lets it evict the first and locks out both.
+### What bootstrap does
+
+Nothing third-party is committed — the repo is ~40 files. `bootstrap.sh` fetches
+the pinned JDK 21, the MegaMek 0.51.0 release *and* source tarball, and Webswing
+Lite 26.4.5; then builds the harness, applies the patchset into
+`MegaMek-patched.jar`, generates the fontconfig, and primes MegaMek's
+`units.cache`. It is idempotent, so re-run it freely.
+
+Host requirements: `curl`, `tar`, `unzip`, `sha256sum`, and **`fontconfig`**
+(`fc-match`). Without fontconfig the client dies at startup with
+"Fontconfig head is null" — bootstrap warns rather than failing, so the message
+is easy to miss.
+
## Patches
Unified diffs in `patches/patchset/`, applied `-p1` against a pristine tree —
@@ -111,15 +125,15 @@ Applied, and verified to be the only differences from the stock jar:
the adapter-pattern prototype)
- `spike-webswing/` — Webswing Lite + MegaMek settings templates, `start.sh`
- `patches/` — patchset, archive, patched-jar builder
-- `scripts/` — `fetch-deps.sh`, `gen-fontconfig.sh` (legacy fontconfig Webswing Lite's
- toolkit needs), `cdp-screenshot.py`, `summarize.py`
+- `scripts/` — `bootstrap.sh` (one-shot setup), `fetch-deps.sh`,
+ `gen-fontconfig.sh` (legacy fontconfig Webswing Lite's toolkit needs),
+ `cdp-screenshot.py`, `summarize.py`
- `results/` — bench CSVs and jstacks kept as evidence for Findings
## Benchmarks
```sh
-./scripts/fetch-deps.sh
-./harness/build.sh
+./scripts/bootstrap.sh
./harness/run.sh --games 8 --settle-round 3 --csv results/out.csv
python3 scripts/summarize.py results/out.csv
```
diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh
new file mode 100755
index 0000000..7951c13
--- /dev/null
+++ b/scripts/bootstrap.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# Take a fresh checkout to the point where you can play a match.
+#
+# ./scripts/bootstrap.sh
+# ./spike-webswing/start.sh # then open http://localhost:8080/megamek/
+#
+# Steps, all idempotent:
+# 1. fetch third-party deps (JDK, MegaMek release + source, Webswing Lite)
+# 2. compile the harness against the stock MegaMek jar
+# 3. build MegaMek-patched.jar by applying patches/patchset/
+# 4. generate the legacy fontconfig Webswing Lite's toolkit requires
+# 5. prime MegaMek's units.cache so the first launch is not 17s slower
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT"
+
+step() { printf '\n=== %s ===\n' "$1"; }
+
+step "1/5 third-party dependencies"
+./scripts/fetch-deps.sh
+
+step "2/5 harness"
+./harness/build.sh
+
+step "3/5 patched MegaMek jar"
+./patches/build-patched-jar.sh
+
+step "4/5 fontconfig"
+# Needs fontconfig tooling on the host; MegaMek's symbol glyphs depend on it.
+if command -v fc-match >/dev/null; then
+ ./scripts/gen-fontconfig.sh
+else
+ echo "SKIPPED: fc-match not found. Install fontconfig, then run" >&2
+ echo " ./scripts/gen-fontconfig.sh" >&2
+ echo "Without it the MegaMek client dies at startup with 'Fontconfig head is null'." >&2
+fi
+
+step "5/5 prime units.cache"
+# MegaMek builds this by walking ~11k unit files on first run - 20s instead of 3s.
+# Doing it here keeps it out of the first match.
+MM="$ROOT/MegaMek-0.51.00"
+if [ -f "$MM/data/mekfiles/units.cache" ]; then
+ echo " already present"
+else
+ echo " building (this takes ~20s) ..."
+ ( cd "$MM" && "$ROOT/jdk-21.0.12+8/bin/java" \
+ -Dlog4j2.configurationFile="$ROOT/harness/log4j2-quiet.xml" \
+ --add-opens java.base/java.util=ALL-UNNAMED \
+ --add-opens java.base/java.util.concurrent=ALL-UNNAMED \
+ -cp "$ROOT/harness/out:$ROOT/MegaMek-patched.jar:$(ls "$MM"/lib/*.jar | tr '\n' ':')" \
+ bench.IsolationCheck >/dev/null 2>&1 ) || true
+ [ -f "$MM/data/mekfiles/units.cache" ] && echo " built" || echo " (not built; first launch will be slower)"
+fi
+
+cat <<'DONE'
+
+=== ready ===
+
+ ./spike-webswing/start.sh then open http://localhost:8080/megamek/
+ ./spike-webswing/stop.sh stop it (never pkill - see README)
+
+ You play TraineeA, Princess plays TraineeB. Press "Done" in the lobby.
+DONE
diff --git a/scripts/fetch-deps.sh b/scripts/fetch-deps.sh
index c57a8f4..e191c49 100755
--- a/scripts/fetch-deps.sh
+++ b/scripts/fetch-deps.sh
@@ -1,20 +1,72 @@
#!/usr/bin/env bash
-# Fetch the pinned JDK and MegaMek release. Both are gitignored; this makes the
-# repo reproducible without committing ~700MB of third-party artifacts.
+# Fetch every third-party artifact the spike needs. All of them are gitignored,
+# so the repo stays small and this script is what makes a fresh checkout usable.
+#
+# Idempotent: already-downloaded archives and already-extracted trees are skipped,
+# so it is safe to re-run.
+#
+# ./scripts/fetch-deps.sh
+#
+# Downloads (~1.1GB total, mostly MegaMek's data directory):
+# Temurin JDK 21 MegaMek needs Java 21 exactly; checksum-verified
+# MegaMek release the jar plus data/ (units, boards, images, fonts)
+# MegaMek source needed by patches/build-patched-jar.sh to apply the patchset
+# Webswing Lite AGPL-3.0 fork by manticore-projects - NOT commercial Webswing
set -euo pipefail
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
mkdir -p dl
JDK_URL="https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12%2B8/OpenJDK21U-jdk_x64_linux_hotspot_21.0.12_8.tar.gz"
JDK_SHA="e4446ff06a276155697597cc0f1b15da004ff083f4964a35271ecee567177370"
-MM_URL="https://github.com/MegaMek/megamek/releases/download/v0.51.0/MegaMek-0.51.0.tar.gz"
+JDK_DIR="jdk-21.0.12+8"
+
+MM_VERSION="0.51.0"
+MM_URL="https://github.com/MegaMek/megamek/releases/download/v${MM_VERSION}/MegaMek-${MM_VERSION}.tar.gz"
+MM_SRC_URL="https://github.com/MegaMek/megamek/archive/refs/tags/v${MM_VERSION}.tar.gz"
+MM_DIR="MegaMek-0.51.00" # note: the release tree has a trailing 0
+MM_SRC_DIR="megamek-${MM_VERSION}"
+
+WS_VERSION="26.4.5"
+WS_URL="https://github.com/manticore-projects/webswing/releases/download/${WS_VERSION}/webswing-${WS_VERSION}.zip"
+WS_DIR="webswing-dist"
+
+need() { command -v "$1" >/dev/null || { echo "ERROR: '$1' not found" >&2; exit 1; }; }
+need curl; need tar; need unzip; need sha256sum
+
+get() { # url dest
+ [ -f "$2" ] && { echo " have $(basename "$2")"; return 0; }
+ echo " downloading $(basename "$2") ..."
+ curl -fsSL -o "$2" "$1"
+}
+
+echo "JDK 21 (Temurin)"
+get "$JDK_URL" dl/jdk21.tar.gz
+echo "${JDK_SHA} dl/jdk21.tar.gz" | sha256sum -c - >/dev/null || {
+ echo "ERROR: JDK checksum mismatch - delete dl/jdk21.tar.gz and retry" >&2; exit 1; }
+[ -d "$JDK_DIR" ] || tar xzf dl/jdk21.tar.gz
+echo " -> $JDK_DIR"
+
+echo "MegaMek $MM_VERSION (release)"
+get "$MM_URL" "dl/MegaMek-${MM_VERSION}.tar.gz"
+[ -d "$MM_DIR" ] || tar xzf "dl/MegaMek-${MM_VERSION}.tar.gz"
+echo " -> $MM_DIR"
-[ -f dl/jdk21.tar.gz ] || curl -sSL -o dl/jdk21.tar.gz "$JDK_URL"
-echo "${JDK_SHA} dl/jdk21.tar.gz" | sha256sum -c -
-[ -d jdk-21.0.12+8 ] || tar xzf dl/jdk21.tar.gz
+echo "MegaMek $MM_VERSION (source, needed to apply the patchset)"
+get "$MM_SRC_URL" "dl/megamek-src-${MM_VERSION}.tar.gz"
+[ -d "$MM_SRC_DIR" ] || tar xzf "dl/megamek-src-${MM_VERSION}.tar.gz"
+echo " -> $MM_SRC_DIR"
-[ -f dl/MegaMek-0.51.0.tar.gz ] || curl -sSL -o dl/MegaMek-0.51.0.tar.gz "$MM_URL"
-[ -d MegaMek-0.51.00 ] || tar xzf dl/MegaMek-0.51.0.tar.gz
+echo "Webswing Lite $WS_VERSION (AGPL-3.0 fork; not commercial Webswing)"
+get "$WS_URL" "dl/webswing-${WS_VERSION}.zip"
+[ -d "$WS_DIR" ] || unzip -q "dl/webswing-${WS_VERSION}.zip" -d "$WS_DIR"
+# The distribution ships no LICENSE file of its own; add the AGPL text so the
+# extracted tree states its terms. See GUIDELINES.md.
+[ -f "$WS_DIR/LICENSE.txt" ] || \
+ curl -fsSL -o "$WS_DIR/LICENSE.txt" "https://www.gnu.org/licenses/agpl-3.0.txt" || \
+ echo " (warning: could not fetch AGPL text for $WS_DIR/LICENSE.txt)"
+echo " -> $WS_DIR"
-echo "deps ready"
+echo
+echo "deps ready. Next: ./scripts/bootstrap.sh"
diff --git a/spike-webswing/start.sh b/spike-webswing/start.sh
index 50e96db..bff094e 100755
--- a/spike-webswing/start.sh
+++ b/spike-webswing/start.sh
@@ -62,8 +62,16 @@ fi
# Materialise the Webswing config with absolute paths for this checkout.
sed -e "s|@ROOT@|$ROOT|g" -e "s|@PORT_MM@|$PORT_MM|g" \
"$ROOT/spike-webswing/webswing.config.template" > "$WS_CONFIG"
-sed "s|^org.webswing.server.http.port=.*|org.webswing.server.http.port=$PORT_WS|" \
- "$WS/jetty.properties" > "$WS_JETTY"
+# Written from scratch rather than derived from the shipped jetty.properties:
+# that file defaults to https=true pointing at an ssl/keystore.jks the
+# distribution does not contain, so a fresh extract fails to start. Generating it
+# means a clean checkout works with no hand-editing of webswing-dist.
+cat > "$WS_JETTY" <