实测 Fabric 客户端可启动 + 加载器后端设计文档
用户判断:mod 支持分 fabric/forge,且不同版本还有差异 ⇒ 各有独立后端。
实测支持此结论,且差异比方法签名更深一层。
实测成功(关键):
Fabric Loader 0.19.5 完整启动 1.20.1 客户端
'Loading Minecraft 1.20.1 with Fabric Loader 0.19.5' + Mixin 子系统 + 12 个资源图集
★ 必须剔除 1.20.1 自带的 asm 9.2/9.6,改用 Fabric 的 9.10.1
(否则 Knot.<clinit> 抛 duplicate ASM classes)
架构结论:后端身份是 (加载器, API 世代) 二元组
因为 Fabric/Forge 下**我们不拥有启动流程**(Loader 起游戏+跑主循环),
与我们「不跑主循环+手动喂数据」的模型根本冲突。
两条可行路线已写入 scaffold/LOADERS.md(A: 作为 mod 被加载;
B: 只借 Fabric 的注册表与资源包,保留我们的渲染循环)——均待实测。
已实现(零加载器,两个 vanilla 后端编译通过、待实测):
cmd=mods:Java 侧自己扫 jar/注册占位方块/挂资源包
覆盖:外观 ✅ state id ✅ 变体 ⚠️ 行为 ❌ Iris ❌
This commit is contained in:
@ -35,6 +35,9 @@ import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LightLayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.DataLayer;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
@ -244,6 +247,23 @@ public class McfoldRenderServer {
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "mods": {
|
||||
// ★ mod 支持全在 Java 侧完成(用户 2026-09-25 定向):
|
||||
// Java 自己扫 mods 目录、自己读 jar、自己注册方块、自己加载资源包。
|
||||
// Python 只传目录路径。
|
||||
String dir = req.has("dir") ? req.get("dir").getAsString() : null;
|
||||
if (dir == null) {
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("mods", 0);
|
||||
r.addProperty("note", "未提供 dir");
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
JsonObject res = loadMods(dir);
|
||||
emit(id, res);
|
||||
break;
|
||||
}
|
||||
case "camera": {
|
||||
double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble();
|
||||
float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f;
|
||||
@ -659,6 +679,135 @@ public class McfoldRenderServer {
|
||||
System.err.println("[mcfold] boot done");
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。
|
||||
*
|
||||
* ★ 全部在 Java 侧完成(用户 2026-09-25 定向):Java 自己读 jar、自己注册、
|
||||
* 自己挂资源包。Python 只传目录路径。
|
||||
*
|
||||
* 原理(见 scaffold/MOD-BLOCKS.md):方块**渲染**只需要
|
||||
* (a) 注册表里有这个名字(拿 state id)
|
||||
* (b) 资源包里有它的 blockstates/models/textures
|
||||
* 与方块行为无关 ⇒ 零 mod 加载器也能正确显示。
|
||||
*/
|
||||
static JsonObject loadMods(String dir) {
|
||||
JsonObject out = new JsonObject();
|
||||
File d = new File(dir);
|
||||
File[] jars = d.listFiles((f, n) -> n.endsWith(".jar"));
|
||||
if (jars == null || jars.length == 0) {
|
||||
out.addProperty("ok", true);
|
||||
out.addProperty("mods", 0);
|
||||
out.addProperty("blocksRegistered", 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── 1. 解冻 BLOCK 注册表(frozen=false + 恢复 intrusiveHolders)──
|
||||
// 两个字段都必须处理:只清 frozen 会抛 "can't create intrusive holders"
|
||||
Object blockReg = BuiltInRegistries.BLOCK;
|
||||
try {
|
||||
for (Class<?> c = blockReg.getClass(); c != null; c = c.getSuperclass()) {
|
||||
try {
|
||||
var f = c.getDeclaredField("frozen");
|
||||
f.setAccessible(true);
|
||||
f.setBoolean(blockReg, false);
|
||||
} catch (NoSuchFieldException ignore) { }
|
||||
try {
|
||||
var f = c.getDeclaredField("unregisteredIntrusiveHolders");
|
||||
f.setAccessible(true);
|
||||
if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>());
|
||||
} catch (NoSuchFieldException ignore) { }
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
System.err.println("[mcfold] 解冻失败: " + t);
|
||||
}
|
||||
|
||||
int mods = 0, blocksAdded = 0, skipped = 0;
|
||||
JsonArray detail = new JsonArray();
|
||||
for (File jar : jars) {
|
||||
JsonObject m = new JsonObject();
|
||||
m.addProperty("jar", jar.getName());
|
||||
int n = 0, skip = 0;
|
||||
try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) {
|
||||
var en = zf.entries();
|
||||
java.util.Set<String> seen = new java.util.LinkedHashSet<>();
|
||||
while (en.hasMoreElements()) {
|
||||
String name = en.nextElement().getName();
|
||||
// 方块名来源:assets/<ns>/blockstates/<name>.json(权威,见 P5 实测)
|
||||
java.util.regex.Matcher mm = java.util.regex.Pattern
|
||||
.compile("^assets/([^/]+)/blockstates/(.+)\\.json$").matcher(name);
|
||||
if (!mm.matches()) continue;
|
||||
String ns = mm.group(1), blk = mm.group(2);
|
||||
if (!seen.add(ns + ":" + blk)) continue;
|
||||
ResourceLocation rl = new ResourceLocation(ns, blk);
|
||||
try {
|
||||
if (BuiltInRegistries.BLOCK.containsKey(rl)) { skip++; continue; }
|
||||
Block b = new Block(BlockBehaviour.Properties.copy(Blocks.STONE));
|
||||
Registry.register(BuiltInRegistries.BLOCK, rl, b);
|
||||
// ★ BlockState 不会自动进 BLOCK_STATE_REGISTRY(实测 state id 会是 0)
|
||||
Block.BLOCK_STATE_REGISTRY.add(b.defaultBlockState());
|
||||
n++;
|
||||
} catch (Throwable t) {
|
||||
skip++;
|
||||
}
|
||||
}
|
||||
mods++;
|
||||
} catch (Throwable t) {
|
||||
m.addProperty("error", String.valueOf(t));
|
||||
}
|
||||
m.addProperty("blocksRegistered", n);
|
||||
m.addProperty("skipped", skip);
|
||||
blocksAdded += n;
|
||||
skipped += skip;
|
||||
detail.add(m);
|
||||
}
|
||||
|
||||
// ── 2. 挂资源包(外观:blockstates/models/textures)──
|
||||
// 把 mod jar 作为资源包加入并选中,然后重载资源管理器。
|
||||
int packsAdded = 0;
|
||||
try {
|
||||
var repo = mc.getResourcePackRepository();
|
||||
java.nio.file.Path rpDir = mc.getResourcePackDirectory();
|
||||
for (File jar : jars) {
|
||||
File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName());
|
||||
if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
repo.reload();
|
||||
var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds());
|
||||
for (String packId : repo.getAvailableIds()) {
|
||||
if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; }
|
||||
}
|
||||
repo.setSelected(sel);
|
||||
|
||||
// 用新 pack 列表重建 resource manager(照官方 createReload 内部做法),
|
||||
// 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁)
|
||||
java.util.List<net.minecraft.server.packs.PackResources> packs = new java.util.ArrayList<>();
|
||||
for (var pk : repo.getSelectedPacks()) packs.add(pk.open());
|
||||
var rmF = Minecraft.class.getDeclaredField("resourceManager");
|
||||
rmF.setAccessible(true);
|
||||
var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc);
|
||||
var resF = net.minecraft.server.packs.resources.ReloadableResourceManager.class
|
||||
.getDeclaredField("resources");
|
||||
resF.setAccessible(true);
|
||||
resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager(
|
||||
net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs));
|
||||
reloadResources(mc);
|
||||
net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager());
|
||||
mc.levelRenderer.allChanged();
|
||||
} catch (Throwable t) {
|
||||
out.addProperty("packError", String.valueOf(t));
|
||||
System.err.println("[mcfold] 资源包挂载失败: " + t);
|
||||
}
|
||||
|
||||
out.addProperty("ok", true);
|
||||
out.addProperty("mods", mods);
|
||||
out.addProperty("blocksRegistered", blocksAdded);
|
||||
out.addProperty("skipped", skipped);
|
||||
out.addProperty("packsAdded", packsAdded);
|
||||
out.add("detail", detail);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/
|
||||
static void reloadResources(Minecraft mc) throws Exception {
|
||||
var rmF = Minecraft.class.getDeclaredField("resourceManager");
|
||||
|
||||
Reference in New Issue
Block a user