fix: patch Create Ore Excavation kubejs_create wrapper race + validate remote URLs
All checks were successful
Build & Release Pack / build (push) Successful in 1m50s

- COE 1.6.8 races kubejs-create for the ProcessingOutput rhino wrapper
  (guard flag set in @Mod ctor, KubeJS plugin callbacks run during its own
  construction) → nondeterministic 'Wrapper already exists!' boot crash.
  Ship ASM-patched jar (tools/patches/build_patched_coe.sh, Patcher.java).
- validate_pack.py: new 'remote' command — fetches every direct URL and
  byte-verifies non-CDN hashes; catches unpushed/ drifted patched jars.
- docs/server.md: stale-metadata failure row (-g is GUI-only), patched-jars
  table; docs/mods.md compat notes.
This commit is contained in:
MiTHRAL 2026-08-24 14:17:35 -04:00
parent a88a0c2421
commit be6fa51900
12 changed files with 220 additions and 13 deletions

View file

@ -352,7 +352,7 @@ metafile = true
[[files]] [[files]]
file = "mods/create-ore-excavation.pw.toml" file = "mods/create-ore-excavation.pw.toml"
hash = "10d3389f72e8e8f78518f5b6786ccec4e62dbad117491d2970e8e1f75e390251" hash = "d95f9869c9566617f24e06921a0809d94b87d4d9137aeb5b3f0f387ccb085441"
metafile = true metafile = true
[[files]] [[files]]

View file

@ -1,11 +1,11 @@
name = "Create Ore Excavation" name = "Create Ore Excavation (patched - no kubejs_create race wrapper)"
filename = "createoreexcavation-1.21-1.6.8.jar" filename = "createoreexcavation-1.21-1.6.8-patched.jar"
side = "both" side = "both"
[download] [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-format = "sha512"
hash = "844ae32ff0d22f4d3c83db3bae2f6edc0f74cd1a530f900bf18829838d5a354b1e439e52688416130d9fd9a52c3310458bbdbc7458f4c1c1d8ffb088ae802d32" hash = "0d2ca0631318639e46b5d6faf61c4905a385af29079e30780941ea14f9eaf85132acac7cdc1c42b15c7a46930aa69b33db4a80a981172af1293c4d1f8b4401b4"
[update] [update]
[update.modrinth] [update.modrinth]

View file

@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
[index] [index]
file = "index.toml" file = "index.toml"
hash-format = "sha256" hash-format = "sha256"
hash = "f854fe332229eab4108b858886c9773f021f831ce78b4d7c74193bc9ddb65d2c" hash = "e00761ce338bdd5fe09338b04cb68660f8ccbe6d3c646dda8ea96e751f4d5b5a"
[versions] [versions]
minecraft = "1.21.1" minecraft = "1.21.1"

View file

@ -89,6 +89,14 @@ config needed there.
- **The Void Dimension 0.0.1** hard-requires **World Portal** (and Dimension - **The Void Dimension 0.0.1** hard-requires **World Portal** (and Dimension
Fixer). World Portal (`side = "server"`) is bundled in both packs so the Fixer). World Portal (`side = "server"`) is bundled in both packs so the
dimension boots and is reachable in SP and MP. 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 - **Create addon libs** (all present): RPL (Big Cannons), Kotlin for Forge
(Slice & Dice, Jetpack), Mechanicals Lib (Mechanical Extruder), Architectury (Slice & Dice, Jetpack), Mechanicals Lib (Mechanical Extruder), Architectury
(Goggles). Flywheel is bundled inside Create — do **not** add a standalone (Goggles). Flywheel is bundled inside Create — do **not** add a standalone

View file

@ -65,6 +65,24 @@ Notes:
| Crash on world gen (Deeper and Darker) | Ensure CF 659011 + its lithostitched data load; update both. | | 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`. | | 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). | | 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/<name>.pw.toml`, run `packwiz refresh`, then
`python3 tools/validate_pack.py <pack> remote`.
## Backups ## Backups

View file

@ -293,7 +293,7 @@ metafile = true
[[files]] [[files]]
file = "mods/create-ore-excavation.pw.toml" file = "mods/create-ore-excavation.pw.toml"
hash = "10d3389f72e8e8f78518f5b6786ccec4e62dbad117491d2970e8e1f75e390251" hash = "d95f9869c9566617f24e06921a0809d94b87d4d9137aeb5b3f0f387ccb085441"
metafile = true metafile = true
[[files]] [[files]]

View file

@ -1,11 +1,11 @@
name = "Create Ore Excavation" name = "Create Ore Excavation (patched - no kubejs_create race wrapper)"
filename = "createoreexcavation-1.21-1.6.8.jar" filename = "createoreexcavation-1.21-1.6.8-patched.jar"
side = "both" side = "both"
[download] [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-format = "sha512"
hash = "844ae32ff0d22f4d3c83db3bae2f6edc0f74cd1a530f900bf18829838d5a354b1e439e52688416130d9fd9a52c3310458bbdbc7458f4c1c1d8ffb088ae802d32" hash = "0d2ca0631318639e46b5d6faf61c4905a385af29079e30780941ea14f9eaf85132acac7cdc1c42b15c7a46930aa69b33db4a80a981172af1293c4d1f8b4401b4"
[update] [update]
[update.modrinth] [update.modrinth]

View file

@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
[index] [index]
file = "index.toml" file = "index.toml"
hash-format = "sha256" hash-format = "sha256"
hash = "458e176e4de0ac679ee808a312155a3376039ebb3b20f42c827270fd183e5f25" hash = "e71c2f015f60a250c965cb6592bc1c5e00ef657b54e2cbe70afc2bbce62cec98"
[versions] [versions]
minecraft = "1.21.1" minecraft = "1.21.1"

View file

@ -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 <KubeJSExcavation.class-in> <class-out>");
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());
}
}

View file

@ -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"

View file

@ -6,15 +6,21 @@ Parses the limited, machine-generated TOML subset used by packwiz metadata.
""" """
import glob import glob
import hashlib
import json import json
import os import os
import re import re
import sys import sys
import urllib.request
import zipfile import zipfile
PW_TOML = re.compile(r"^(\w[\w.-]*)\s*=\s*(?:\"([^\"]*)\"|([0-9]+))", re.M) PW_TOML = re.compile(r"^(\w[\w.-]*)\s*=\s*(?:\"([^\"]*)\"|([0-9]+))", re.M)
SECTION = re.compile(r"^\[\[?([\w.\-]+)\]\]?$", 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): def parse_toml(path):
"""Parse the minimal TOML subset packwiz emits: scalars + sections + array-of-tables.""" """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))) 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(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("usage: validate_pack.py <pack_dir> [cf <zip>]", file=sys.stderr) print("usage: validate_pack.py <pack_dir> [cf <zip>] [remote]", file=sys.stderr)
sys.exit(2) sys.exit(2)
pack_dir = sys.argv[1] pack_dir = sys.argv[1]
cmd = sys.argv[2] if len(sys.argv) > 2 else "integrity" cmd = sys.argv[2] if len(sys.argv) > 2 else "integrity"
@ -138,6 +190,8 @@ def main():
check_cf(pack_dir, sys.argv[3]) check_cf(pack_dir, sys.argv[3])
elif cmd == "sanity": elif cmd == "sanity":
check_sanity(pack_dir) check_sanity(pack_dir)
elif cmd == "remote":
check_remote(pack_dir)
else: else:
check_integrity(pack_dir) check_integrity(pack_dir)