fix: TDWCM join failure — optional channels + install on both sides
All checks were successful
Build & Release Pack / build (push) Successful in 1m51s

tdwcm 0.2.1 registered ~31 payloads via playBidirectional without
.optional() AND registers real blocks/items/entities, so a one-sided
install fails login twice (required-channel mismatch, then registry
sync). Ship ASM-patched jar (tools/patches/build_patched_tdwcm.sh,
TdwcmPatcher.java) that inserts registrar.optional(), and add it to
server_pack so negotiation and registry sync pass on both sides.

Verified: dedicated server boots clean (Done ~27s), tdwcm:tardis_interior
dimension comes up under Sable physics + IPSable portal storage.
Audit: no other client-only mod in the pack uses NeoForge's payload API.
This commit is contained in:
MiTHRAL 2026-08-24 16:47:12 -04:00
parent ae0ede6c28
commit fde71b7f86
11 changed files with 163 additions and 9 deletions

View file

@ -862,7 +862,7 @@ metafile = true
[[files]]
file = "mods/the-doctor-who-client-mod.pw.toml"
hash = "133badb6d33d880f47ad59a60dd3916231168ba07701c957aee1f781d5d54a5a"
hash = "dd77144a6fe93e69509999bc5676017580649157602c8a61653344a3b6853879"
metafile = true
[[files]]

View file

@ -1,11 +1,11 @@
name = "THE Doctor Who Client Mod"
filename = "tdwcm-0.2.1.jar"
side = "client"
name = "THE Doctor Who Client Mod (patched - optional network channels)"
filename = "tdwcm-0.2.1-patched.jar"
side = "both"
[download]
url = "https://cdn.modrinth.com/data/96oAiXlp/versions/xpTvgljr/tdwcm-0.2.1.jar"
url = "https://git.mithraic.cloud/ad3laid3/SciCraft/raw/branch/main/tools/patches/tdwcm-0.2.1-patched.jar"
hash-format = "sha512"
hash = "68b3de21b7d3f6a5bc8d8e386a8a80e56828eb8c7335be9994875533120052c0c689d524ec140fec850cf2c13c40c6daac3f6f3cd3bff0df7c0ad2ab1888a292"
hash = "4169de97214ab71178791299d5164fd04b99ed2429cb2799b8e177849979406d98158954564b33149a0d505cfe23d56005b8462dc22390c0b77ee7edaa8f43a4"
[update]
[update.modrinth]

View file

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

View file

@ -21,9 +21,14 @@
`dynamic-fps`, `3dskinlayers`, `visuality-forge`, `midnightlib`,
`entityculling`, `immediatelyfast`, `badoptimizations`, `athena-ctm`,
`journeymap-integration`, `stellarview`, `stellar-view-mixin-renderer`,
`stellaris-stellarview-compatibility`, `the-doctor-who-client-mod`,
`stellaris-stellarview-compatibility`,
`simply-tooltips`, `inventory-profiles-next` (with `libipn`), `just-enough-mekanism-multiblocks`.
> **Note:** `the-doctor-who-client-mod` (tdwcm) used to be listed here but
> CANNOT run one-sided: it registers blocks/items/entities (registry sync)
> plus ~31 required network channels. It ships patched on BOTH sides since
> v1.0.1 — see compatibility notes below.
## Dimensional physics stack (both packs)
Physics / portals run on **Sable** + **Create Aeronautics** + **IPSable**
@ -97,6 +102,13 @@ config needed there.
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.
- **THE Doctor Who Client Mod runs on BOTH sides (patched)**: it registers
real blocks/items/entities via DeferredRegister and ~31 network payloads, so
a one-sided install fails login twice over (required-channel mismatch, then
registry sync). The patch marks all channels `.optional()`; with the mod on
both packs the negotiation passes regardless. Verified: dedicated server
boots clean with it (`Done` in ~10s). Audit 2026-08: no other client-only
mod in this pack uses NeoForge's payload API.
- **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

View file

@ -79,6 +79,7 @@ for any fresh install to work**:
|-----|-------|----------------|
| `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` |
| `tdwcm-0.2.1-patched.jar` | Marks all TDWCM network channels `.optional()` (client-only mod registered ~31 required payloads → join rejection vs servers without it) | `bash tools/patches/build_patched_tdwcm.sh` |
After changing either: update the hash in **both** packs'
`mods/<name>.pw.toml`, run `packwiz refresh`, then

View file

@ -711,6 +711,11 @@ file = "mods/terralith.pw.toml"
hash = "81c5c05f9ed4c07b9f027ff6c421939bb04e476075091f135d34aaa05af1f950"
metafile = true
[[files]]
file = "mods/the-doctor-who-client-mod.pw.toml"
hash = "70b16676553132b386a3cf5e082e7f86d9d2aa488efe047d74096481269e9793"
metafile = true
[[files]]
file = "mods/thevoiddimension.pw.toml"
hash = "41d3d7c6960b26b3f26eb532a3499c71ae976d2e434732d88e0ca56f7fd3b376"

View file

