实测 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:
81
scaffold/LOADERS.md
Normal file
81
scaffold/LOADERS.md
Normal file
@ -0,0 +1,81 @@
|
||||
# 加载器差异与后端设计(实测)
|
||||
|
||||
用户判断(2026-09-25):**mod 支持分 fabric/forge,两者不同版本还有差异,
|
||||
所以各应有一个独立后端。** —— 实测支持这个结论,且差异比"方法签名"更深一层。
|
||||
|
||||
## 实测:Fabric 客户端能启动(2026-09-25)
|
||||
|
||||
命令(成功的形态):
|
||||
|
||||
```
|
||||
# ★ 必须剔除 1.20.1 自带的 asm(9.2 + 9.6 两版并存),改用 Fabric 的 asm 9.10.1
|
||||
# 否则 Knot.<clinit> 抛 "duplicate ASM classes found on classpath"
|
||||
grep -v "/org/ow2/asm/" vanilla_libs.txt > base.txt
|
||||
CP="$(paste -sd: base.txt):fabric-loader:asm*:sponge-mixin:intermediary:1.20.1.jar"
|
||||
DISPLAY=:98 java -cp "$CP" -Djava.library.path=nativ \
|
||||
net.fabricmc.loader.impl.launch.knot.KnotClient --gameDir ... --assets ... --assetIndex 1.20.1
|
||||
```
|
||||
|
||||
实测输出(证明真的启动了):
|
||||
|
||||
```
|
||||
[main/INFO]: Loading Minecraft 1.20.1 with Fabric Loader 0.19.5
|
||||
[main/INFO]: SpongePowered MIXIN Subsystem Version=0.8.7 ... Env=CLIENT
|
||||
[Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas
|
||||
... 共 12 个资源图集
|
||||
```
|
||||
|
||||
Fabric 必需的 8 个库(从 meta API 的 profile 拿,**纯声明式**):
|
||||
asm / asm-analysis / asm-commons / asm-tree / asm-util /
|
||||
sponge-mixin / intermediary / fabric-loader
|
||||
|
||||
## 为什么加载器必须各有独立后端(不只是版本差异)
|
||||
|
||||
| | vanilla | fabric | forge |
|
||||
|---|---|---|---|
|
||||
| 主类 | **我们自己的** `McfoldRenderServer` | `KnotClient`(固定,不可替换) | ForgeWrapper → FML |
|
||||
| 我们的代码形态 | 直接 `new Minecraft(cfg)` | **必须作为 mod 被加载** | 必须作为 coremod/mod |
|
||||
| 命名空间 | Mojang official(已反混淆) | **intermediary**(运行时重映射) | SRG/TSRG |
|
||||
| 起 Minecraft 实例 | 自己 new | 由 Loader 创建,我们**拿不到控制权** | 同 fabric |
|
||||
|
||||
★ 关键:Fabric/Forge 下**我们不拥有启动流程** —— Loader 起游戏、进主菜单、
|
||||
跑主循环。我们的「不跑主循环 + 手动喂世界数据」模型与它们**根本冲突**。
|
||||
|
||||
所以后端身份是 **(加载器, API 世代)** 二元组:
|
||||
|
||||
```
|
||||
vanilla:
|
||||
gen_1_20 已实测(渲染通过)
|
||||
gen_1_21 已实测(渲染通过)
|
||||
fabric:
|
||||
gen_1_20 待设计(架构与 vanilla 不同,见下)
|
||||
gen_1_21 待设计
|
||||
forge:
|
||||
gen_1_20 待设计
|
||||
gen_1_21 待设计
|
||||
```
|
||||
|
||||
## Fabric 下的两条可行路线(都待实测)
|
||||
|
||||
### 路线 A:作为 Fabric mod 被加载
|
||||
把我们的服务端打包成 fabric mod(`fabric.mod.json` + entrypoint),
|
||||
由 Loader 在游戏初始化后调用我们。**问题**:Loader 会继续跑主循环,
|
||||
我们要在 entrypoint 里接管渲染循环(或让它跑着、我们在 client tick 里注入数据)。
|
||||
改造量大,但能得到完整的 mod 生态(Iris 也在内)。
|
||||
|
||||
### 路线 B:只借 Fabric 的**注册表与资源包**,不跑其主循环
|
||||
用 Knot 启动(它会加载我们的 mod 并注册方块),但在我们的 mod entrypoint
|
||||
里**阻止进入主菜单**(替换 Screen),然后按 vanilla 那套手动喂数据 + 渲染。
|
||||
—— 即保留我们已验证的渲染循环,只从 Fabric 借「mod 加载 + 注册表」。
|
||||
|
||||
路线 B 更贴近现有架构(渲染循环不用重写),且已验证的部分(反混淆后的
|
||||
渲染调用、setblocks、RLE 路由)能复用。
|
||||
|
||||
## 当前已具备的零加载器能力(不依赖上面两条)
|
||||
|
||||
`cmd="mods"`(已在两个 vanilla 后端实现,编译通过、待实测):
|
||||
Java 侧自己扫 mods 目录 → 读 jar 的 `assets/<ns>/blockstates/*.json`
|
||||
→ 解冻 BLOCK 注册表 → 注册占位方块 + state → jar 挂为资源包。
|
||||
|
||||
覆盖范围(诚实标注):
|
||||
方块**外观** ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌ / Iris 光影 ❌
|
||||
@ -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");
|
||||
|
||||
@ -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.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.core.Registry;
|
||||
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;
|
||||
@ -703,6 +723,137 @@ 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;
|
||||
// ★ 世代差异(1.21+):ResourceLocation 构造变 private → 静态工厂
|
||||
ResourceLocation rl = ResourceLocation.fromNamespaceAndPath(ns, blk);
|
||||
try {
|
||||
if (BuiltInRegistries.BLOCK.containsKey(rl)) { skip++; continue; }
|
||||
// ★ 世代差异(1.21+):Properties.copy(Block) 已移除,只有 of()
|
||||
Block b = new Block(BlockBehaviour.Properties.of());
|
||||
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