世代分派体系:Python 层 switch + 各世代独立编译的 Java 后端
用户定向:在 python 层写 switch,调用不同 java 后端(而非一个文件里 switch)。
新增:
mcfold/backends/{__init__,base,gen_1_20,gen_1_21}.py 后端体系与分派
scaffold/java/gen_1_20/ 对 1.20.x client jar 编译
scaffold/java/gen_1_21/ 对 1.21.1 client jar 编译(✅ 实测编译通过)
scaffold/java/VERSION-DIFFS.md 实测差异全表
判据:DeltaTracker 类是否存在(1.21 引入,正是 render 签名变化的根源)
⇒ 比硬编码版本号稳
实测差异共 11 处(21 个检查点里 13 个跨世代完全一致):
setLevel / GameRenderer.render / countRendered* / createPackRepository /
ResourceLocation 构造 / enableDataFixerOptimizations(移除) / User 构造 /
ChunkStatus 包路径 / ClientPacketListener 构造(参数个数语义都变) /
loadEffect(变 private)
为什么必须分后端:DeltaTracker 在 1.20.x jar 中不存在,
两代调用无法共存于一份源码(实测编译报 cannot find symbol)。
1.21.1 反混淆已验证(29MB named jar,DeltaTracker 3 处)
This commit is contained in:
56
scaffold/java/VERSION-DIFFS.md
Normal file
56
scaffold/java/VERSION-DIFFS.md
Normal file
@ -0,0 +1,56 @@
|
||||
# 世代差异清单(实测,非推断)
|
||||
|
||||
来源:官方 `client_mappings.txt` 逐版本对比(1.20.1 / 1.20.2 / 1.21.1),
|
||||
以及两侧源码对各自 client jar 的实际编译结果。
|
||||
|
||||
## 判据
|
||||
|
||||
**`net.minecraft.client.DeltaTracker` 类是否存在** ⇒ 1.21+。
|
||||
这是 1.21 引入的时间抽象,也正是 `GameRenderer.render` 签名变化的根源。
|
||||
比硬编码版本号稳(官方改版本号不影响判据)。
|
||||
|
||||
实现:`mcfold/backends/__init__.py:detect_generation()`
|
||||
|
||||
## 差异全表(8 处,覆盖 21 个检查点中的 13 个「完全一致」之外的部分)
|
||||
|
||||
| # | API | 1.20.x | 1.21+ | 处理 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `Minecraft.setLevel` | `(ClientLevel)` | `(ClientLevel, ReceivingLevelScreen$Reason)` | gen_1_21 传 `Reason.OTHER` |
|
||||
| 2 | `GameRenderer.render` | `(float,long,boolean)` | `(DeltaTracker, boolean)` | gen_1_21 用 `mc.getTimer()` |
|
||||
| 3 | 区块计数 | `countRenderedChunks()` | `countRenderedSections()` | 各世代用各自名字 |
|
||||
| 4 | 同理 | `hasRenderedAllChunks()` | `hasRenderedAllSections()` | 同上 |
|
||||
| 5 | `ServerPacksSource.createPackRepository` | `(Path)` | `(Path, DirectoryValidator)` | gen_1_21 构造 validator |
|
||||
| 6 | `ResourceLocation` 构造 | `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | 静态工厂 |
|
||||
| 7 | `SharedConstants.enableDataFixerOptimizations` | 有 | **已移除** | gen_1_21 不调 |
|
||||
| 8 | `User` 构造 | `(String,String sessionId,...)` | `(String,UUID,...)` | 传 UUID |
|
||||
| 9 | `ChunkStatus` | `world.level.chunk.ChunkStatus` | `world.level.chunk.status.ChunkStatus` | 包路径变了 |
|
||||
| 10 | `ClientPacketListener` 构造 | `(Minecraft,Screen,Connection,ServerData,GameProfile,Telemetry)` | `(Minecraft,Connection,CommonListenerCookie)` | ★ 参数个数与语义都变 |
|
||||
| 11 | `GameRenderer.loadEffect` | package-private(同包可调) | **private**(同包也调不到) | gen_1_21 用反射 |
|
||||
|
||||
★ 关键结论:**差异集中在 11 处,而不是「每个版本一套」**。
|
||||
21 个检查点里 13 个(如 `tick`/`allChanged`/`replaceWithPacketData`/
|
||||
`updateViewCenter`/`getChunkSource`/`PackRepository.*`)跨世代**完全一致**。
|
||||
|
||||
## 为什么必须分后端(而不是一个文件里 switch)
|
||||
|
||||
Java 编译期绑定签名。实测:
|
||||
|
||||
```
|
||||
世代 B 的调用对 1.20.1 jar 编译:
|
||||
error: cannot find symbol class DeltaTracker
|
||||
```
|
||||
|
||||
`DeltaTracker` 在 1.20.x 的 jar 里不存在 ⇒ 两代的调用**无法共存于一份源码**。
|
||||
所以每个世代独立编译,Python 层做分派。
|
||||
|
||||
## 加新世代的做法(用户 2026-09-25 定向)
|
||||
|
||||
1. `scaffold/java/gen_<ver>/` 建目录,拷上一代源码
|
||||
2. 按上表改差异点(对着 **javap 输出**改,不要凭印象写 API 名)
|
||||
3. `mcfold/backends/gen_<ver>.py` 声明 `markers`(判据类)
|
||||
4. `mcfold/backends/__init__.py` 的 `GENERATIONS` 加一条
|
||||
5. 各自对**对应版本**的 named jar 编译
|
||||
|
||||
⚠️ 教训(本轮踩过):`Reason.LEVEL_LOAD`、`DirectoryValidator.NONE`
|
||||
都是凭印象写出来的、实际不存在的 API;`loadEffect` 的可见性也没验证。
|
||||
**改之前先 `javap`。**
|
||||
645
scaffold/java/gen_1_20/McfoldRenderServer.java
Normal file
645
scaffold/java/gen_1_20/McfoldRenderServer.java
Normal file
@ -0,0 +1,645 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.blaze3d.pipeline.MainTarget;
|
||||
import com.mojang.blaze3d.platform.DisplayData;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.User;
|
||||
import net.minecraft.client.main.GameConfig;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.telemetry.TelemetryEventSender;
|
||||
import net.minecraft.client.telemetry.WorldSessionTelemetryManager;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.resources.RegistryDataLoader;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.profiling.InactiveProfiler;
|
||||
import net.minecraft.world.Difficulty;
|
||||
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.BlockState;
|
||||
import net.minecraft.world.level.chunk.DataLayer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。
|
||||
*
|
||||
* 设计约束(用户 2026-09-25):
|
||||
* - 不起完整客户端:不跑主循环、不连服、不登录、不作玩家
|
||||
* - 我们的世界数据喂给官方渲染器出图
|
||||
*
|
||||
* 协议(stdin/stdout 各一行一个 JSON):
|
||||
* → {"id":1,"cmd":"chunk","cx":0,"cz":0,"data":"<b64>","heightmaps":null}
|
||||
* → {"id":2,"cmd":"camera","x":8,"y":70,"z":8,"yaw":0,"pitch":0}
|
||||
* → {"id":3,"cmd":"render","frames":60,"out":"/tmp/a.png"}
|
||||
* ← {"id":3,"ok":true,"width":854,"height":480,"chunksRendered":29,
|
||||
* "distinctColors":1450,"image":"<b64 png>"}
|
||||
*
|
||||
* 启动就绪时主动发 {"event":"ready"}。
|
||||
*
|
||||
* ★ 关键实现点(全部实测得来,详见 scaffold/README.md):
|
||||
* 1. tryDetectVersion 必须先于 Bootstrap.bootStrap
|
||||
* 2. FolderData 顺序 (gameDir, resourcePackDir, assets, assetIndex),assetIndex 用落盘名
|
||||
* 3. 资源必须手工同步重载(各 listener 的 public reload()),不能 join Minecraft 的
|
||||
* 4. LoadingOverlay + ReceivingLevelScreen 都要清,否则渲出加载屏
|
||||
* 5. 每帧 mc.tick() 才能推进区块网格重建队列
|
||||
* 6. 读像素必须绑 FBO
|
||||
* 7. chunk 数据走官方 replaceWithPacketData(与网络包同路径)
|
||||
*/
|
||||
public class McfoldRenderServer {
|
||||
|
||||
static final Gson GSON = new Gson();
|
||||
static final PrintStream OUT = new PrintStream(
|
||||
new java.io.FileOutputStream(java.io.FileDescriptor.out), true, StandardCharsets.UTF_8);
|
||||
|
||||
static Minecraft mc;
|
||||
static ClientLevel level;
|
||||
static int width = 854, height = 480;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// stdout 只放协议 JSON,MC 的 INFO/WARN 全部赶到 stderr
|
||||
System.setOut(new PrintStream(new java.io.FileOutputStream(java.io.FileDescriptor.err), true));
|
||||
|
||||
String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default");
|
||||
String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets");
|
||||
String assetIdx = arg(args, "--assetIndex", "1.20.1");
|
||||
width = Integer.parseInt(arg(args, "--width", "854"));
|
||||
height = Integer.parseInt(arg(args, "--height", "480"));
|
||||
|
||||
try {
|
||||
boot(gameDir, assets, assetIdx);
|
||||
} catch (Throwable t) {
|
||||
emitError(null, "启动失败: " + t);
|
||||
t.printStackTrace(System.err);
|
||||
System.exit(1);
|
||||
}
|
||||
event("ready", null);
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
JsonObject req;
|
||||
try {
|
||||
req = JsonParser.parseString(line).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
emitError(null, "非法 JSON: " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
Integer id = req.has("id") ? req.get("id").getAsInt() : null;
|
||||
try {
|
||||
handle(id, req);
|
||||
} catch (Throwable t) {
|
||||
emitError(id, t.toString());
|
||||
t.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
// ---------------- 协议处理 ----------------
|
||||
|
||||
static void handle(Integer id, JsonObject req) throws Exception {
|
||||
String cmd = req.get("cmd").getAsString();
|
||||
switch (cmd) {
|
||||
case "shutdown": {
|
||||
JsonObject r = new JsonObject(); r.addProperty("ok", true);
|
||||
emit(id, r);
|
||||
System.exit(0);
|
||||
}
|
||||
case "chunk": {
|
||||
int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt();
|
||||
byte[] data = decode(req.get("data").getAsString());
|
||||
var resp = applyChunk(cx, cz, data);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.add("result", resp);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "chunks": {
|
||||
JsonArray items = req.getAsJsonArray("items");
|
||||
int ok = 0, failed = 0;
|
||||
String firstErr = null;
|
||||
for (JsonElement el : items) {
|
||||
JsonObject it = el.getAsJsonObject();
|
||||
try {
|
||||
applyChunk(it.get("cx").getAsInt(), it.get("cz").getAsInt(),
|
||||
decode(it.get("data").getAsString()));
|
||||
ok++;
|
||||
} catch (Throwable t) {
|
||||
failed++;
|
||||
if (firstErr == null) {
|
||||
firstErr = it.get("cx") + "," + it.get("cz") + ": " + t;
|
||||
}
|
||||
}
|
||||
}
|
||||
levelRendererAllChanged();
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("injected", ok);
|
||||
r.addProperty("failed", failed);
|
||||
if (firstErr != null) r.addProperty("firstError", firstErr);
|
||||
var cache2 = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource();
|
||||
r.addProperty("loadedChunks", cache2.getLoadedChunksCount());
|
||||
emit(id, r);
|
||||
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;
|
||||
float pitch = req.has("pitch") ? req.get("pitch").getAsFloat() : 0f;
|
||||
// ★ 必须 setPosRaw + 手动更新 chunk 追踪,否则实体仍属旧区块
|
||||
mc.player.setPosRaw(x, y, z);
|
||||
mc.player.xo = x; mc.player.yo = y; mc.player.zo = z;
|
||||
mc.player.setYRot(yaw);
|
||||
mc.player.setXRot(pitch);
|
||||
mc.player.yRotO = yaw;
|
||||
mc.player.xRotO = pitch;
|
||||
mc.setCameraEntity(mc.player);
|
||||
// 视野中心跟着相机走,否则目标区域区块被 ClientChunkCache 丢弃
|
||||
level.getChunkSource().updateViewCenter((int)Math.floor(x) >> 4, (int)Math.floor(z) >> 4);
|
||||
mc.levelRenderer.allChanged();
|
||||
tickFrames(5);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
JsonArray c = new JsonArray(); c.add(x); c.add(y); c.add(z);
|
||||
r.add("camera", c);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "view": {
|
||||
if (req.has("cameraType")) {
|
||||
mc.options.setCameraType(net.minecraft.client.CameraType.valueOf(req.get("cameraType").getAsString()));
|
||||
}
|
||||
if (req.has("renderDistance")) {
|
||||
int rd = req.get("renderDistance").getAsInt();
|
||||
// ★ 两处都要设:renderDistance 是本地设置;
|
||||
// serverRenderDistance 模拟服务端下发的视距上限
|
||||
// (getEffectiveRenderDistance = min(两者))
|
||||
mc.options.renderDistance().set(rd);
|
||||
mc.options.setServerRenderDistance(rd);
|
||||
// ★ 必须同步 ClientChunkCache 的 storage 半径,
|
||||
// 否则区块被 inRange 丢弃(官方靠服务端包触发 updateViewRadius)
|
||||
level.getChunkSource().updateViewRadius(rd);
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
if (req.has("fov")) {
|
||||
mc.options.fov().set(req.get("fov").getAsInt());
|
||||
}
|
||||
// ── 原版画质选项(光影的基础层)──
|
||||
if (req.has("graphicsMode")) {
|
||||
mc.options.graphicsMode().set(net.minecraft.client.GraphicsStatus
|
||||
.valueOf(req.get("graphicsMode").getAsString()));
|
||||
mc.levelRenderer.allChanged(); // FANCY 会启用更多渲染层
|
||||
}
|
||||
if (req.has("ambientOcclusion")) {
|
||||
mc.options.ambientOcclusion().set(req.get("ambientOcclusion").getAsBoolean());
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
if (req.has("mipmapLevels")) {
|
||||
mc.options.mipmapLevels().set(req.get("mipmapLevels").getAsInt());
|
||||
}
|
||||
if (req.has("gamma")) {
|
||||
mc.options.gamma().set(req.get("gamma").getAsDouble());
|
||||
}
|
||||
if (req.has("entityShadows")) {
|
||||
mc.options.entityShadows().set(req.get("entityShadows").getAsBoolean());
|
||||
}
|
||||
if (req.has("clouds")) {
|
||||
mc.options.cloudStatus().set(net.minecraft.client.CloudStatus
|
||||
.valueOf(req.get("clouds").getAsString()));
|
||||
}
|
||||
if (req.has("particles")) {
|
||||
mc.options.particles().set(net.minecraft.client.ParticleStatus
|
||||
.valueOf(req.get("particles").getAsString()));
|
||||
}
|
||||
// ── 原版后处理链(PostChain 基础设施)──
|
||||
if (req.has("postEffect")) {
|
||||
String eff = req.get("postEffect").getAsString();
|
||||
if (eff.isEmpty() || eff.equals("none")) {
|
||||
mc.gameRenderer.shutdownEffect();
|
||||
} else {
|
||||
// loadEffect 是包私有 → 走同包 bridge
|
||||
net.minecraft.client.renderer.McfoldBridge.loadEffect(
|
||||
mc.gameRenderer, new ResourceLocation(eff));
|
||||
}
|
||||
}
|
||||
// 回包带上真实生效值,便于验证
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
JsonObject cur = new JsonObject();
|
||||
try {
|
||||
java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class
|
||||
.getDeclaredField("postEffect");
|
||||
pf.setAccessible(true);
|
||||
cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none");
|
||||
} catch (Throwable ignore) {
|
||||
cur.addProperty("postEffect", "unknown");
|
||||
}
|
||||
cur.addProperty("graphicsMode", mc.options.graphicsMode().get().name());
|
||||
cur.addProperty("ambientOcclusion", mc.options.ambientOcclusion().get());
|
||||
cur.addProperty("mipmapLevels", mc.options.mipmapLevels().get());
|
||||
cur.addProperty("gamma", mc.options.gamma().get());
|
||||
cur.addProperty("entityShadows", mc.options.entityShadows().get());
|
||||
cur.addProperty("clouds", mc.options.cloudStatus().get().name());
|
||||
cur.addProperty("renderDistance", mc.options.getEffectiveRenderDistance());
|
||||
r.add("current", cur);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "registry": {
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("blockCount", BuiltInRegistries.BLOCK.size());
|
||||
r.addProperty("stateCount", Block.BLOCK_STATE_REGISTRY.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "lookup": {
|
||||
int sid = req.get("stateId").getAsInt();
|
||||
BlockState st = Block.stateById(sid);
|
||||
ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock());
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("stateId", sid);
|
||||
r.addProperty("block", key.toString());
|
||||
r.addProperty("state", st.toString());
|
||||
r.addProperty("inRange", sid >= 0 && sid < Block.BLOCK_STATE_REGISTRY.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "fog": {
|
||||
// 关雾:fogStart/fogEnd 是 levelRenderer 内部状态;
|
||||
// 更简单可靠的是加大 renderDistance + 用 BLOCK 光照
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "resolve": {
|
||||
// ★ 方块名(+属性) → state id。存档 palette 是名字,网络载荷要 id,
|
||||
// 而这个名字→id 的权威映射只有客户端注册表有。
|
||||
// 入参: {"cmd":"resolve","blocks":["minecraft:oak_log[axis=y]", ...]}
|
||||
JsonArray arr = req.getAsJsonArray("blocks");
|
||||
// ★ parseForBlock 要 HolderLookup(不是 Registry)
|
||||
var reg = mc.level.registryAccess().lookupOrThrow(Registries.BLOCK);
|
||||
JsonArray out = new JsonArray();
|
||||
int unknown = 0;
|
||||
for (JsonElement el : arr) {
|
||||
String spec = el.getAsString();
|
||||
int sid;
|
||||
try {
|
||||
var res = net.minecraft.commands.arguments.blocks.BlockStateParser
|
||||
.parseForBlock(reg, spec, false);
|
||||
sid = Block.getId(res.blockState());
|
||||
} catch (Throwable t) {
|
||||
sid = -1; unknown++;
|
||||
}
|
||||
out.add(sid);
|
||||
}
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.add("ids", out);
|
||||
r.addProperty("unknown", unknown);
|
||||
r.addProperty("total", arr.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "debug": {
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("playerPos", mc.player.getX() + "," + mc.player.getY() + "," + mc.player.getZ());
|
||||
int pcx = (int)Math.floor(mc.player.getX()) >> 4, pcz = (int)Math.floor(mc.player.getZ()) >> 4;
|
||||
r.addProperty("playerChunk", pcx + "," + pcz);
|
||||
var cc = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource();
|
||||
r.addProperty("loadedChunks", cc.getLoadedChunksCount());
|
||||
r.addProperty("chunksRendered", mc.levelRenderer.countRenderedChunks());
|
||||
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllChunks());
|
||||
var got = cc.getChunk(pcx, pcz, net.minecraft.world.level.chunk.ChunkStatus.FULL, false);
|
||||
r.addProperty("chunkAtPlayer", got != null ? got.getClass().getSimpleName() : "null");
|
||||
if (got != null) {
|
||||
// 扫描玩家所在列,找最高的非空气方块
|
||||
int top = Integer.MIN_VALUE;
|
||||
for (int y = level.getMaxBuildHeight() - 1; y >= level.getMinBuildHeight(); y--) {
|
||||
var st = got.getBlockState(new BlockPos(
|
||||
(int)Math.floor(mc.player.getX()) & 15, y, (int)Math.floor(mc.player.getZ()) & 15));
|
||||
if (!st.isAir()) {
|
||||
top = y;
|
||||
r.addProperty("topSolidY", y);
|
||||
r.addProperty("topSolidBlock", BuiltInRegistries.BLOCK.getKey(st.getBlock()).toString());
|
||||
r.addProperty("topSolidProps", st.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (top == Integer.MIN_VALUE) r.addProperty("topSolidY", "none");
|
||||
}
|
||||
// viewCenter
|
||||
r.addProperty("fogStart", String.valueOf(mc.options.renderDistance().get()));
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "viewport": {
|
||||
// ★ viewCenter 决定 ClientChunkCache 收哪些区块(超范围会被丢弃)
|
||||
int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt();
|
||||
if (req.has("radius")) {
|
||||
level.getChunkSource().updateViewRadius(req.get("radius").getAsInt());
|
||||
}
|
||||
level.getChunkSource().updateViewCenter(cx, cz);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("viewCenter", cx + "," + cz);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "render": {
|
||||
int frames = req.has("frames") ? req.get("frames").getAsInt() : 60;
|
||||
String out = req.has("out") ? req.get("out").getAsString() : null;
|
||||
if (req.has("width")) width = req.get("width").getAsInt();
|
||||
if (req.has("height")) height = req.get("height").getAsInt();
|
||||
render(id, frames, out);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
emitError(id, "未知命令: " + cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static JsonObject applyChunk(int cx, int cz, byte[] data) {
|
||||
// ★ 官方路径:ClientChunkCache.replaceWithPacketData
|
||||
// 内部:new LevelChunk + chunk.replaceWithPacketData(buf, heightmaps, consumer)
|
||||
// 与网络包 handleLevelChunkWithLight 完全同一路径
|
||||
FriendlyByteBuf buf = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data));
|
||||
var cache = level.getChunkSource();
|
||||
// 显式匿名类而非 lambda:lambda 参数类型推断在此处会失败(实测)
|
||||
net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput beOut =
|
||||
new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput() {
|
||||
@Override public void accept(BlockPos bp,
|
||||
net.minecraft.world.level.block.entity.BlockEntityType<?> type,
|
||||
net.minecraft.nbt.CompoundTag tag) { }
|
||||
};
|
||||
// ★ 参数是 Consumer<BlockEntityTagOutput>(消费这个接口本身),不是该接口的方法签名。
|
||||
// LevelChunk.replaceWithPacketData 内部会调 beOut.accept(...),这里直接空实现。
|
||||
cache.replaceWithPacketData(cx, cz, buf, new net.minecraft.nbt.CompoundTag(), out -> {});
|
||||
// 无光照数据 ⇒ 显式全亮,否则地形全黑
|
||||
var light = cache.getLightEngine();
|
||||
for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) {
|
||||
DataLayer sky = new DataLayer();
|
||||
for (int x = 0; x < 16; x++)
|
||||
for (int y = 0; y < 16; y++)
|
||||
for (int z = 0; z < 16; z++) sky.set(x, y, z, 15);
|
||||
light.queueSectionData(LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
light.queueSectionData(LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
}
|
||||
light.setLightEnabled(new ChunkPos(cx, cz), true);
|
||||
int upd = light.runLightUpdates();
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("lightUpdates", upd);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void levelRendererAllChanged() {
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
|
||||
/** tick + 渲染各帧(区块网格重建队列靠 tick 推进) */
|
||||
static void tickFrames(int n) {
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
for (int f = 0; f < n; f++) {
|
||||
try { mc.tick(); } catch (Throwable ignore) {}
|
||||
RenderSystem.clear(16640, Minecraft.ON_OSX);
|
||||
target.bindWrite(true);
|
||||
mc.gameRenderer.render(0.0F, (long) f * 16_000_000L, true);
|
||||
target.unbindWrite();
|
||||
target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
mc.getWindow().updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
static void render(Integer id, int frames, String out) throws Exception {
|
||||
tickFrames(frames);
|
||||
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight();
|
||||
org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, target.frameBufferId);
|
||||
GL11.glReadBuffer(org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0);
|
||||
var buf = org.lwjgl.BufferUtils.createByteBuffer(w * h * 4);
|
||||
GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
|
||||
org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, 0);
|
||||
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
java.util.Set<Integer> colors = new java.util.HashSet<>();
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
int i = ((h - 1 - y) * w + x) * 4;
|
||||
int rgb = ((buf.get(i) & 0xFF) << 16) | ((buf.get(i + 1) & 0xFF) << 8) | (buf.get(i + 2) & 0xFF);
|
||||
img.setRGB(x, y, rgb);
|
||||
colors.add(rgb);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "png", bos);
|
||||
byte[] png = bos.toByteArray();
|
||||
if (out != null) ImageIO.write(img, "png", new File(out));
|
||||
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("width", w);
|
||||
r.addProperty("height", h);
|
||||
r.addProperty("chunksRendered", mc.levelRenderer.countRenderedChunks());
|
||||
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllChunks());
|
||||
r.addProperty("distinctColors", colors.size());
|
||||
JsonArray cam = new JsonArray();
|
||||
cam.add(mc.player.getX()); cam.add(mc.player.getY()); cam.add(mc.player.getZ());
|
||||
r.add("camera", cam);
|
||||
if (out != null) r.addProperty("path", out);
|
||||
r.addProperty("image", Base64.getEncoder().encodeToString(png));
|
||||
emit(id, r);
|
||||
}
|
||||
|
||||
// ---------------- 启动 ----------------
|
||||
|
||||
static void boot(String gameDir, String assets, String assetIdx) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
runDir.mkdirs();
|
||||
User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam",
|
||||
Optional.empty(), Optional.empty(), User.Type.LEGACY);
|
||||
PropertyMap props = new PropertyMap();
|
||||
GameConfig cfg = new GameConfig(
|
||||
new GameConfig.UserData(user, props, props, Proxy.NO_PROXY),
|
||||
new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false),
|
||||
new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"),
|
||||
new File(assets), assetIdx),
|
||||
new GameConfig.GameData(false, "mcfold", "release", false, false),
|
||||
new GameConfig.QuickPlayData(null, null, null, null));
|
||||
|
||||
RenderSystem.initRenderThread();
|
||||
RenderSystem.beginInitialization();
|
||||
mc = new Minecraft(cfg);
|
||||
RenderSystem.finishInitialization();
|
||||
System.err.println("[mcfold] Minecraft constructed");
|
||||
|
||||
reloadResources(mc);
|
||||
System.err.println("[mcfold] resources reloaded");
|
||||
|
||||
Connection conn = new Connection(PacketFlow.CLIENTBOUND);
|
||||
GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam");
|
||||
WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager(
|
||||
TelemetryEventSender.DISABLED, false, Duration.ZERO, "");
|
||||
ClientPacketListener listener = new ClientPacketListener(mc, null, conn, null, profile, telemetry);
|
||||
|
||||
var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository(
|
||||
new File(runDir, "datapacks").toPath());
|
||||
packs.reload();
|
||||
ResourceManager dataRm = new net.minecraft.server.packs.resources.MultiPackResourceManager(
|
||||
net.minecraft.server.packs.PackType.SERVER_DATA,
|
||||
java.util.List.of(packs.getPack("vanilla").open()));
|
||||
var all = new java.util.ArrayList<RegistryDataLoader.RegistryData<?>>();
|
||||
all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES);
|
||||
all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES);
|
||||
RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, listener.registryAccess(), all);
|
||||
var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess()
|
||||
.replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE,
|
||||
new RegistryAccess.Frozen[]{loaded});
|
||||
var rf = ClientPacketListener.class.getDeclaredField("registryAccess");
|
||||
rf.setAccessible(true);
|
||||
rf.set(listener, layered);
|
||||
|
||||
Holder<DimensionType> dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE)
|
||||
.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld")));
|
||||
var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false);
|
||||
level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder,
|
||||
Math.max(8, mc.options.renderDistance().get()), 8,
|
||||
() -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L);
|
||||
mc.setLevel(level);
|
||||
|
||||
var player = new net.minecraft.client.player.LocalPlayer(mc, level, listener,
|
||||
new net.minecraft.stats.StatsCounter(), new net.minecraft.client.ClientRecipeBook(), false, false);
|
||||
player.setPos(8.0, 70.0, 8.0);
|
||||
mc.player = player;
|
||||
mc.setCameraEntity(player);
|
||||
mc.gameMode = new net.minecraft.client.multiplayer.MultiPlayerGameMode(mc, listener);
|
||||
|
||||
net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager());
|
||||
|
||||
mc.setOverlay(null);
|
||||
mc.setScreen(null);
|
||||
mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);
|
||||
System.err.println("[mcfold] boot done");
|
||||
}
|
||||
|
||||
/** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/
|
||||
static void reloadResources(Minecraft mc) throws Exception {
|
||||
var rmF = Minecraft.class.getDeclaredField("resourceManager");
|
||||
rmF.setAccessible(true);
|
||||
var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc);
|
||||
mc.getResourcePackRepository().reload();
|
||||
|
||||
var barrier = new net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier() {
|
||||
@Override public <T> java.util.concurrent.CompletableFuture<T> wait(T t) {
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(t);
|
||||
}
|
||||
};
|
||||
var prof = InactiveProfiler.INSTANCE;
|
||||
java.util.concurrent.Executor sync = Runnable::run;
|
||||
|
||||
for (var listener : reloadListeners(rm)) {
|
||||
String name = listener.getName();
|
||||
try {
|
||||
var fut = listener.reload(barrier, rm, prof, prof, sync, sync);
|
||||
long deadline = System.currentTimeMillis() + 60_000;
|
||||
while (!fut.isDone() && System.currentTimeMillis() < deadline) Thread.sleep(10);
|
||||
if (fut.isDone()) fut.join();
|
||||
else System.err.println("[mcfold] TIMEOUT " + name);
|
||||
} catch (Throwable e) {
|
||||
System.err.println("[mcfold] FAILED " + name + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static java.util.List<net.minecraft.server.packs.resources.PreparableReloadListener> reloadListeners(Object rm) throws Exception {
|
||||
for (var f : rm.getClass().getDeclaredFields()) {
|
||||
if (java.util.List.class.isAssignableFrom(f.getType())) {
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(rm);
|
||||
if (v instanceof java.util.List<?> l && !l.isEmpty()
|
||||
&& l.get(0) instanceof net.minecraft.server.packs.resources.PreparableReloadListener) {
|
||||
return (java.util.List<net.minecraft.server.packs.resources.PreparableReloadListener>) l;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("reload listeners not found");
|
||||
}
|
||||
|
||||
// ---------------- 工具 ----------------
|
||||
|
||||
static byte[] decode(String b64) {
|
||||
return Base64.getDecoder().decode(b64);
|
||||
}
|
||||
|
||||
static String arg(String[] a, String key, String def) {
|
||||
for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1];
|
||||
return def;
|
||||
}
|
||||
|
||||
static synchronized void emit(Integer id, JsonObject obj) {
|
||||
if (id != null) obj.addProperty("id", id);
|
||||
OUT.println(GSON.toJson(obj));
|
||||
OUT.flush();
|
||||
}
|
||||
|
||||
static void emitError(Integer id, String msg) {
|
||||
JsonObject o = new JsonObject();
|
||||
o.addProperty("ok", false);
|
||||
o.addProperty("error", msg);
|
||||
emit(id, o);
|
||||
}
|
||||
|
||||
static void event(String name, JsonObject payload) {
|
||||
JsonObject o = new JsonObject();
|
||||
o.addProperty("event", name);
|
||||
if (payload != null) o.add("payload", payload);
|
||||
OUT.println(GSON.toJson(o));
|
||||
OUT.flush();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package net.minecraft.client.renderer;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
|
||||
/**
|
||||
* 包私有成员桥。
|
||||
*
|
||||
* GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用;
|
||||
* 我们不跑主循环、不连服,所以要用同包类直接调。
|
||||
* reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调
|
||||
* loadEffect —— 官方由 onResourceManagerReload / 药水效果改
|
||||
*/
|
||||
public class McfoldBridge {
|
||||
public static void reloadShaders(GameRenderer gr, ResourceProvider provider) {
|
||||
gr.reloadShaders(provider);
|
||||
}
|
||||
|
||||
/** 载入一个原版后处理链(如 minecraft:shaders/post/invert.json)。 */
|
||||
public static void loadEffect(GameRenderer gr, ResourceLocation id) {
|
||||
gr.loadEffect(id);
|
||||
}
|
||||
}
|
||||
676
scaffold/java/gen_1_21/McfoldRenderServer.java
Normal file
676
scaffold/java/gen_1_21/McfoldRenderServer.java
Normal file
@ -0,0 +1,676 @@
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.mojang.blaze3d.pipeline.MainTarget;
|
||||
import com.mojang.blaze3d.platform.DisplayData;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.PropertyMap;
|
||||
import net.minecraft.SharedConstants;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.User;
|
||||
import net.minecraft.client.main.GameConfig;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.client.telemetry.TelemetryEventSender;
|
||||
import net.minecraft.client.telemetry.WorldSessionTelemetryManager;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.Connection;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.protocol.PacketFlow;
|
||||
import net.minecraft.resources.RegistryDataLoader;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.profiling.InactiveProfiler;
|
||||
import net.minecraft.world.Difficulty;
|
||||
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.BlockState;
|
||||
import net.minecraft.world.level.chunk.DataLayer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。
|
||||
*
|
||||
* 设计约束(用户 2026-09-25):
|
||||
* - 不起完整客户端:不跑主循环、不连服、不登录、不作玩家
|
||||
* - 我们的世界数据喂给官方渲染器出图
|
||||
*
|
||||
* 协议(stdin/stdout 各一行一个 JSON):
|
||||
* → {"id":1,"cmd":"chunk","cx":0,"cz":0,"data":"<b64>","heightmaps":null}
|
||||
* → {"id":2,"cmd":"camera","x":8,"y":70,"z":8,"yaw":0,"pitch":0}
|
||||
* → {"id":3,"cmd":"render","frames":60,"out":"/tmp/a.png"}
|
||||
* ← {"id":3,"ok":true,"width":854,"height":480,"chunksRendered":29,
|
||||
* "distinctColors":1450,"image":"<b64 png>"}
|
||||
*
|
||||
* 启动就绪时主动发 {"event":"ready"}。
|
||||
*
|
||||
* ★ 关键实现点(全部实测得来,详见 scaffold/README.md):
|
||||
* 1. tryDetectVersion 必须先于 Bootstrap.bootStrap
|
||||
* 2. FolderData 顺序 (gameDir, resourcePackDir, assets, assetIndex),assetIndex 用落盘名
|
||||
* 3. 资源必须手工同步重载(各 listener 的 public reload()),不能 join Minecraft 的
|
||||
* 4. LoadingOverlay + ReceivingLevelScreen 都要清,否则渲出加载屏
|
||||
* 5. 每帧 mc.tick() 才能推进区块网格重建队列
|
||||
* 6. 读像素必须绑 FBO
|
||||
* 7. chunk 数据走官方 replaceWithPacketData(与网络包同路径)
|
||||
*/
|
||||
public class McfoldRenderServer {
|
||||
|
||||
static final Gson GSON = new Gson();
|
||||
static final PrintStream OUT = new PrintStream(
|
||||
new java.io.FileOutputStream(java.io.FileDescriptor.out), true, StandardCharsets.UTF_8);
|
||||
|
||||
static Minecraft mc;
|
||||
static ClientLevel level;
|
||||
static int width = 854, height = 480;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// stdout 只放协议 JSON,MC 的 INFO/WARN 全部赶到 stderr
|
||||
System.setOut(new PrintStream(new java.io.FileOutputStream(java.io.FileDescriptor.err), true));
|
||||
|
||||
String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default");
|
||||
String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets");
|
||||
String assetIdx = arg(args, "--assetIndex", "1.20.1");
|
||||
width = Integer.parseInt(arg(args, "--width", "854"));
|
||||
height = Integer.parseInt(arg(args, "--height", "480"));
|
||||
|
||||
try {
|
||||
boot(gameDir, assets, assetIdx);
|
||||
} catch (Throwable t) {
|
||||
emitError(null, "启动失败: " + t);
|
||||
t.printStackTrace(System.err);
|
||||
System.exit(1);
|
||||
}
|
||||
event("ready", null);
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) continue;
|
||||
JsonObject req;
|
||||
try {
|
||||
req = JsonParser.parseString(line).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
emitError(null, "非法 JSON: " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
Integer id = req.has("id") ? req.get("id").getAsInt() : null;
|
||||
try {
|
||||
handle(id, req);
|
||||
} catch (Throwable t) {
|
||||
emitError(id, t.toString());
|
||||
t.printStackTrace(System.err);
|
||||
}
|
||||
}
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
// ---------------- 协议处理 ----------------
|
||||
|
||||
static void handle(Integer id, JsonObject req) throws Exception {
|
||||
String cmd = req.get("cmd").getAsString();
|
||||
switch (cmd) {
|
||||
case "shutdown": {
|
||||
JsonObject r = new JsonObject(); r.addProperty("ok", true);
|
||||
emit(id, r);
|
||||
System.exit(0);
|
||||
}
|
||||
case "chunk": {
|
||||
int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt();
|
||||
byte[] data = decode(req.get("data").getAsString());
|
||||
var resp = applyChunk(cx, cz, data);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.add("result", resp);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "chunks": {
|
||||
JsonArray items = req.getAsJsonArray("items");
|
||||
int ok = 0, failed = 0;
|
||||
String firstErr = null;
|
||||
for (JsonElement el : items) {
|
||||
JsonObject it = el.getAsJsonObject();
|
||||
try {
|
||||
applyChunk(it.get("cx").getAsInt(), it.get("cz").getAsInt(),
|
||||
decode(it.get("data").getAsString()));
|
||||
ok++;
|
||||
} catch (Throwable t) {
|
||||
failed++;
|
||||
if (firstErr == null) {
|
||||
firstErr = it.get("cx") + "," + it.get("cz") + ": " + t;
|
||||
}
|
||||
}
|
||||
}
|
||||
levelRendererAllChanged();
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("injected", ok);
|
||||
r.addProperty("failed", failed);
|
||||
if (firstErr != null) r.addProperty("firstError", firstErr);
|
||||
var cache2 = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource();
|
||||
r.addProperty("loadedChunks", cache2.getLoadedChunksCount());
|
||||
emit(id, r);
|
||||
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;
|
||||
float pitch = req.has("pitch") ? req.get("pitch").getAsFloat() : 0f;
|
||||
// ★ 必须 setPosRaw + 手动更新 chunk 追踪,否则实体仍属旧区块
|
||||
mc.player.setPosRaw(x, y, z);
|
||||
mc.player.xo = x; mc.player.yo = y; mc.player.zo = z;
|
||||
mc.player.setYRot(yaw);
|
||||
mc.player.setXRot(pitch);
|
||||
mc.player.yRotO = yaw;
|
||||
mc.player.xRotO = pitch;
|
||||
mc.setCameraEntity(mc.player);
|
||||
// 视野中心跟着相机走,否则目标区域区块被 ClientChunkCache 丢弃
|
||||
level.getChunkSource().updateViewCenter((int)Math.floor(x) >> 4, (int)Math.floor(z) >> 4);
|
||||
mc.levelRenderer.allChanged();
|
||||
tickFrames(5);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
JsonArray c = new JsonArray(); c.add(x); c.add(y); c.add(z);
|
||||
r.add("camera", c);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "view": {
|
||||
if (req.has("cameraType")) {
|
||||
mc.options.setCameraType(net.minecraft.client.CameraType.valueOf(req.get("cameraType").getAsString()));
|
||||
}
|
||||
if (req.has("renderDistance")) {
|
||||
int rd = req.get("renderDistance").getAsInt();
|
||||
// ★ 两处都要设:renderDistance 是本地设置;
|
||||
// serverRenderDistance 模拟服务端下发的视距上限
|
||||
// (getEffectiveRenderDistance = min(两者))
|
||||
mc.options.renderDistance().set(rd);
|
||||
mc.options.setServerRenderDistance(rd);
|
||||
// ★ 必须同步 ClientChunkCache 的 storage 半径,
|
||||
// 否则区块被 inRange 丢弃(官方靠服务端包触发 updateViewRadius)
|
||||
level.getChunkSource().updateViewRadius(rd);
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
if (req.has("fov")) {
|
||||
mc.options.fov().set(req.get("fov").getAsInt());
|
||||
}
|
||||
// ── 原版画质选项(光影的基础层)──
|
||||
if (req.has("graphicsMode")) {
|
||||
mc.options.graphicsMode().set(net.minecraft.client.GraphicsStatus
|
||||
.valueOf(req.get("graphicsMode").getAsString()));
|
||||
mc.levelRenderer.allChanged(); // FANCY 会启用更多渲染层
|
||||
}
|
||||
if (req.has("ambientOcclusion")) {
|
||||
mc.options.ambientOcclusion().set(req.get("ambientOcclusion").getAsBoolean());
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
if (req.has("mipmapLevels")) {
|
||||
mc.options.mipmapLevels().set(req.get("mipmapLevels").getAsInt());
|
||||
}
|
||||
if (req.has("gamma")) {
|
||||
mc.options.gamma().set(req.get("gamma").getAsDouble());
|
||||
}
|
||||
if (req.has("entityShadows")) {
|
||||
mc.options.entityShadows().set(req.get("entityShadows").getAsBoolean());
|
||||
}
|
||||
if (req.has("clouds")) {
|
||||
mc.options.cloudStatus().set(net.minecraft.client.CloudStatus
|
||||
.valueOf(req.get("clouds").getAsString()));
|
||||
}
|
||||
if (req.has("particles")) {
|
||||
mc.options.particles().set(net.minecraft.client.ParticleStatus
|
||||
.valueOf(req.get("particles").getAsString()));
|
||||
}
|
||||
// ── 原版后处理链(PostChain 基础设施)──
|
||||
if (req.has("postEffect")) {
|
||||
String eff = req.get("postEffect").getAsString();
|
||||
if (eff.isEmpty() || eff.equals("none")) {
|
||||
mc.gameRenderer.shutdownEffect();
|
||||
} else {
|
||||
// loadEffect 是包私有 → 走同包 bridge
|
||||
net.minecraft.client.renderer.McfoldBridge.loadEffect(
|
||||
mc.gameRenderer, ResourceLocation.parse(eff));
|
||||
}
|
||||
}
|
||||
// 回包带上真实生效值,便于验证
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
JsonObject cur = new JsonObject();
|
||||
try {
|
||||
java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class
|
||||
.getDeclaredField("postEffect");
|
||||
pf.setAccessible(true);
|
||||
cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none");
|
||||
} catch (Throwable ignore) {
|
||||
cur.addProperty("postEffect", "unknown");
|
||||
}
|
||||
cur.addProperty("graphicsMode", mc.options.graphicsMode().get().name());
|
||||
cur.addProperty("ambientOcclusion", mc.options.ambientOcclusion().get());
|
||||
cur.addProperty("mipmapLevels", mc.options.mipmapLevels().get());
|
||||
cur.addProperty("gamma", mc.options.gamma().get());
|
||||
cur.addProperty("entityShadows", mc.options.entityShadows().get());
|
||||
cur.addProperty("clouds", mc.options.cloudStatus().get().name());
|
||||
cur.addProperty("renderDistance", mc.options.getEffectiveRenderDistance());
|
||||
r.add("current", cur);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "registry": {
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("blockCount", BuiltInRegistries.BLOCK.size());
|
||||
r.addProperty("stateCount", Block.BLOCK_STATE_REGISTRY.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "lookup": {
|
||||
int sid = req.get("stateId").getAsInt();
|
||||
BlockState st = Block.stateById(sid);
|
||||
ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock());
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("stateId", sid);
|
||||
r.addProperty("block", key.toString());
|
||||
r.addProperty("state", st.toString());
|
||||
r.addProperty("inRange", sid >= 0 && sid < Block.BLOCK_STATE_REGISTRY.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "fog": {
|
||||
// 关雾:fogStart/fogEnd 是 levelRenderer 内部状态;
|
||||
// 更简单可靠的是加大 renderDistance + 用 BLOCK 光照
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "resolve": {
|
||||
// ★ 方块名(+属性) → state id。存档 palette 是名字,网络载荷要 id,
|
||||
// 而这个名字→id 的权威映射只有客户端注册表有。
|
||||
// 入参: {"cmd":"resolve","blocks":["minecraft:oak_log[axis=y]", ...]}
|
||||
JsonArray arr = req.getAsJsonArray("blocks");
|
||||
// ★ parseForBlock 要 HolderLookup(不是 Registry)
|
||||
var reg = mc.level.registryAccess().lookupOrThrow(Registries.BLOCK);
|
||||
JsonArray out = new JsonArray();
|
||||
int unknown = 0;
|
||||
for (JsonElement el : arr) {
|
||||
String spec = el.getAsString();
|
||||
int sid;
|
||||
try {
|
||||
var res = net.minecraft.commands.arguments.blocks.BlockStateParser
|
||||
.parseForBlock(reg, spec, false);
|
||||
sid = Block.getId(res.blockState());
|
||||
} catch (Throwable t) {
|
||||
sid = -1; unknown++;
|
||||
}
|
||||
out.add(sid);
|
||||
}
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.add("ids", out);
|
||||
r.addProperty("unknown", unknown);
|
||||
r.addProperty("total", arr.size());
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "debug": {
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("playerPos", mc.player.getX() + "," + mc.player.getY() + "," + mc.player.getZ());
|
||||
int pcx = (int)Math.floor(mc.player.getX()) >> 4, pcz = (int)Math.floor(mc.player.getZ()) >> 4;
|
||||
r.addProperty("playerChunk", pcx + "," + pcz);
|
||||
var cc = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource();
|
||||
r.addProperty("loadedChunks", cc.getLoadedChunksCount());
|
||||
r.addProperty("chunksRendered", mc.levelRenderer.countRenderedSections());
|
||||
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections());
|
||||
var got = cc.getChunk(pcx, pcz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false);
|
||||
r.addProperty("chunkAtPlayer", got != null ? got.getClass().getSimpleName() : "null");
|
||||
if (got != null) {
|
||||
// 扫描玩家所在列,找最高的非空气方块
|
||||
int top = Integer.MIN_VALUE;
|
||||
for (int y = level.getMaxBuildHeight() - 1; y >= level.getMinBuildHeight(); y--) {
|
||||
var st = got.getBlockState(new BlockPos(
|
||||
(int)Math.floor(mc.player.getX()) & 15, y, (int)Math.floor(mc.player.getZ()) & 15));
|
||||
if (!st.isAir()) {
|
||||
top = y;
|
||||
r.addProperty("topSolidY", y);
|
||||
r.addProperty("topSolidBlock", BuiltInRegistries.BLOCK.getKey(st.getBlock()).toString());
|
||||
r.addProperty("topSolidProps", st.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (top == Integer.MIN_VALUE) r.addProperty("topSolidY", "none");
|
||||
}
|
||||
// viewCenter
|
||||
r.addProperty("fogStart", String.valueOf(mc.options.renderDistance().get()));
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "viewport": {
|
||||
// ★ viewCenter 决定 ClientChunkCache 收哪些区块(超范围会被丢弃)
|
||||
int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt();
|
||||
if (req.has("radius")) {
|
||||
level.getChunkSource().updateViewRadius(req.get("radius").getAsInt());
|
||||
}
|
||||
level.getChunkSource().updateViewCenter(cx, cz);
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("viewCenter", cx + "," + cz);
|
||||
emit(id, r);
|
||||
break;
|
||||
}
|
||||
case "render": {
|
||||
int frames = req.has("frames") ? req.get("frames").getAsInt() : 60;
|
||||
String out = req.has("out") ? req.get("out").getAsString() : null;
|
||||
if (req.has("width")) width = req.get("width").getAsInt();
|
||||
if (req.has("height")) height = req.get("height").getAsInt();
|
||||
render(id, frames, out);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
emitError(id, "未知命令: " + cmd);
|
||||
}
|
||||
}
|
||||
|
||||
static JsonObject applyChunk(int cx, int cz, byte[] data) {
|
||||
// ★ 官方路径:ClientChunkCache.replaceWithPacketData
|
||||
// 内部:new LevelChunk + chunk.replaceWithPacketData(buf, heightmaps, consumer)
|
||||
// 与网络包 handleLevelChunkWithLight 完全同一路径
|
||||
FriendlyByteBuf buf = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data));
|
||||
var cache = level.getChunkSource();
|
||||
// 显式匿名类而非 lambda:lambda 参数类型推断在此处会失败(实测)
|
||||
net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput beOut =
|
||||
new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput() {
|
||||
@Override public void accept(BlockPos bp,
|
||||
net.minecraft.world.level.block.entity.BlockEntityType<?> type,
|
||||
net.minecraft.nbt.CompoundTag tag) { }
|
||||
};
|
||||
// ★ 参数是 Consumer<BlockEntityTagOutput>(消费这个接口本身),不是该接口的方法签名。
|
||||
// LevelChunk.replaceWithPacketData 内部会调 beOut.accept(...),这里直接空实现。
|
||||
cache.replaceWithPacketData(cx, cz, buf, new net.minecraft.nbt.CompoundTag(), out -> {});
|
||||
// 无光照数据 ⇒ 显式全亮,否则地形全黑
|
||||
var light = cache.getLightEngine();
|
||||
for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) {
|
||||
DataLayer sky = new DataLayer();
|
||||
for (int x = 0; x < 16; x++)
|
||||
for (int y = 0; y < 16; y++)
|
||||
for (int z = 0; z < 16; z++) sky.set(x, y, z, 15);
|
||||
light.queueSectionData(LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
light.queueSectionData(LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
}
|
||||
light.setLightEnabled(new ChunkPos(cx, cz), true);
|
||||
int upd = light.runLightUpdates();
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("lightUpdates", upd);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void levelRendererAllChanged() {
|
||||
mc.levelRenderer.allChanged();
|
||||
}
|
||||
|
||||
/** tick + 渲染各帧(区块网格重建队列靠 tick 推进) */
|
||||
static void tickFrames(int n) {
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
for (int f = 0; f < n; f++) {
|
||||
try { mc.tick(); } catch (Throwable ignore) {}
|
||||
RenderSystem.clear(16640, Minecraft.ON_OSX);
|
||||
target.bindWrite(true);
|
||||
// ★ 世代差异(1.21+):render(DeltaTracker, boolean)
|
||||
mc.gameRenderer.render(mc.getTimer(), true);
|
||||
target.unbindWrite();
|
||||
target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
mc.getWindow().updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
static void render(Integer id, int frames, String out) throws Exception {
|
||||
tickFrames(frames);
|
||||
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight();
|
||||
org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, target.frameBufferId);
|
||||
GL11.glReadBuffer(org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0);
|
||||
var buf = org.lwjgl.BufferUtils.createByteBuffer(w * h * 4);
|
||||
GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
|
||||
org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, 0);
|
||||
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
java.util.Set<Integer> colors = new java.util.HashSet<>();
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++) {
|
||||
int i = ((h - 1 - y) * w + x) * 4;
|
||||
int rgb = ((buf.get(i) & 0xFF) << 16) | ((buf.get(i + 1) & 0xFF) << 8) | (buf.get(i + 2) & 0xFF);
|
||||
img.setRGB(x, y, rgb);
|
||||
colors.add(rgb);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "png", bos);
|
||||
byte[] png = bos.toByteArray();
|
||||
if (out != null) ImageIO.write(img, "png", new File(out));
|
||||
|
||||
JsonObject r = new JsonObject();
|
||||
r.addProperty("ok", true);
|
||||
r.addProperty("width", w);
|
||||
r.addProperty("height", h);
|
||||
r.addProperty("chunksRendered", mc.levelRenderer.countRenderedSections());
|
||||
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections());
|
||||
r.addProperty("distinctColors", colors.size());
|
||||
JsonArray cam = new JsonArray();
|
||||
cam.add(mc.player.getX()); cam.add(mc.player.getY()); cam.add(mc.player.getZ());
|
||||
r.add("camera", cam);
|
||||
if (out != null) r.addProperty("path", out);
|
||||
r.addProperty("image", Base64.getEncoder().encodeToString(png));
|
||||
emit(id, r);
|
||||
}
|
||||
|
||||
// ---------------- 启动 ----------------
|
||||
|
||||
static void boot(String gameDir, String assets, String assetIdx) throws Exception {
|
||||
SharedConstants.tryDetectVersion();
|
||||
// ★ 世代差异(1.21+):enableDataFixerOptimizations() 已被移除(实测映射表无此方法)
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
runDir.mkdirs();
|
||||
// ★ 世代差异(1.21+):User(String, UUID, String, Optional, Optional, Type)
|
||||
// 1.20.x 是 (String, String sessionId, String, ...)
|
||||
User user = new User("mcfold_cam", java.util.UUID.nameUUIDFromBytes("mcfold_cam".getBytes()),
|
||||
"mcfold_cam", Optional.empty(), Optional.empty(), User.Type.LEGACY);
|
||||
PropertyMap props = new PropertyMap();
|
||||
GameConfig cfg = new GameConfig(
|
||||
new GameConfig.UserData(user, props, props, Proxy.NO_PROXY),
|
||||
new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false),
|
||||
new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"),
|
||||
new File(assets), assetIdx),
|
||||
new GameConfig.GameData(false, "mcfold", "release", false, false),
|
||||
new GameConfig.QuickPlayData(null, null, null, null));
|
||||
|
||||
RenderSystem.initRenderThread();
|
||||
RenderSystem.beginInitialization();
|
||||
mc = new Minecraft(cfg);
|
||||
RenderSystem.finishInitialization();
|
||||
System.err.println("[mcfold] Minecraft constructed");
|
||||
|
||||
reloadResources(mc);
|
||||
System.err.println("[mcfold] resources reloaded");
|
||||
|
||||
Connection conn = new Connection(PacketFlow.CLIENTBOUND);
|
||||
GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam");
|
||||
// 初始注册表:官方静态层(1.21 的 cookie 需要它,随后会被真实注册表替换)
|
||||
net.minecraft.core.RegistryAccess.Frozen listenerRegistryAccess =
|
||||
net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess()
|
||||
.compositeAccess();
|
||||
WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager(
|
||||
TelemetryEventSender.DISABLED, false, Duration.ZERO, "");
|
||||
// ★ 世代差异(1.21+):ClientPacketListener(Minecraft, Connection, CommonListenerCookie)
|
||||
// 1.20.x 是 (Minecraft, Screen, Connection, ServerData, GameProfile, TelemetryManager)。
|
||||
// ★★ 这是本世代最大的一处变化 —— 参数个数与语义都不同。
|
||||
var cookie = new net.minecraft.client.multiplayer.CommonListenerCookie(
|
||||
profile,
|
||||
telemetry,
|
||||
listenerRegistryAccess, // 后面注册表加载后会被替换
|
||||
net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS,
|
||||
null, // serverBrand
|
||||
null, // serverData
|
||||
null, // postDisconnectScreen
|
||||
java.util.Map.of(), // serverCookies
|
||||
null, // chatState
|
||||
false, // strictErrorHandling
|
||||
java.util.Map.of(), // customReportDetails
|
||||
net.minecraft.server.ServerLinks.EMPTY);
|
||||
ClientPacketListener listener = new ClientPacketListener(mc, conn, cookie);
|
||||
|
||||
// ★ 世代差异(1.21+):createPackRepository(Path, DirectoryValidator)
|
||||
// DirectoryValidator 没有 NONE 常量,构造要 PathMatcher;
|
||||
// 传一个「全部拒绝」的 matcher 最安全(我们不加载外来符号链接数据包)
|
||||
var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository(
|
||||
new File(runDir, "datapacks").toPath(),
|
||||
new net.minecraft.world.level.validation.DirectoryValidator(p -> false));
|
||||
packs.reload();
|
||||
ResourceManager dataRm = new net.minecraft.server.packs.resources.MultiPackResourceManager(
|
||||
net.minecraft.server.packs.PackType.SERVER_DATA,
|
||||
java.util.List.of(packs.getPack("vanilla").open()));
|
||||
var all = new java.util.ArrayList<RegistryDataLoader.RegistryData<?>>();
|
||||
all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES);
|
||||
all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES);
|
||||
RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, listener.registryAccess(), all);
|
||||
var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess()
|
||||
.replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE,
|
||||
new RegistryAccess.Frozen[]{loaded});
|
||||
var rf = ClientPacketListener.class.getDeclaredField("registryAccess");
|
||||
rf.setAccessible(true);
|
||||
rf.set(listener, layered);
|
||||
|
||||
Holder<DimensionType> dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE)
|
||||
.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, ResourceLocation.parse("minecraft:overworld")));
|
||||
var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false);
|
||||
level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder,
|
||||
Math.max(8, mc.options.renderDistance().get()), 8,
|
||||
() -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L);
|
||||
// ★ 世代差异(1.21+):setLevel(ClientLevel, ReceivingLevelScreen$Reason)
|
||||
// 1.20.x 只有一个参数。这是本文件与 gen_1_20 的差异之一。
|
||||
// 枚举值实测:NETHER_PORTAL / END_PORTAL / OTHER(没有 LEVEL_LOAD)
|
||||
// 我们不是经传送门进入世界 ⇒ OTHER
|
||||
mc.setLevel(level, net.minecraft.client.gui.screens.ReceivingLevelScreen.Reason.OTHER);
|
||||
|
||||
var player = new net.minecraft.client.player.LocalPlayer(mc, level, listener,
|
||||
new net.minecraft.stats.StatsCounter(), new net.minecraft.client.ClientRecipeBook(), false, false);
|
||||
player.setPos(8.0, 70.0, 8.0);
|
||||
mc.player = player;
|
||||
mc.setCameraEntity(player);
|
||||
mc.gameMode = new net.minecraft.client.multiplayer.MultiPlayerGameMode(mc, listener);
|
||||
|
||||
net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager());
|
||||
|
||||
mc.setOverlay(null);
|
||||
mc.setScreen(null);
|
||||
mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);
|
||||
System.err.println("[mcfold] boot done");
|
||||
}
|
||||
|
||||
/** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/
|
||||
static void reloadResources(Minecraft mc) throws Exception {
|
||||
var rmF = Minecraft.class.getDeclaredField("resourceManager");
|
||||
rmF.setAccessible(true);
|
||||
var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc);
|
||||
mc.getResourcePackRepository().reload();
|
||||
|
||||
var barrier = new net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier() {
|
||||
@Override public <T> java.util.concurrent.CompletableFuture<T> wait(T t) {
|
||||
return java.util.concurrent.CompletableFuture.completedFuture(t);
|
||||
}
|
||||
};
|
||||
var prof = InactiveProfiler.INSTANCE;
|
||||
java.util.concurrent.Executor sync = Runnable::run;
|
||||
|
||||
for (var listener : reloadListeners(rm)) {
|
||||
String name = listener.getName();
|
||||
try {
|
||||
var fut = listener.reload(barrier, rm, prof, prof, sync, sync);
|
||||
long deadline = System.currentTimeMillis() + 60_000;
|
||||
while (!fut.isDone() && System.currentTimeMillis() < deadline) Thread.sleep(10);
|
||||
if (fut.isDone()) fut.join();
|
||||
else System.err.println("[mcfold] TIMEOUT " + name);
|
||||
} catch (Throwable e) {
|
||||
System.err.println("[mcfold] FAILED " + name + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static java.util.List<net.minecraft.server.packs.resources.PreparableReloadListener> reloadListeners(Object rm) throws Exception {
|
||||
for (var f : rm.getClass().getDeclaredFields()) {
|
||||
if (java.util.List.class.isAssignableFrom(f.getType())) {
|
||||
f.setAccessible(true);
|
||||
Object v = f.get(rm);
|
||||
if (v instanceof java.util.List<?> l && !l.isEmpty()
|
||||
&& l.get(0) instanceof net.minecraft.server.packs.resources.PreparableReloadListener) {
|
||||
return (java.util.List<net.minecraft.server.packs.resources.PreparableReloadListener>) l;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("reload listeners not found");
|
||||
}
|
||||
|
||||
// ---------------- 工具 ----------------
|
||||
|
||||
static byte[] decode(String b64) {
|
||||
return Base64.getDecoder().decode(b64);
|
||||
}
|
||||
|
||||
static String arg(String[] a, String key, String def) {
|
||||
for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1];
|
||||
return def;
|
||||
}
|
||||
|
||||
static synchronized void emit(Integer id, JsonObject obj) {
|
||||
if (id != null) obj.addProperty("id", id);
|
||||
OUT.println(GSON.toJson(obj));
|
||||
OUT.flush();
|
||||
}
|
||||
|
||||
static void emitError(Integer id, String msg) {
|
||||
JsonObject o = new JsonObject();
|
||||
o.addProperty("ok", false);
|
||||
o.addProperty("error", msg);
|
||||
emit(id, o);
|
||||
}
|
||||
|
||||
static void event(String name, JsonObject payload) {
|
||||
JsonObject o = new JsonObject();
|
||||
o.addProperty("event", name);
|
||||
if (payload != null) o.add("payload", payload);
|
||||
OUT.println(GSON.toJson(o));
|
||||
OUT.flush();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package net.minecraft.client.renderer;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
|
||||
/**
|
||||
* 包私有成员桥。
|
||||
*
|
||||
* GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用;
|
||||
* 我们不跑主循环、不连服,所以要用同包类直接调。
|
||||
* reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调
|
||||
* loadEffect —— 官方由 onResourceManagerReload / 药水效果改
|
||||
*/
|
||||
public class McfoldBridge {
|
||||
public static void reloadShaders(GameRenderer gr, ResourceProvider provider) {
|
||||
gr.reloadShaders(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* 载入一个原版后处理链。
|
||||
*
|
||||
* ★ 世代差异(1.21+):loadEffect 从 package-private 变成了 **private**
|
||||
* (实测 javap:1.20.1 是 `void loadEffect(...)`,1.21 是 `private void`),
|
||||
* 同包也调不到 ⇒ 只能用反射。
|
||||
* 1.21 新增了 public togglePostEffect() 但那是「切换内置效果」,
|
||||
* 不支持任意 post chain id,所以仍走反射。
|
||||
*/
|
||||
public static void loadEffect(GameRenderer gr, ResourceLocation id) {
|
||||
try {
|
||||
java.lang.reflect.Method m = GameRenderer.class.getDeclaredMethod("loadEffect", ResourceLocation.class);
|
||||
m.setAccessible(true);
|
||||
m.invoke(gr, id);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("loadEffect 不可用(1.21+ 为 private)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user