@ -0,0 +1,13 @@
name = "THE Doctor Who Client Mod (patched - optional channels)"
filename = "tdwcm-0.2.1-patched.jar"
side = "both"
[download]
url = "https://git.mithraic.cloud/ad3laid3/SciCraft/raw/branch/main/tools/patches/tdwcm-0.2.1-patched.jar"
hash-format = "sha512"
hash = "4169de97214ab71178791299d5164fd04b99ed2429cb2799b8e177849979406d98158954564b33149a0d505cfe23d56005b8462dc22390c0b77ee7edaa8f43a4"
[update]
[update.modrinth]
mod-id = "96oAiXlp"
version = "xpTvgljr"

View file

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

View file

@ -0,0 +1,68 @@
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.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* Makes all of TDWCM's payload registrations optional.
*
* Upstream bug: net.dumonde.tdwcm.networking.NetworkRegistry.register() does
* registrar = event.registrar("1.0");
* MESSAGES.forEach((t, m) -> registrar.playBidirectional(t, ...));
* — every channel (entity_rotation_sync + ~30 more) is REQUIRED, so clients
* carrying the mod cannot join any server without it ("channel missing").
*
* Fix: insert PayloadRegistrar.optional() right after registrar creation:
* registrar = event.registrar("1.0").optional();
* Optional payloads are skipped during login negotiation when absent on the
* other side, and client→server messages are dropped server-side instead of
* failing handshake. Exactly what a client-side-only mod wants.
*/
public class TdwcmPatcher {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("usage: TdwcmPatcher <NetworkRegistry.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 patched = false;
for (MethodNode mn : cn.methods) {
if (!"register".equals(mn.name)) continue;
for (AbstractInsnNode n : mn.instructions) {
if (!(n instanceof MethodInsnNode mi)) continue;
if (!"registrar".equals(mi.name)) continue;
if (!(mi.getNext() instanceof VarInsnNode store) || store.getOpcode() != Opcodes.ASTORE) continue;
InsnList ins = new InsnList();
ins.add(new MethodInsnNode(
Opcodes.INVOKEVIRTUAL,
"net/neoforged/neoforge/network/registration/PayloadRegistrar",
"optional",
"()Lnet/neoforged/neoforge/network/registration/PayloadRegistrar;",
false));
mn.instructions.insert(mi, ins);
patched = true;
break;
}
}
if (!patched) throw new IllegalStateException("registrar(...) call site not found");
// COMPUTE_FRAMES: offsets shifted by the insertion; classpath must contain
// the original jar + neoforge universal jar for hierarchy resolution.
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cn.accept(cw);
Files.write(Paths.get(args[1]), cw.toByteArray());
System.out.println("patched " + cn.name + ".register: registrar(...).optional() inserted");
}
}

View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# SCICRAFT — rebuild tdwcm with optional network channels.
#
# Why: THE Doctor Who Client Mod 0.2.1 registers all its payloads via
# PayloadRegistrar.playBidirectional WITHOUT .optional(), so clients carrying
# it are rejected by any server without the mod ("tdwcm:entity_rotation_sync +
# 30 more channels missing"). The mod is client-side-only in this pack, so we
# patch NetworkRegistry.register() to call registrar.optional() — absent
# payloads are then skipped in negotiation instead of failing login.
#
# Usage: tools/patches/build_patched_tdwcm.sh (from repo root)
set -euo pipefail
cd "$(dirname "$0")"
TDWCM_URL="https://cdn.modrinth.com/data/96oAiXlp/versions/xpTvgljr/tdwcm-0.2.1.jar"
OUT_JAR="tdwcm-0.2.1-patched.jar"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
echo "[patch] downloading original..."
curl -fsSL -o "$WORK/tdwcm.jar" "$TDWCM_URL"
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
# Neoforge universal jar for COMPUTE_FRAMES hierarchy resolution
NFG="${NEOFORGE_UNIVERSAL:-$WORK/neoforge-universal.jar}"
if [ ! -f "$NFG" ]; then
curl -fsSL -o "$WORK/neoforge-universal.jar" \
https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.248/neoforge-21.1.248-universal.jar
NFG="$WORK/neoforge-universal.jar"
fi
echo "[patch] compiling + running patcher..."
javac -cp asm.jar:asm-tree.jar TdwcmPatcher.java
unzip -q "$WORK/tdwcm.jar" "net/dumonde/tdwcm/networking/NetworkRegistry.class" -d "$WORK/x"
java -cp ".:asm.jar:asm-tree.jar:$WORK/tdwcm.jar:$NFG" TdwcmPatcher \
"$WORK/x/net/dumonde/tdwcm/networking/NetworkRegistry.class" \
"$WORK/NetworkRegistry.class"
echo "[patch] rebuilding jar..."
cp "$WORK/tdwcm.jar" "$OUT_JAR"
mkdir -p "$WORK/stage/net/dumonde/tdwcm/networking"
cp "$WORK/NetworkRegistry.class" "$WORK/stage/net/dumonde/tdwcm/networking/"
(cd "$WORK/stage" && jar uf "$OLDPWD/$OUT_JAR" net)
echo "[patch] verifying patched class inside jar..."
unzip -p "$OUT_JAR" net/dumonde/tdwcm/networking/NetworkRegistry.class > "$WORK/after.class"
cmp -s "$WORK/x/net/dumonde/tdwcm/networking/NetworkRegistry.class" "$WORK/after.class" \
&& { echo "[patch] ERROR: class unchanged"; exit 1; }
sha512sum "$OUT_JAR"
echo "[patch] done → $OUT_JAR"

Binary file not shown.