P8 完成:世界数据 → 官方渲染器 → 真实出图
不起完整客户端、不连服、不登录,只初始化 MC 渲染栈并喂入我们自己造的世界数据。
已验证(全绿):
countRenderedChunks=29 distinct colors=1450
画面:草地/原木/树叶/天空/HUD/英文文字(sample-world-render.png)
打通的关卡:
- 反混淆(client-named.jar)+ GL 离屏 + new Minecraft(cfg) 不进主循环
- 资源同步重载:22 listener(ModelManager/FontManager/Dispatcher)
★ 不能 join reloadResourcePacks(等主循环→死锁),直接调各 listener reload()
- 注册表从客户端自带 data/ 加载(dimension_type=4)
- 区块注入:LevelChunk + setBlockState → ClientChunkCache.Storage.chunks
+ LightEngine.queueSectionData(全亮)→ allChanged()
- 每帧 mc.tick() 推进网格重建队列(不跑则 countRenderedChunks=0)
踩坑记录(10 条,README 详列):
LoadingOverlay/ReceivingLevelScreen 遮挡、FolderData 参数序、
assetIndex 用落盘名、reloadShaders 包私有、读像素必须绑 FBO、
判据用 distinct colors 而非 non-black
更正附录 D:state_id=1003 → note_block 不是误映射(note_block 有 800 状态)
—— 真客户端给完整状态,是 1.20.1 上 mod 方块语义的唯一权威来源
This commit is contained in:
@ -1,21 +0,0 @@
|
||||
import org.lwjgl.glfw.*;
|
||||
import org.lwjgl.opengl.*;
|
||||
import static org.lwjgl.glfw.GLFW.*;
|
||||
import static org.lwjgl.opengl.GL11.*;
|
||||
import static org.lwjgl.system.MemoryUtil.NULL;
|
||||
|
||||
public class GlProbe {
|
||||
public static void main(String[] a) {
|
||||
GLFWErrorCallback.createPrint(System.err).set();
|
||||
if (!glfwInit()) { System.out.println("FAIL: glfwInit"); return; }
|
||||
glfwDefaultWindowHints();
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
long win = glfwCreateWindow(320, 240, "probe", NULL, NULL);
|
||||
if (win == NULL) { System.out.println("FAIL: createWindow"); return; }
|
||||
glfwMakeContextCurrent(win);
|
||||
GL.createCapabilities();
|
||||
System.out.println("OK gl=" + glGetString(GL_VERSION));
|
||||
System.out.println("OK renderer=" + glGetString(GL_RENDERER));
|
||||
glfwDestroyWindow(win); glfwTerminate();
|
||||
}
|
||||
}
|
||||
@ -6,67 +6,102 @@
|
||||
## 实测结论(2026-09-25 全绿)
|
||||
|
||||
客户端 jar 是 **ProGuard 全量混淆**的(`LevelRenderer` 叫 `fjv`),
|
||||
所以「抽渲染器」的第一步必须是**反混淆**。三层关卡全部验证通过:
|
||||
所以「抽渲染器」的第一步必须是**反混淆**。全部关卡已验证:
|
||||
|
||||
| 关卡 | 证据 | 结果 |
|
||||
|---|---|---|
|
||||
| 反混淆 | `client-named.jar` 25MB;`javap` 读出 `net.minecraft.client.renderer.LevelRenderer` | ✅ |
|
||||
| 编译期引用 | `javac` 编译引用 `Minecraft`/`LevelRenderer` 的代码 | ✅ |
|
||||
| 反混淆 | `client-named.jar`;`javap` 读出 `LevelRenderer` | ✅ |
|
||||
| GL 离屏 | Xvfb + llvmpipe → OpenGL 4.5 core | ✅ |
|
||||
| **渲染栈初始化** | `new Minecraft(cfg)` 完成 GL/资源/渲染器初始化,**不进主循环** | ✅ |
|
||||
| **真渲染出图** | 渲 3 帧 + 读像素 → PNG,画面是 Mojang 加载屏(logo 贴图 + 进度条) | ✅ |
|
||||
| 渲染栈初始化 | `new Minecraft(cfg)` 完成初始化,**不进主循环** | ✅ |
|
||||
| 资源重载 | 22 个 listener;ModelManager/FontManager/Dispatcher 全 ok | ✅ |
|
||||
| 注册表 | 客户端自带 `data/` → dimension_type=4 | ✅ |
|
||||
| 区块注入 | 81 chunks → `countRenderedChunks=29` | ✅ |
|
||||
| **真实世界渲染** | 草地+原木+树叶+天空+HUD 文字,`distinct colors=1450` | ✅ |
|
||||
|
||||
见 `sample-world-render.png`。
|
||||
|
||||
## 为什么渲染器不能独立 new
|
||||
|
||||
反编译(Vineflower)给出的实测签名:
|
||||
反编译(Vineflower)实测签名:
|
||||
|
||||
```java
|
||||
LevelRenderer(Minecraft, EntityRenderDispatcher, BlockEntityRenderDispatcher, RenderBuffers)
|
||||
renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f)
|
||||
```
|
||||
|
||||
全部挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。
|
||||
全挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。
|
||||
正确形态是「初始化 MC 渲染栈,但不跑主循环」。
|
||||
|
||||
## 权威序列(反编译 `net.minecraft.client.main.Main` 得到)
|
||||
## 权威序列
|
||||
|
||||
初始化(反编译 `net.minecraft.client.main.Main`):
|
||||
|
||||
```java
|
||||
SharedConstants.tryDetectVersion(); // ← 漏了会 "Game version not set"
|
||||
SharedConstants.tryDetectVersion(); // 漏了 → "Game version not set"
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
RenderSystem.initRenderThread();
|
||||
RenderSystem.beginInitialization();
|
||||
Minecraft mc = new Minecraft(gameConfig); // ← 构造即完成渲染栈初始化
|
||||
Minecraft mc = new Minecraft(gameConfig);
|
||||
RenderSystem.finishInitialization();
|
||||
// 此处官方会起 Game thread 跑主循环 —— 我们不做
|
||||
```
|
||||
|
||||
渲染一帧(反编译 `Minecraft.render()` 得到):
|
||||
渲染一帧(反编译 `Minecraft.render()`):
|
||||
|
||||
```java
|
||||
RenderSystem.clear(16640, ON_OSX);
|
||||
mainRenderTarget.bindWrite(true);
|
||||
gameRenderer.render(partialTick, nanos, renderLevel);
|
||||
gameRenderer.render(partialTick, nanos, true);
|
||||
mainRenderTarget.unbindWrite();
|
||||
mainRenderTarget.blitToScreen(w, h);
|
||||
window.updateDisplay();
|
||||
```
|
||||
|
||||
## 踩过的三个坑(都有实测证据)
|
||||
## 踩过的坑(全部有实测证据)
|
||||
|
||||
1. **漏 `tryDetectVersion()`** → `DataFixers.<clinit>` 抛 `Game version not set`
|
||||
2. **`FolderData` 参数顺序**
|
||||
`(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)`
|
||||
—— 顺序写错会让 `icons/icon_16x16.png` 404
|
||||
3. **assetIndex 名要用落盘名**
|
||||
version.json 里 `assetIndex.id = "5"`,但磁盘是 `assets/indexes/1.20.1.json`
|
||||
(MCLC 按 `version.number` 落盘)—— 传 `"5"` 则索引加载失败、资源全找不到
|
||||
2. **`FolderData` 参数顺序** = `(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)`
|
||||
—— 写错则 `icons/icon_16x16.png` 404
|
||||
3. **assetIndex 要用落盘名**(`1.20.1`)而非 `version.json` 的 `assetIndex.id`(`5`)
|
||||
—— MCLC 按 `version.number` 落盘
|
||||
4. **`LoadingOverlay` 必须摘** → 否则永远渲出 Mojang 加载屏
|
||||
5. **`ReceivingLevelScreen`("Joining world...")必须清** → 否则客户端认为未进世界
|
||||
6. **必须跑 `mc.tick()`** → 区块网格重建队列靠 tick 推进(不跑则 `countRenderedChunks=0`)
|
||||
7. **`reloadResourcePacks()` 不可 join**(等主循环 → 死锁);
|
||||
正解是**直接调各 listener 的 public `reload()`**,barrier 同步返回、
|
||||
apply 用 `Runnable::run`(主线程即渲染线程)
|
||||
8. **`GameRenderer.reloadShaders` 包私有** → 同包 bridge 调用
|
||||
9. **读像素必须绑 FBO**(`glBindFramebuffer(target.frameBufferId)` + `GL_COLOR_ATTACHMENT0`);
|
||||
`bindRead()` 路径实测读到的是残留缓冲
|
||||
10. **判据要锐利**:`non-black=100%` 会被红底加载屏骗过;
|
||||
`distinct colors` 才区分得出「加载屏(199) vs 世界(1450)」
|
||||
|
||||
## 下一步:渲染真实世界
|
||||
## 注入世界数据
|
||||
|
||||
`ClientLevel` 构造只需 `listener.registryAccess()`(其余字段不用 listener),
|
||||
且 `ClientChunkCache.replaceWithPacketData(...)` 接收的正是 **`map_chunk` 包的原始字节**
|
||||
—— 与 mcfold `chunk.py` 解码器同一格式。这就是设计文档说的桥梁落点。
|
||||
```java
|
||||
LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz));
|
||||
chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false);
|
||||
// ★ 放进 ClientChunkCache.Storage.chunks(反射 getIndex)
|
||||
// ★ 光照:LightEngine.queueSectionData(SKY/BLOCK, SectionPos, DataLayer 全15)
|
||||
// ★ 触发重建:levelRenderer.allChanged() + 每帧 mc.tick()
|
||||
```
|
||||
|
||||
**与 mcfold Python 客户端的接口**:`ClientChunkCache.replaceWithPacketData(...)`
|
||||
接收的正是 `map_chunk` 包原始字节 —— **Python 客户端收到的就是它**,
|
||||
可零转换直通;或用 Python 解码后的方块写入 `LevelChunk`。
|
||||
|
||||
## 注册表:state id ↔ 方块名(mod 支持的关键)
|
||||
|
||||
```java
|
||||
BuiltInRegistries.BLOCK.size() // 1003
|
||||
Block.BLOCK_STATE_REGISTRY.size() // 24135
|
||||
Block.stateById(1003) // note_block[instrument=xylophone,note=7,powered=false]
|
||||
BuiltInRegistries.BLOCK.getKey(state.getBlock())
|
||||
```
|
||||
|
||||
★ **更正附录 D 的一个错误结论**:`1003 → note_block` **不是**静默误映射
|
||||
—— note_block 有 800 个状态,1003 真的落在其范围内。真客户端给的是**完整状态**
|
||||
(`instrument/note/powered`),这正是 1.20.1 上 mod 方块语义的唯一权威来源。
|
||||
|
||||
## 构建
|
||||
|
||||
|
||||
82
scaffold/RegistryProbe.java
Normal file
82
scaffold/RegistryProbe.java
Normal file
@ -0,0 +1,82 @@
|
||||
import com.mojang.blaze3d.platform.DisplayData;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
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.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.Bootstrap;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* 证明:真客户端的注册表能给出 state id ↔ 方块名的权威映射。
|
||||
*
|
||||
* 这正是不起完整客户端、只用客户端注册表的价值所在:
|
||||
* 1.20.1 不从网络下发方块表(附录D),mod 方块的 state id 只有客户端注册表知道。
|
||||
*/
|
||||
public class RegistryProbe {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
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");
|
||||
|
||||
SharedConstants.tryDetectVersion();
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
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(854, 480, 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();
|
||||
Minecraft mc = new Minecraft(cfg);
|
||||
RenderSystem.finishInitialization();
|
||||
System.out.println("[reg] Minecraft constructed");
|
||||
|
||||
// ★ 核心:客户端的方块注册表
|
||||
int blockCount = BuiltInRegistries.BLOCK.size();
|
||||
int stateCount = Block.BLOCK_STATE_REGISTRY.size();
|
||||
System.out.println("[reg] BLOCK registry size = " + blockCount);
|
||||
System.out.println("[reg] BLOCK_STATE registry size = " + stateCount);
|
||||
|
||||
// ★ 关键验证:mineflayer 会静默误映射的那些越界 id,客户端怎么答
|
||||
int[] probes = {0, 1, 1002, 1003, 1502, 30000, stateCount - 1};
|
||||
for (int id : probes) {
|
||||
BlockState st = Block.stateById(id);
|
||||
ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock());
|
||||
System.out.println("[reg] state_id=" + id + " -> " + key + " [" + st + "]");
|
||||
}
|
||||
|
||||
// ★ 列出几个方块名(证明可读)
|
||||
System.out.println("[reg] sample blocks:");
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ResourceLocation k = BuiltInRegistries.BLOCK.getKey(BuiltInRegistries.BLOCK.byId(i));
|
||||
System.out.println(" id=" + i + " -> " + k);
|
||||
}
|
||||
|
||||
System.out.println("[reg] DONE");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
import com.mojang.blaze3d.pipeline.MainTarget;
|
||||
import com.mojang.blaze3d.platform.DisplayData;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
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.server.Bootstrap;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* mcfold render probe —— 起 MC 渲染栈,渲一帧,读像素写 PNG。
|
||||
* 全程:不连服、不登录、不进主循环、不 gameplay。
|
||||
*/
|
||||
public class RenderProbe {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
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");
|
||||
String out = arg(args, "--out", "/tmp/scaffold-frame.png");
|
||||
int width = Integer.parseInt(arg(args, "--width", "854"));
|
||||
int height = Integer.parseInt(arg(args, "--height", "480"));
|
||||
|
||||
SharedConstants.tryDetectVersion();
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
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();
|
||||
Minecraft mc = new Minecraft(cfg);
|
||||
RenderSystem.finishInitialization();
|
||||
|
||||
System.out.println("[probe] constructed. w=" + mc.getWindow().getWidth()
|
||||
+ " h=" + mc.getWindow().getHeight());
|
||||
|
||||
// ★ 渲染一帧:照抄 Minecraft.render() 的实测序列(反编译 nout2/.../Minecraft.java:1110-1140)
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
for (int frame = 0; frame < 3; frame++) {
|
||||
RenderSystem.clear(16640, Minecraft.ON_OSX);
|
||||
target.bindWrite(true);
|
||||
mc.gameRenderer.render(0.0F, 0L, true);
|
||||
target.unbindWrite();
|
||||
target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight());
|
||||
mc.getWindow().updateDisplay();
|
||||
System.out.println("[probe] frame " + frame + " rendered");
|
||||
}
|
||||
|
||||
// ★ 读像素:从 GL 后台缓冲直接读(比截屏更可靠,且不依赖 X)
|
||||
GL11.glReadBuffer(GL11.GL_BACK);
|
||||
int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight();
|
||||
ByteBuffer buf = org.lwjgl.BufferUtils.createByteBuffer(w * h * 4);
|
||||
GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
|
||||
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
// GL 原点在左下,PNG 在左上,需翻转
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int i = ((h - 1 - y) * w + x) * 4;
|
||||
int r = buf.get(i) & 0xFF, g = buf.get(i + 1) & 0xFF, b = buf.get(i + 2) & 0xFF;
|
||||
img.setRGB(x, y, (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
ImageIO.write(img, "png", new File(out));
|
||||
System.out.println("[probe] wrote " + out + " (" + w + "x" + h + ")");
|
||||
|
||||
// 统计非黑像素,证明真的画了东西(而非空缓冲)
|
||||
long nonBlack = 0; int total = w * h;
|
||||
for (int p : img.getRGB(0, 0, w, h, null, 0, w)) if ((p & 0xFFFFFF) != 0) nonBlack++;
|
||||
System.out.println("[probe] non-black pixels = " + nonBlack + "/" + total
|
||||
+ " (" + (100.0 * nonBlack / total) + "%)");
|
||||
|
||||
GLFW.glfwTerminate();
|
||||
System.out.println("[probe] DONE");
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
import com.mojang.blaze3d.platform.DisplayData;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
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.server.Bootstrap;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
/**
|
||||
* mcfold scaffold —— 只起 MC 的**渲染栈**,不进游戏主循环。
|
||||
*
|
||||
* 依据(反编译 net.minecraft.client.main.Main 的实测序列):
|
||||
* Bootstrap.bootStrap()
|
||||
* RenderSystem.initRenderThread(); beginInitialization();
|
||||
* new Minecraft(gameConfig) ← 构造即完成 GL/资源/渲染器初始化
|
||||
* RenderSystem.finishInitialization()
|
||||
* 之后 Main 才起 Game thread 跑主循环 —— 我们**不**走那步。
|
||||
*/
|
||||
public class Scaffold {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default");
|
||||
String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets");
|
||||
// ★ 索引名必须是 assets/indexes/<name>.json 中真实存在的那个。
|
||||
// version.json 的 assetIndex.id 是 "5",但 MCLC 落盘用 version.number(1.20.1)
|
||||
// —— 传 "5" 会导致索引加载失败,所有资源/icons 全部找不到(实测踩过)。
|
||||
String assetIdx = arg(args, "--assetIndex", "1.20.1");
|
||||
int width = Integer.parseInt(arg(args, "--width", "1280"));
|
||||
int height = Integer.parseInt(arg(args, "--height", "720"));
|
||||
|
||||
System.out.println("[scaffold] bootstrap...");
|
||||
// ★ 官方 Main 的实测序列:tryDetectVersion 必须在 bootStrap 之前,
|
||||
// 否则 DataFixers.<clinit> 抛 "Game version not set"
|
||||
SharedConstants.tryDetectVersion();
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
System.out.println("[scaffold] bootstrap ok, mcVersion=" + SharedConstants.getCurrentVersion().getName());
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
File assetsDir = new File(assets);
|
||||
|
||||
User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam",
|
||||
java.util.Optional.empty(), java.util.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(
|
||||
// ★ 顺序是 (gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)
|
||||
// 反着写会把 assets 当游戏目录,导致资源全找不到(实测踩过:icons/icon_16x16.png 404)
|
||||
runDir, new File(runDir, "resourcepacks"), assetsDir, assetIdx),
|
||||
new GameConfig.GameData(
|
||||
false, // demo
|
||||
"mcfold", // version
|
||||
"release", // versionType
|
||||
false, // disableMultiplayer
|
||||
false), // disableChat
|
||||
new GameConfig.QuickPlayData(null, null, null, null)
|
||||
);
|
||||
|
||||
System.out.println("[scaffold] RenderSystem init...");
|
||||
RenderSystem.initRenderThread();
|
||||
RenderSystem.beginInitialization();
|
||||
|
||||
System.out.println("[scaffold] new Minecraft(cfg) ...");
|
||||
Minecraft mc = new Minecraft(cfg);
|
||||
|
||||
RenderSystem.finishInitialization();
|
||||
System.out.println("[scaffold] OK Minecraft constructed");
|
||||
|
||||
// 检查渲染栈关键对象是否就绪(不启主循环)
|
||||
System.out.println("[scaffold] window=" + mc.getWindow().getWidth() + "x" + mc.getWindow().getHeight());
|
||||
System.out.println("[scaffold] levelRenderer=" + (mc.levelRenderer != null ? "present" : "null"));
|
||||
System.out.println("[scaffold] gameRenderer=" + (mc.gameRenderer != null ? "present" : "null"));
|
||||
System.out.println("[scaffold] level=" + (mc.level != null ? "present" : "null (не connected)"));
|
||||
|
||||
System.out.println("[scaffold] DONE (no main loop)");
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
344
scaffold/WorldRender.java
Normal file
344
scaffold/WorldRender.java
Normal file
@ -0,0 +1,344 @@
|
||||
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.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.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 世界渲染器 —— 把世界数据喂给真客户端渲染器并出图。
|
||||
*
|
||||
* 实测要点(2026-09-25):
|
||||
* 1. 不跑主循环 ⇒ 渲染栈手动初始化(tryDetectVersion/Bootstrap/RenderSystem/new Minecraft)
|
||||
* 2. 注册表从客户端自带 data/ 加载(RegistryDataLoader + vanilla pack)
|
||||
* 3. shader 需显式编译(GameRenderer.reloadShaders 包私有 → 同包 bridge)
|
||||
* 4. LoadingOverlay 必须摘掉,否则永远渲出加载屏
|
||||
* 5. 区块靠 LevelChunk + setBlockState 注入 + setSectionDirty 触发网格重建
|
||||
* 6. 读像素必须绑 FBO(glBindFramebuffer(frameBufferId)),GL_BACK 读到的是残留
|
||||
*/
|
||||
public class WorldRender {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
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");
|
||||
String out = arg(args, "--out", "/tmp/scaffold-world.png");
|
||||
int W = Integer.parseInt(arg(args, "--width", "854"));
|
||||
int H = Integer.parseInt(arg(args, "--height", "480"));
|
||||
|
||||
SharedConstants.tryDetectVersion();
|
||||
SharedConstants.enableDataFixerOptimizations();
|
||||
Bootstrap.bootStrap();
|
||||
|
||||
File runDir = new File(gameDir);
|
||||
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(W, H, 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();
|
||||
Minecraft mc = new Minecraft(cfg);
|
||||
RenderSystem.finishInitialization();
|
||||
System.out.println("[wr] Minecraft ok");
|
||||
|
||||
// ── 资源:字体/贴图/模型 ──
|
||||
// createReload 的 future 不该在主线程 join(会死锁),但字体加载是同步的,
|
||||
// 这里用官方 FontManager.reload 直接同步加载字体(修「文字成方框」)
|
||||
// ★ 资源重载:modelManager/fontManager/textureManager 全是 reload listener,
|
||||
// 不跑重载则方块模型/字体/贴图全空(地形渲不出来)。
|
||||
// 驱动方式照抄 LoadingOverlay:轮询 isDone(),不能 join(会死锁)。
|
||||
reloadResources(mc);
|
||||
System.out.println("[wr] resources reloaded");
|
||||
|
||||
// ── listener ──
|
||||
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);
|
||||
|
||||
// ── 注册表:从客户端自带 data/ 加载 ──
|
||||
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()));
|
||||
java.util.List<RegistryDataLoader.RegistryData<?>> all = new java.util.ArrayList<>();
|
||||
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});
|
||||
java.lang.reflect.Field rf = ClientPacketListener.class.getDeclaredField("registryAccess");
|
||||
rf.setAccessible(true);
|
||||
rf.set(listener, layered);
|
||||
System.out.println("[wr] registries loaded: dimension_type="
|
||||
+ loaded.registryOrThrow(Registries.DIMENSION_TYPE).size());
|
||||
|
||||
Holder<DimensionType> dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE)
|
||||
.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld")));
|
||||
|
||||
// ── ClientLevel ──
|
||||
var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false);
|
||||
ClientLevel level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder,
|
||||
8, 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);
|
||||
System.out.println("[wr] level + player ok");
|
||||
|
||||
// ── shader 编译(包私有,走同包 bridge)──
|
||||
net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager());
|
||||
System.out.println("[wr] shaders ok: " + (RenderSystem.getShader() != null));
|
||||
|
||||
// ── LoadingOverlay 必须摘掉 ──
|
||||
mc.setOverlay(null);
|
||||
// ★ "Joining world..." 是 ReceivingLevelScreen;不清它客户端始终认为未进世界
|
||||
mc.setScreen(null);
|
||||
|
||||
// ── 注入区块 ──
|
||||
int injected = injectWorld(level, mc);
|
||||
System.out.println("[wr] injected chunks=" + injected);
|
||||
|
||||
// ── 第三人称(跳过 renderItemInHand)──
|
||||
mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);
|
||||
|
||||
// ── 渲染(每帧先 tick:区块网格重建队列靠 tick 推进)──
|
||||
MainTarget target = (MainTarget) mc.getMainRenderTarget();
|
||||
for (int f = 0; f < 60; f++) {
|
||||
try { mc.tick(); } catch (Throwable t) { if (f == 0) System.out.println("[wr] tick err: " + t); }
|
||||
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();
|
||||
}
|
||||
System.out.println("[wr] rendered 20 frames");
|
||||
|
||||
// ── 诊断:区块与网格状态 ──
|
||||
System.out.println("[wr] hasRenderedAllChunks=" + mc.levelRenderer.hasRenderedAllChunks()
|
||||
+ " countRenderedChunks=" + mc.levelRenderer.countRenderedChunks());
|
||||
var got = level.getChunkSource().getChunk(0, 0, net.minecraft.world.level.chunk.ChunkStatus.FULL, false);
|
||||
System.out.println("[wr] getChunk(0,0)=" + (got != null ? got.getClass().getSimpleName() : "null")
|
||||
+ " block(0,64,0)=" + (got != null ? got.getBlockState(new BlockPos(0,64,0)) : "-"));
|
||||
System.out.println("[wr] isChunkCompiled(0,64,0)=" + mc.levelRenderer.isChunkCompiled(new BlockPos(0,64,0)));
|
||||
|
||||
save(target, W, H, out);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步驱动各 reload listener。
|
||||
*
|
||||
* 为什么不用 Minecraft.reloadResourcePacks():
|
||||
* 它的 future 靠主循环 tick 推进(LoadingOverlay),我们不跑主循环 ⇒ 死锁。
|
||||
* 为什么不用 createReload():
|
||||
* prepare 阶段仍走真后台池,实测 120s 超时。
|
||||
* 正解:直接调各 listener 的 public reload(),barrier 同步返回,
|
||||
* 且 apply 用 Runnable::run 保证在渲染线程执行。
|
||||
*/
|
||||
private 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);
|
||||
var packs = mc.getResourcePackRepository();
|
||||
packs.reload();
|
||||
|
||||
// 同步 barrier:prepare 完成后立刻交出,不排队
|
||||
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;
|
||||
// prepare 用当前线程(同步跑),apply 也用当前线程(渲染线程)
|
||||
java.util.concurrent.Executor sync = Runnable::run;
|
||||
|
||||
var all = reloadListeners(mc);
|
||||
System.out.println("[wr] listeners=" + all.size());
|
||||
for (var listener : all) {
|
||||
String name = listener.getName();
|
||||
System.out.println("[wr] -> " + name + " ...");
|
||||
try {
|
||||
var fut = listener.reload(barrier, rm, prof, prof, sync, sync);
|
||||
boolean done = false;
|
||||
long dl = System.currentTimeMillis() + 30_000;
|
||||
while (!done && System.currentTimeMillis() < dl) {
|
||||
done = fut.isDone();
|
||||
if (!done) Thread.sleep(10);
|
||||
}
|
||||
if (!done) { System.out.println("[wr] TIMEOUT " + name); continue; }
|
||||
fut.join();
|
||||
System.out.println("[wr] ok: " + name);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("[wr] FAILED " + name + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 取出注册过的 listener(private 字段) */
|
||||
@SuppressWarnings("unchecked")
|
||||
private static java.util.List<net.minecraft.server.packs.resources.PreparableReloadListener> reloadListeners(Minecraft mc) throws Exception {
|
||||
var rmF = Minecraft.class.getDeclaredField("resourceManager");
|
||||
rmF.setAccessible(true);
|
||||
var rm = rmF.get(mc);
|
||||
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");
|
||||
}
|
||||
|
||||
/** 造地形并注入:区块 + 光照 + 网格重建 */
|
||||
private static int injectWorld(ClientLevel level, Minecraft mc) throws Exception {
|
||||
int radius = 4; // 9x9 区块 = 144x144 格
|
||||
int yBottom = level.getMinBuildHeight();
|
||||
var cache = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource();
|
||||
var light = cache.getLightEngine();
|
||||
|
||||
// 反射拿 Storage.chunks / getIndex(包私有)
|
||||
var storageF = net.minecraft.client.multiplayer.ClientChunkCache.class.getDeclaredField("storage");
|
||||
storageF.setAccessible(true);
|
||||
Object storage = storageF.get(cache);
|
||||
var chunksF = storage.getClass().getDeclaredField("chunks");
|
||||
chunksF.setAccessible(true);
|
||||
var chunks = (java.util.concurrent.atomic.AtomicReferenceArray<LevelChunk>) chunksF.get(storage);
|
||||
var getIndex = storage.getClass().getDeclaredMethod("getIndex", int.class, int.class);
|
||||
getIndex.setAccessible(true);
|
||||
System.out.println("[wr] storage chunks.length=" + chunks.length());
|
||||
|
||||
int count = 0;
|
||||
for (int cx = -radius; cx <= radius; cx++) {
|
||||
for (int cz = -radius; cz <= radius; cz++) {
|
||||
LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz));
|
||||
for (int lx = 0; lx < 16; lx++) {
|
||||
for (int lz = 0; lz < 16; lz++) {
|
||||
int wx = cx * 16 + lx, wz = cz * 16 + lz;
|
||||
for (int y = yBottom; y < 63; y++)
|
||||
chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.STONE.defaultBlockState(), false);
|
||||
chunk.setBlockState(new BlockPos(lx, 63, lz), Blocks.DIRT.defaultBlockState(), false);
|
||||
chunk.setBlockState(new BlockPos(lx, 64, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false);
|
||||
if ((wx % 7 == 0 && wz % 7 == 0) && (wx != 0 || wz != 0)) {
|
||||
for (int y = 65; y < 72; y++)
|
||||
chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.OAK_LOG.defaultBlockState(), false);
|
||||
for (int dx = -2; dx <= 2; dx++)
|
||||
for (int dz = -2; dz <= 2; dz++)
|
||||
if (lx+dx >= 0 && lx+dx < 16 && lz+dz >= 0 && lz+dz < 16
|
||||
&& Math.abs(dx) + Math.abs(dz) <= 2)
|
||||
chunk.setBlockState(new BlockPos(lx+dx, 72, lz+dz),
|
||||
Blocks.OAK_LEAVES.defaultBlockState(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ★ 核心:直接放进 ClientChunkCache.Storage.chunks
|
||||
int idx = (int) getIndex.invoke(storage, cx, cz);
|
||||
chunks.set(idx, chunk);
|
||||
count++;
|
||||
|
||||
// ★ 光照:全亮(否则地形全黑)。section y 0..19 覆盖 build height
|
||||
for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) {
|
||||
var sky = new net.minecraft.world.level.chunk.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(net.minecraft.world.level.LightLayer.SKY,
|
||||
net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
light.queueSectionData(net.minecraft.world.level.LightLayer.BLOCK,
|
||||
net.minecraft.core.SectionPos.of(cx, sy, cz), sky);
|
||||
}
|
||||
light.setLightEnabled(new ChunkPos(cx, cz), true);
|
||||
chunk.setLightCorrect(true);
|
||||
}
|
||||
}
|
||||
int upd = light.runLightUpdates();
|
||||
System.out.println("[wr] light updates=" + upd);
|
||||
|
||||
// 触发区块网格重建
|
||||
mc.levelRenderer.allChanged();
|
||||
System.out.println("[wr] allChanged done");
|
||||
return count;
|
||||
}
|
||||
|
||||
private static void save(MainTarget target, int W, int H, String out) throws Exception {
|
||||
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);
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int x = 0; x < W; x++) {
|
||||
int i = ((H - 1 - y) * W + x) * 4;
|
||||
img.setRGB(x, y, ((buf.get(i) & 0xFF) << 16) | ((buf.get(i+1) & 0xFF) << 8) | (buf.get(i+2) & 0xFF));
|
||||
}
|
||||
ImageIO.write(img, "png", new File(out));
|
||||
java.util.Set<Integer> colors = new java.util.HashSet<>();
|
||||
for (int p : img.getRGB(0, 0, W, H, null, 0, W)) colors.add(p & 0xFFFFFF);
|
||||
System.out.println("[wr] wrote " + out + " distinct colors=" + colors.size());
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
14
scaffold/net/minecraft/client/renderer/McfoldBridge.java
Normal file
14
scaffold/net/minecraft/client/renderer/McfoldBridge.java
Normal file
@ -0,0 +1,14 @@
|
||||
package net.minecraft.client.renderer;
|
||||
|
||||
import net.minecraft.server.packs.resources.ResourceProvider;
|
||||
|
||||
/**
|
||||
* 包私有成员桥(GameRenderer.reloadShaders 是 package-private)。
|
||||
* 官方由匿名内部类 GameRenderer$1 在资源重载时调用;
|
||||
* 我们不跑主循环 ⇒ 用同包类直接调用,跳过整个 reload 基础设施。
|
||||
*/
|
||||
public class McfoldBridge {
|
||||
public static void reloadShaders(GameRenderer gr, ResourceProvider provider) {
|
||||
gr.reloadShaders(provider);
|
||||
}
|
||||
}
|
||||
BIN
scaffold/sample-world-render.png
Normal file
BIN
scaffold/sample-world-render.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
Reference in New Issue
Block a user