diff --git a/client_pack/index.toml b/client_pack/index.toml index b0a4416..e7caf03 100644 --- a/client_pack/index.toml +++ b/client_pack/index.toml @@ -352,7 +352,7 @@ metafile = true [[files]] file = "mods/create-ore-excavation.pw.toml" -hash = "10d3389f72e8e8f78518f5b6786ccec4e62dbad117491d2970e8e1f75e390251" +hash = "d95f9869c9566617f24e06921a0809d94b87d4d9137aeb5b3f0f387ccb085441" metafile = true [[files]] diff --git a/client_pack/mods/create-ore-excavation.pw.toml b/client_pack/mods/create-ore-excavation.pw.toml index 67db197..0ec29af 100644 --- a/client_pack/mods/create-ore-excavation.pw.toml +++ b/client_pack/mods/create-ore-excavation.pw.toml @@ -1,11 +1,11 @@ -name = "Create Ore Excavation" -filename = "createoreexcavation-1.21-1.6.8.jar" +name = "Create Ore Excavation (patched - no kubejs_create race wrapper)" +filename = "createoreexcavation-1.21-1.6.8-patched.jar" side = "both" [download] -url = "https://cdn.modrinth.com/data/ResbpANg/versions/tivxiPTo/createoreexcavation-1.21-1.6.8.jar" +url = "https://git.mithraic.cloud/ad3laid3/SciCraft/raw/branch/main/tools/patches/createoreexcavation-1.21-1.6.8-patched.jar" hash-format = "sha512" -hash = "844ae32ff0d22f4d3c83db3bae2f6edc0f74cd1a530f900bf18829838d5a354b1e439e52688416130d9fd9a52c3310458bbdbc7458f4c1c1d8ffb088ae802d32" +hash = "0d2ca0631318639e46b5d6faf61c4905a385af29079e30780941ea14f9eaf85132acac7cdc1c42b15c7a46930aa69b33db4a80a981172af1293c4d1f8b4401b4" [update] [update.modrinth] diff --git a/client_pack/pack.toml b/client_pack/pack.toml index f80978c..513bfff 100644 --- a/client_pack/pack.toml +++ b/client_pack/pack.toml @@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "f854fe332229eab4108b858886c9773f021f831ce78b4d7c74193bc9ddb65d2c" +hash = "e00761ce338bdd5fe09338b04cb68660f8ccbe6d3c646dda8ea96e751f4d5b5a" [versions] minecraft = "1.21.1" diff --git a/docs/mods.md b/docs/mods.md index 83ab480..37607a0 100644 --- a/docs/mods.md +++ b/docs/mods.md @@ -89,6 +89,14 @@ config needed there. - **The Void Dimension 0.0.1** hard-requires **World Portal** (and Dimension Fixer). World Portal (`side = "server"`) is bundled in both packs so the dimension boots and is reachable in SP and MP. +- **World Portal ships patched** (EntityLMixin stripped — it hard-conflicts + with Sable's entity mixins on dedicated servers). See `tools/patches/` and + the "Patched jars" table in `docs/server.md`. +- **Create Ore Excavation ships patched**: its KubeJS plugin races + kubejs-create to register a `ProcessingOutput` type-wrapper (guard flag is + set in the @Mod constructor while KubeJS runs plugin callbacks during its own + construction → nondeterministic boot crash). The patch removes COE's guarded + registration; kubejs-create remains the wrapper provider. - **Create addon libs** (all present): RPL (Big Cannons), Kotlin for Forge (Slice & Dice, Jetpack), Mechanicals Lib (Mechanical Extruder), Architectury (Goggles). Flywheel is bundled inside Create — do **not** add a standalone diff --git a/docs/server.md b/docs/server.md index 40abbc8..69f0697 100644 --- a/docs/server.md +++ b/docs/server.md @@ -65,6 +65,24 @@ Notes: | Crash on world gen (Deeper and Darker) | Ensure CF 659011 + its lithostitched data load; update both. | | TPS lag from many AE2 cells | Check FTB chunk claims; lower `simulation-distance`. | | Immersive Portals won't load | Must be the Fabric jar added via `url add --force` (Modrinth blocks loader mismatch). | +| Server boots with OLD mod set after a pack update | `packwiz-installer -g` only disables the GUI — it does NOT refresh metadata. Freshness comes from which `pack.toml` it reads: if the panel cloned the repo, `git pull` (or wipe + re-clone) before restart; the start script prefers a **local** `server_pack/pack.toml` over the remote URL. Verify with `python3 tools/validate_pack.py server_pack remote`. | +| `EntityLMixin` / worldportal crash on boot | The pack ships a patched World Portal (`tools/patches/`); make sure `mods/worldportal-neoforge-0.0.10+1.21-patched.jar` is what actually landed (see stale-metadata row above). | +| KubeJS `Wrapper for class ...ProcessingOutput already exists!` | Pack ships a patched Create Ore Excavation (`tools/patches/build_patched_coe.sh`) that removes its duplicate rhino wrapper; upstream race between COE and kubejs-create mod-construction order. Verify the `-patched.jar` landed (see stale-metadata row above). | + +### Patched jars in this pack + +Two mods ship as locally patched jars under `tools/patches/`; both are +referenced by `mods/*.pw.toml` via raw git URLs, so **the jars must be pushed +for any fresh install to work**: + +| Jar | Patch | Reproduce with | +|-----|-------|----------------| +| `worldportal-neoforge-0.0.10+1.21-patched.jar` | Removes `EntityLMixin` (conflicts with Sable mixins) | hand-patched; see commit a88a0c2 | +| `createoreexcavation-1.21-1.6.8-patched.jar` | Strips COE's guarded `ProcessingOutput` wrapper registration (race vs kubejs-create) | `bash tools/patches/build_patched_coe.sh` | + +After changing either: update the hash in **both** packs' +`mods/.pw.toml`, run `packwiz refresh`, then +`python3 tools/validate_pack.py remote`. ## Backups diff --git a/server_pack/index.toml b/server_pack/index.toml index 35b5c1d..e299d01 100644 --- a/server_pack/index.toml +++ b/server_pack/index.toml @@ -293,7 +293,7 @@ metafile = true [[files]] file = "mods/create-ore-excavation.pw.toml" -hash = "10d3389f72e8e8f78518f5b6786ccec4e62dbad117491d2970e8e1f75e390251" +hash = "d95f9869c9566617f24e06921a0809d94b87d4d9137aeb5b3f0f387ccb085441" metafile = true [[files]] diff --git a/server_pack/mods/create-ore-excavation.pw.toml b/server_pack/mods/create-ore-excavation.pw.toml index 67db197..0ec29af 100644 --- a/server_pack/mods/create-ore-excavation.pw.toml +++ b/server_pack/mods/create-ore-excavation.pw.toml @@ -1,11 +1,11 @@ -name = "Create Ore Excavation" -filename = "createoreexcavation-1.21-1.6.8.jar" +name = "Create Ore Excavation (patched - no kubejs_create race wrapper)" +filename = "createoreexcavation-1.21-1.6.8-patched.jar" side = "both" [download] -url = "https://cdn.modrinth.com/data/ResbpANg/versions/tivxiPTo/createoreexcavation-1.21-1.6.8.jar" +url = "https://git.mithraic.cloud/ad3laid3/SciCraft/raw/branch/main/tools/patches/createoreexcavation-1.21-1.6.8-patched.jar" hash-format = "sha512" -hash = "844ae32ff0d22f4d3c83db3bae2f6edc0f74cd1a530f900bf18829838d5a354b1e439e52688416130d9fd9a52c3310458bbdbc7458f4c1c1d8ffb088ae802d32" +hash = "0d2ca0631318639e46b5d6faf61c4905a385af29079e30780941ea14f9eaf85132acac7cdc1c42b15c7a46930aa69b33db4a80a981172af1293c4d1f8b4401b4" [update] [update.modrinth] diff --git a/server_pack/pack.toml b/server_pack/pack.toml index bfd6c84..7dbd164 100644 --- a/server_pack/pack.toml +++ b/server_pack/pack.toml @@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0" [index] file = "index.toml" hash-format = "sha256" -hash = "458e176e4de0ac679ee808a312155a3376039ebb3b20f42c827270fd183e5f25" +hash = "e71c2f015f60a250c965cb6592bc1c5e00ef657b54e2cbe70afc2bbce62cec98" [versions] minecraft = "1.21.1" diff --git a/tools/patches/Patcher.java b/tools/patches/Patcher.java new file mode 100644 index 0000000..72e1282 --- /dev/null +++ b/tools/patches/Patcher.java @@ -0,0 +1,78 @@ +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodNode; + +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Strips Create Ore Excavation's guarded ProcessingOutput type-wrapper registration + * from KubeJSExcavation.registerTypeWrappers. + * + * Upstream guard: `if (!CreateOreExcavation.kubeJSCreate) register(ProcessingOutput, ...)` + * — but kubeJSCreate is assigned in the @Mod constructor while KubeJS invokes plugin + * callbacks during KubeJS's own construction. FML constructs independent mods on + * parallel workers, so the flag may still be false when the callback runs → duplicate + * wrapper vs kubejs-create → "Wrapper already exists!" crash (nondeterministic). + * + * This pack always ships kubejs-create, so COE never needs its own wrapper: remove + * the whole guarded block (getstatic/ifne/…/invokevirtual) up to its branch-target + * label. Later frames stay valid because operand stack at that label is unchanged. + */ +public class Patcher { + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.err.println("usage: Patcher "); + System.exit(2); + } + byte[] bytes = Files.readAllBytes(Paths.get(args[0])); + ClassNode cn = new ClassNode(); + new ClassReader(bytes).accept(cn, 0); + + boolean found = false; + for (MethodNode mn : cn.methods) { + if (!"registerTypeWrappers".equals(mn.name)) continue; + found = true; + + AbstractInsnNode start = null; + for (AbstractInsnNode n : mn.instructions) { + if (n instanceof FieldInsnNode f + && n.getOpcode() == Opcodes.GETSTATIC + && "kubeJSCreate".equals(f.name)) { + start = n; + break; + } + } + if (start == null) throw new IllegalStateException("kubeJSCreate guard not found"); + + AbstractInsnNode jump = start.getNext(); + if (!(jump instanceof JumpInsnNode) || jump.getOpcode() != Opcodes.IFNE) { + throw new IllegalStateException("expected IFNE after getstatic"); + } + LabelNode target = ((JumpInsnNode) jump).label; + + int removed = 0; + AbstractInsnNode n = start; + while (n != target) { + AbstractInsnNode next = n.getNext(); + mn.instructions.remove(n); + n = next; + removed++; + if (n == null) throw new IllegalStateException("branch target not reached"); + } + System.out.println("patched " + cn.name + "." + mn.name + + ": removed " + removed + " instructions up to branch target"); + } + if (!found) throw new IllegalStateException("registerTypeWrappers not found"); + + ClassWriter cw = new ClassWriter(0); + cn.accept(cw); + Files.write(Paths.get(args[1]), cw.toByteArray()); + } +} diff --git a/tools/patches/build_patched_coe.sh b/tools/patches/build_patched_coe.sh new file mode 100644 index 0000000..194de65 --- /dev/null +++ b/tools/patches/build_patched_coe.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# SCICRAFT — rebuild createoreexcavation with the kubejs-create wrapper race fixed. +# +# Why: COE 1.21.1-1.6.8 decides in KubeJSExcavation.registerTypeWrappers whether to +# register its own ProcessingOutput rhino wrapper, based on a flag set in the @Mod +# constructor. KubeJS runs plugin callbacks during ITS construction; FML constructs +# independent mods on parallel workers → race. If the callback wins, both COE and +# kubejs-create register ProcessingOutput → "Wrapper already exists!" boot crash. +# +# Fix: this pack always ships kubejs-create (scripts use event.recipes.create.*), +# so strip COE's guarded registration entirely via ASM (see Patcher.java). +# +# Usage: tools/patches/build_patched_coe.sh (from repo root) +set -euo pipefail +cd "$(dirname "$0")" + +COE_URL="https://cdn.modrinth.com/data/ResbpANg/versions/tivxiPTo/createoreexcavation-1.21-1.6.8.jar" +OUT_JAR="createoreexcavation-1.21-1.6.8-patched.jar" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +echo "[patch] downloading original..." +curl -fsSL -o "$WORK/coe.jar" "$COE_URL" +unzip -q "$WORK/coe.jar" "com/tom/createores/kubejs/KubeJSExcavation.class" -d "$WORK/x" + +if [ ! -f asm.jar ]; then + echo "[patch] fetching ASM..." + curl -fsSL -o asm.jar https://repo1.maven.org/maven2/org/ow2/asm/asm/9.7.1/asm-9.7.1.jar + curl -fsSL -o asm-tree.jar https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.7.1/asm-tree-9.7.1.jar +fi + +echo "[patch] compiling + running patcher..." +javac -cp asm.jar:asm-tree.jar Patcher.java +java -cp .:asm.jar:asm-tree.jar Patcher \ + "$WORK/x/com/tom/createores/kubejs/KubeJSExcavation.class" \ + "$WORK/KubeJSExcavation.class" + +echo "[patch] rebuilding jar..." +cp "$WORK/coe.jar" "$OUT_JAR" +mkdir -p "$WORK/stage/com/tom/createores/kubejs" +cp "$WORK/KubeJSExcavation.class" "$WORK/stage/com/tom/createores/kubejs/" +(cd "$WORK/stage" && jar uf "$OLDPWD/$OUT_JAR" com) + +echo "[patch] verifying: patched class inside jar differs, rest untouched..." +unzip -p "$OUT_JAR" com/tom/createores/kubejs/KubeJSExcavation.class > "$WORK/after.class" +cmp -s "$WORK/x/com/tom/createores/kubejs/KubeJSExcavation.class" "$WORK/after.class" \ + && { echo "[patch] ERROR: class unchanged"; exit 1; } +sha512sum "$OUT_JAR" +echo "[patch] done → $OUT_JAR" diff --git a/tools/patches/createoreexcavation-1.21-1.6.8-patched.jar b/tools/patches/createoreexcavation-1.21-1.6.8-patched.jar new file mode 100644 index 0000000..0fcab75 Binary files /dev/null and b/tools/patches/createoreexcavation-1.21-1.6.8-patched.jar differ diff --git a/tools/validate_pack.py b/tools/validate_pack.py index e9be1e5..9032a8b 100644 --- a/tools/validate_pack.py +++ b/tools/validate_pack.py @@ -6,15 +6,21 @@ Parses the limited, machine-generated TOML subset used by packwiz metadata. """ import glob +import hashlib import json import os import re import sys +import urllib.request import zipfile PW_TOML = re.compile(r"^(\w[\w.-]*)\s*=\s*(?:\"([^\"]*)\"|([0-9]+))", re.M) SECTION = re.compile(r"^\[\[?([\w.\-]+)\]\]?$", re.M) +# Content-addressed CDNs are immutable; full-hash them opportunistically only. +# Everything else (git raw links, GitHub releases) can drift — verify bytes fully. +FULL_HASH_HOSTS = () + def parse_toml(path): """Parse the minimal TOML subset packwiz emits: scalars + sections + array-of-tables.""" @@ -128,9 +134,55 @@ def check_cf(pack_dir, zip_path): print("OK: CurseForge export complete (%d manifest files + %d overrides)" % (len(pids), len(ov))) +def _fetch(url, timeout=30): + req = urllib.request.Request(url, method="GET", headers={"User-Agent": "scicraft-validate"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.read() + + +def check_remote(pack_dir): + """Verify every direct-URL mod is reachable; fully hash-verify non-CDN URLs. + + Catches the two deployment failure modes seen in practice: + 1. patched jars whose raw git link 404s (never pushed / branch renamed) + 2. remote drift: served bytes no longer match the index hash + """ + from urllib.parse import urlparse + + errors = 0 + checked = hashed = 0 + for f in sorted(glob.glob(os.path.join(pack_dir, "mods", "*.pw.toml"))): + d = parse_toml(f).get("download", {}) + url = d.get("url") + if not url or d.get("mode", "").startswith("metadata:"): + continue + name = os.path.basename(f) + host = urlparse(url).netloc + try: + body = _fetch(url) + except Exception as e: + print("ERROR: %s unreachable (%s): %s" % (name, host, e)) + errors += 1 + continue + checked += 1 + if host not in FULL_HASH_HOSTS: + digest = hashlib.sha512(body).hexdigest() + expected = d.get("hash", "") + if d.get("hash-format") == "sha512" and digest != expected: + print("ERROR: %s remote hash mismatch (%s)" % (name, host)) + print(" expected %s" % expected[:32] + "...") + print(" remote %s..." % digest[:32]) + errors += 1 + else: + hashed += 1 + if errors: + sys.exit(1) + print("OK: %d direct URLs reachable, %d byte-for-byte hash-verified" % (checked, hashed)) + + def main(): if len(sys.argv) < 2: - print("usage: validate_pack.py [cf ]", file=sys.stderr) + print("usage: validate_pack.py [cf ] [remote]", file=sys.stderr) sys.exit(2) pack_dir = sys.argv[1] cmd = sys.argv[2] if len(sys.argv) > 2 else "integrity" @@ -138,6 +190,8 @@ def main(): check_cf(pack_dir, sys.argv[3]) elif cmd == "sanity": check_sanity(pack_dir) + elif cmd == "remote": + check_remote(pack_dir) else: check_integrity(pack_dir)