@xiaozhu2007/koishi-plugin-litematic-renderer 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/lib/index.d.ts +242 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
## 维护版变更
|
|
10
10
|
|
|
11
|
-
- 渲染图片改用 OneBot `base64://` 发送,支持跨数据中心和 Kubernetes namespace 部署
|
|
12
|
-
- 合并转发只发送渲染结果,不再额外发送“渲染结果如上”
|
|
13
|
-
- 精简配置说明和提示文案,移除句尾句号
|
|
14
|
-
- 升级至 TypeScript 6,添加 `ignoreDeprecations: "6.0"` 以处理 `moduleResolution=node10` 弃用提示
|
|
15
|
-
- 构建时使用 Terser 压缩 `lib/index.js`
|
|
16
11
|
- 可通过实验性 `sentryDsn` 配置或 `SENTRY_DSN` 环境变量上报渲染错误
|
|
12
|
+
- 渲染图片改用 OneBot `base64://` 发送,支持跨数据中心和 Kubernetes namespace 部署
|
|
13
|
+
- 修复六视图字形(`SIX_FACE_LABELS`)混乱问题,改用文泉驿 9pt 点阵
|
|
14
|
+
- 合并转发只发送渲染结果,不再额外发送「渲染结果如上」
|
|
15
|
+
- 升级至 TypeScript 6,构建时使用 Terser 压缩 `lib/index.js`
|
|
16
|
+
- 为代码添加可读性类型注解,便于 IDE 自动补全
|
package/lib/index.d.ts
CHANGED
|
@@ -1,60 +1,113 @@
|
|
|
1
1
|
import { Context, h, Schema, Session } from "koishi";
|
|
2
|
+
/** Koishi 插件名称 */
|
|
2
3
|
export declare const name = "litematic-renderer";
|
|
4
|
+
/** 插件可选注入的 Koishi 服务 */
|
|
3
5
|
export declare const inject: {
|
|
4
6
|
optional: string[];
|
|
5
7
|
};
|
|
8
|
+
/** Litematic 渲染插件的完整配置 */
|
|
6
9
|
export interface Config {
|
|
10
|
+
/** QQ 机器人的接入方式 */
|
|
7
11
|
qqBotType: QqBotType;
|
|
12
|
+
/** 独立渲染器使用的 Java 可执行文件;为空时自动查找 Java 21 或更高版本 */
|
|
8
13
|
standaloneJavaCommand: string;
|
|
14
|
+
/** Minecraft 客户端 JAR 或基础资源包路径 */
|
|
9
15
|
minecraftJarPath: string;
|
|
16
|
+
/** 自定义材质包路径,后面的材质包具有更高优先级 */
|
|
10
17
|
resourcePackPaths: string[];
|
|
18
|
+
/** 用于生成投影图的渲染后端 */
|
|
11
19
|
renderEngine: "standalone" | "java" | "webgl" | "cpu";
|
|
20
|
+
/** 独立 Java 渲染器 JAR 的路径 */
|
|
12
21
|
standaloneRendererJar: string;
|
|
22
|
+
/** 独立 Java 渲染任务的超时时间,单位为毫秒 */
|
|
13
23
|
standaloneRenderTimeout: number;
|
|
24
|
+
/** 独立 Java 渲染器首次运行时的最大堆内存,单位为 MiB */
|
|
14
25
|
standaloneJavaMaxHeapMb: number;
|
|
26
|
+
/** 内存不足后重试进程的最大堆内存,单位为 MiB */
|
|
15
27
|
standaloneJavaRetryMaxHeapMb: number;
|
|
28
|
+
/** 检测到内存不足时允许重启 Java 进程的次数 */
|
|
16
29
|
standaloneJavaMemoryRestartLimit: number;
|
|
30
|
+
/** 自动处理的最大文件大小,单位为 KiB */
|
|
17
31
|
maxFileSize: number;
|
|
32
|
+
/** 投影声明方块数及所有区域总体积的上限 */
|
|
18
33
|
maxBlocks: number;
|
|
34
|
+
/** 六面图和 CPU 渲染结果的最长边像素上限 */
|
|
19
35
|
outputSize: number;
|
|
36
|
+
/** CPU 正二轴测渲染中方块菱形的半宽 */
|
|
20
37
|
isometricCellSize: number;
|
|
38
|
+
/** PNG 背景颜色,格式为十六进制 RGB */
|
|
21
39
|
background: string;
|
|
40
|
+
/** 是否将 PNG 背景输出为透明 */
|
|
22
41
|
transparentBackground: boolean;
|
|
42
|
+
/** 是否默认使用 QQ 合并转发发送渲染结果 */
|
|
23
43
|
sendAsForward: boolean;
|
|
44
|
+
/** 是否在每张渲染图前显示视图标题 */
|
|
24
45
|
showViewTitles: boolean;
|
|
46
|
+
/** 是否在结果消息中引用并提及发送者 */
|
|
25
47
|
replyAndMention: boolean;
|
|
48
|
+
/** 是否为支持的发送方式附加六面正交合成图 */
|
|
26
49
|
sixFaceOverview: boolean;
|
|
50
|
+
/** 六面正交合成图的排列方向 */
|
|
27
51
|
sixFaceLayout: SixFaceLayout;
|
|
52
|
+
/** 针对指定群聊覆盖的发送选项 */
|
|
28
53
|
groupSendOptions: GroupSendOption[];
|
|
54
|
+
/** 文件下载及 WebGL 页面操作的超时时间,单位为毫秒 */
|
|
29
55
|
renderTimeout: number;
|
|
56
|
+
/** 持久化渲染缓存的目录 */
|
|
30
57
|
cacheDirectory: string;
|
|
58
|
+
/** 所有版本渲染缓存的总容量上限,单位为 GiB */
|
|
31
59
|
cacheMaxSizeGb: number;
|
|
60
|
+
/** 聚合渲染诊断信息的 JSON 文件路径 */
|
|
32
61
|
diagnosticsFilePath: string;
|
|
62
|
+
/** 用于上报渲染异常的 Sentry DSN */
|
|
33
63
|
sentryDsn: string;
|
|
64
|
+
/** 是否修复 OneBot 适配器对群文件上传事件的映射 */
|
|
34
65
|
patchOneBotGroupUpload: boolean;
|
|
66
|
+
/** Fabric Java 渲染桥交换任务文件的目录 */
|
|
35
67
|
javaBridgeDirectory: string;
|
|
68
|
+
/** 优先调用的外部 GPU 渲染器命令 */
|
|
36
69
|
gpuRendererCommand: string;
|
|
70
|
+
/** 等待 Fabric Java 渲染桥返回结果的超时时间,单位为毫秒 */
|
|
37
71
|
javaRenderTimeout: number;
|
|
72
|
+
/** Java 渲染器最终输出图像的边长 */
|
|
38
73
|
javaResolution: number;
|
|
74
|
+
/** Java 渲染器的离屏超采样倍数 */
|
|
39
75
|
javaSupersampling: number;
|
|
76
|
+
/** WebGL 渲染的质量预设 */
|
|
40
77
|
webglQuality: "standard" | "high" | "ultra";
|
|
78
|
+
/** WebGL 单张渲染图的宽度 */
|
|
41
79
|
webglWidth: number;
|
|
80
|
+
/** WebGL 单张渲染图的高度 */
|
|
42
81
|
webglHeight: number;
|
|
82
|
+
/** 是否将 WebGL 正二轴测结果裁成正方形 */
|
|
43
83
|
isometricSquare: boolean;
|
|
84
|
+
/** 正二轴测主体在画布中的最大占比 */
|
|
44
85
|
isometricFill: number;
|
|
86
|
+
/** 正二轴测视图绕竖直轴旋转的角度 */
|
|
45
87
|
isometricRotation: number;
|
|
88
|
+
/** 正二轴测视图的俯仰角 */
|
|
46
89
|
isometricSlant: number;
|
|
47
90
|
}
|
|
91
|
+
/** 自建 QQ 中发送多张渲染图的方式 */
|
|
48
92
|
export type SendMode = "forward" | "combined";
|
|
93
|
+
/** 群聊是否覆盖全局的引用及提及设置 */
|
|
49
94
|
export type ReplyAndMentionOverride = "inherit" | "enabled" | "disabled";
|
|
95
|
+
/** 六面正交合成图的排列方向 */
|
|
50
96
|
export type SixFaceLayout = "horizontal" | "vertical";
|
|
97
|
+
/** QQ 机器人的接入方式 */
|
|
51
98
|
export type QqBotType = "official" | "selfHosted";
|
|
99
|
+
/** 指定群聊的消息发送覆盖项 */
|
|
52
100
|
export interface GroupSendOption {
|
|
101
|
+
/** 要应用覆盖设置的 QQ 群号 */
|
|
53
102
|
groupId: string;
|
|
103
|
+
/** 多张渲染图的发送方式 */
|
|
54
104
|
sendMode: SendMode;
|
|
105
|
+
/** 引用及提及行为相对于全局配置的覆盖方式 */
|
|
55
106
|
replyAndMention: ReplyAndMentionOverride;
|
|
56
107
|
}
|
|
108
|
+
/** Koishi 控制台使用的插件配置模式 */
|
|
57
109
|
export declare const Config: Schema<Config>;
|
|
110
|
+
/** 来自 Koishi 或 OneBot 事件的兼容文件元素 */
|
|
58
111
|
interface FileElement {
|
|
59
112
|
name?: string;
|
|
60
113
|
filename?: string;
|
|
@@ -69,70 +122,259 @@ interface FileElement {
|
|
|
69
122
|
fileId?: string;
|
|
70
123
|
busid?: string | number;
|
|
71
124
|
}
|
|
125
|
+
/** 投影中的非空气方块及其世界坐标 */
|
|
72
126
|
interface Block {
|
|
73
127
|
x: number;
|
|
74
128
|
y: number;
|
|
75
129
|
z: number;
|
|
76
130
|
name: string;
|
|
77
131
|
}
|
|
132
|
+
/** 已写入磁盘、可发送给会话的渲染图 */
|
|
78
133
|
interface ImageResult {
|
|
79
134
|
title: string;
|
|
80
135
|
path: string;
|
|
81
136
|
}
|
|
137
|
+
/** 从 Litematic 根标签中提取的展示元数据 */
|
|
82
138
|
export interface LitematicMetadata {
|
|
139
|
+
/** 创建投影的游戏 ID,缺失时为“未知” */
|
|
83
140
|
author: string;
|
|
141
|
+
/** 格式化后的创建时间,使用中国标准时间 */
|
|
84
142
|
createdAt: string;
|
|
143
|
+
/** 文件声明的非空气方块总数 */
|
|
85
144
|
totalBlocks?: number;
|
|
145
|
+
/** 文件声明的区域总体积 */
|
|
86
146
|
totalVolume?: number;
|
|
147
|
+
/** 投影包围盒在 X、Y、Z 轴上的尺寸 */
|
|
87
148
|
size?: [number, number, number];
|
|
149
|
+
/** Litematic 文件格式版本 */
|
|
88
150
|
litematicVersion?: number;
|
|
151
|
+
/** Minecraft 数据版本号 */
|
|
89
152
|
minecraftDataVersion?: number;
|
|
153
|
+
/** 由数据版本号推断出的 Minecraft 版本 */
|
|
90
154
|
minecraftVersion?: string;
|
|
91
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* 注册 Litematic 文件监听、中间件、命令、缓存和诊断端点
|
|
158
|
+
*
|
|
159
|
+
* @param ctx - 当前 Koishi 上下文
|
|
160
|
+
* @param config - 插件配置
|
|
161
|
+
* @returns 无返回值
|
|
162
|
+
*/
|
|
92
163
|
export declare function apply(ctx: Context, config: Config): void;
|
|
164
|
+
/**
|
|
165
|
+
* 为影响渲染结果的配置生成稳定的短哈希
|
|
166
|
+
*
|
|
167
|
+
* @param configuration - 可被 JSON 序列化的渲染配置
|
|
168
|
+
* @returns SHA-256 摘要的前 16 个十六进制字符
|
|
169
|
+
*/
|
|
93
170
|
export declare function hashRenderConfiguration(configuration: unknown): string;
|
|
171
|
+
/** OneBot 适配器源码兼容补丁的处理结果 */
|
|
94
172
|
interface SourcePatchResult {
|
|
173
|
+
/** 处理后的适配器源码 */
|
|
95
174
|
source: string;
|
|
175
|
+
/** 本次处理是否修改了源码 */
|
|
96
176
|
changed: boolean;
|
|
177
|
+
/** 原始源码是否已经支持群文件上传事件 */
|
|
97
178
|
alreadyCompatible: boolean;
|
|
98
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* 恢复特定 OneBot 适配器版本中被注释的群文件上传事件映射
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* 如果源码已经兼容或无法识别预期代码块,函数会原样返回源码
|
|
185
|
+
*
|
|
186
|
+
* @param source - OneBot 适配器编译后的 JavaScript 源码
|
|
187
|
+
* @returns 补丁后的源码以及是否修改、是否已兼容的状态
|
|
188
|
+
*/
|
|
99
189
|
export declare function patchOneBotAdapterSource(source: string): SourcePatchResult;
|
|
190
|
+
/**
|
|
191
|
+
* 从投影文件名生成跨平台安全的缓存名称片段
|
|
192
|
+
*
|
|
193
|
+
* @param filename - 原始投影文件名或路径
|
|
194
|
+
* @returns 移除扩展名和非法字符后、最长 80 个字符的名称
|
|
195
|
+
*/
|
|
100
196
|
export declare function cacheNameSegment(filename: string): string;
|
|
197
|
+
/**
|
|
198
|
+
* 按最近最少使用顺序删除缓存条目,直至容量不超过限制
|
|
199
|
+
*
|
|
200
|
+
* @param cacheDirectory - 包含版本子目录的缓存根目录
|
|
201
|
+
* @param maxBytes - 允许保留的最大总字节数
|
|
202
|
+
* @param protectedDirectory - 本轮清理中不得删除的缓存条目
|
|
203
|
+
* @returns 清理后的总字节数、删除字节数和删除条目数
|
|
204
|
+
* @throws 无法读取或删除缓存内容时抛出文件系统错误
|
|
205
|
+
*/
|
|
101
206
|
export declare function enforceCacheLimit(cacheDirectory: string, maxBytes: number, protectedDirectory?: string): Promise<{
|
|
102
207
|
totalBytes: number;
|
|
103
208
|
removedBytes: number;
|
|
104
209
|
removedEntries: number;
|
|
105
210
|
}>;
|
|
211
|
+
/** 查找上传文件所需的最小会话数据 */
|
|
106
212
|
type FileSession = Pick<Session, "elements" | "content"> & {
|
|
107
213
|
event?: unknown;
|
|
108
214
|
_data?: unknown;
|
|
109
215
|
data?: unknown;
|
|
110
216
|
message?: unknown;
|
|
111
217
|
};
|
|
218
|
+
/**
|
|
219
|
+
* 在 Koishi 会话元素及常见适配器原始事件字段中查找 Litematic 文件
|
|
220
|
+
*
|
|
221
|
+
* @param session - 包含消息元素或适配器原始数据的会话
|
|
222
|
+
* @returns 首个识别出的 `.litematic` 文件;未找到时返回 `undefined`
|
|
223
|
+
*/
|
|
112
224
|
export declare function findLitematicFile(session: FileSession): FileElement | undefined;
|
|
225
|
+
/**
|
|
226
|
+
* 判断 Java 主版本是否满足独立渲染器的最低要求
|
|
227
|
+
*
|
|
228
|
+
* @param version - Java 主版本号;检测失败时为 `undefined`
|
|
229
|
+
* @returns 版本为 21 或更高时返回 `true`
|
|
230
|
+
*/
|
|
113
231
|
export declare function supportsStandaloneJavaVersion(version: number | undefined): boolean;
|
|
232
|
+
/**
|
|
233
|
+
* 生成独立 Java 渲染进程使用的 JVM 内存和垃圾回收参数
|
|
234
|
+
*
|
|
235
|
+
* @param maxHeapMb - 请求的最大堆内存,单位为 MiB
|
|
236
|
+
* @returns 最大堆被限制在 128 到 32768 MiB 范围内的 JVM 参数
|
|
237
|
+
*/
|
|
114
238
|
export declare function standaloneJvmOptions(maxHeapMb: number): string[];
|
|
239
|
+
/**
|
|
240
|
+
* 根据退出码和标准错误判断 Java 进程是否因内存不足而失败
|
|
241
|
+
*
|
|
242
|
+
* @param output - Java 进程的标准错误输出
|
|
243
|
+
* @param exitCode - Java 进程退出码
|
|
244
|
+
* @returns 输出包含常见内存错误或退出码为 137 时返回 `true`
|
|
245
|
+
*/
|
|
115
246
|
export declare function isJavaMemoryFailure(output: string, exitCode?: number | null): boolean;
|
|
247
|
+
/**
|
|
248
|
+
* 将单次独立渲染诊断合并到插件级诊断文件
|
|
249
|
+
*
|
|
250
|
+
* @remarks
|
|
251
|
+
* 相同方块问题会累加计数并最多保留 20 个样本,相同渲染错误也会累加计数
|
|
252
|
+
*
|
|
253
|
+
* @param output - 包含 `render-diagnostics.json` 的渲染输出目录
|
|
254
|
+
* @param destination - 插件级诊断 JSON 文件路径
|
|
255
|
+
* @param logger - 用于报告新增诊断类别的 Koishi 日志器
|
|
256
|
+
* @returns 无诊断内容时直接解决,否则在合并文件写入后解决
|
|
257
|
+
* @throws 无法读取或写入诊断文件时抛出文件系统错误
|
|
258
|
+
*/
|
|
259
|
+
export declare function mergeRenderDiagnostics(output: string, destination: string, logger: ReturnType<Context["logger"]>): Promise<void>;
|
|
260
|
+
/**
|
|
261
|
+
* 将内部渲染错误转换为不泄露本地路径的用户提示
|
|
262
|
+
*
|
|
263
|
+
* @param error - 捕获到的任意错误值
|
|
264
|
+
* @param config - 格式化文件大小提示所需的配置
|
|
265
|
+
* @returns 可直接发送给用户的中文错误消息
|
|
266
|
+
*/
|
|
116
267
|
export declare function formatRenderError(error: unknown, config: Pick<Config, "maxFileSize">): string;
|
|
268
|
+
/** 已结合全局配置和群聊覆盖项的最终发送选项 */
|
|
117
269
|
export interface ResolvedSendOptions {
|
|
270
|
+
/** 当前 QQ 机器人的接入方式 */
|
|
118
271
|
qqBotType: QqBotType;
|
|
272
|
+
/** 多张图片的发送方式 */
|
|
119
273
|
sendMode: SendMode;
|
|
274
|
+
/** 是否引用并按平台能力提及发送者 */
|
|
120
275
|
replyAndMention: boolean;
|
|
276
|
+
/** 是否在图片前显示视图标题 */
|
|
121
277
|
showViewTitles: boolean;
|
|
278
|
+
/** 是否生成并发送六面正交合成图 */
|
|
122
279
|
sixFaceOverview: boolean;
|
|
123
280
|
}
|
|
281
|
+
/** 解析消息发送选项所需的配置子集 */
|
|
124
282
|
type SendConfig = Pick<Config, "sendAsForward" | "replyAndMention" | "groupSendOptions"> & Partial<Pick<Config, "qqBotType" | "showViewTitles" | "sixFaceOverview">>;
|
|
283
|
+
/**
|
|
284
|
+
* 合并全局发送配置与指定群聊最后一条匹配的覆盖项
|
|
285
|
+
*
|
|
286
|
+
* @param config - 全局发送配置及群聊覆盖项
|
|
287
|
+
* @param groupId - 当前 QQ 群号
|
|
288
|
+
* @returns 适用于当前会话的最终发送选项
|
|
289
|
+
*/
|
|
125
290
|
export declare function resolveSendOptions(config: SendConfig, groupId?: string): ResolvedSendOptions;
|
|
291
|
+
/**
|
|
292
|
+
* 根据 QQ 接入类型和发送模式发送渲染图及投影元数据
|
|
293
|
+
*
|
|
294
|
+
* @remarks
|
|
295
|
+
* 官方 QQ 会先将所有图片合成为一张总览图,自建 QQ 支持合并转发或联合消息
|
|
296
|
+
*
|
|
297
|
+
* @param session - 接收渲染结果的 Koishi 会话
|
|
298
|
+
* @param images - 已写入磁盘的渲染图片
|
|
299
|
+
* @param metadata - 格式化后的投影元数据
|
|
300
|
+
* @param options - 已解析的发送选项
|
|
301
|
+
* @param projectionName - 在成功消息中展示的投影名称
|
|
302
|
+
* @returns 所有图片和元数据发送完成后解决的 Promise
|
|
303
|
+
* @throws 图片读取、合成或消息发送失败时抛出错误
|
|
304
|
+
*/
|
|
126
305
|
export declare function sendImages(session: Session, images: ImageResult[], metadata: string, options: ResolvedSendOptions, projectionName?: string): Promise<void>;
|
|
306
|
+
/**
|
|
307
|
+
* 将轴测图和可选六面图排版为官方 QQ 使用的单张总览 PNG
|
|
308
|
+
*
|
|
309
|
+
* @param images - 要合成的磁盘 PNG 图片
|
|
310
|
+
* @returns 写入首张图片所在目录的总览图绝对路径
|
|
311
|
+
* @throws 图片列表为空、图片无效或文件系统操作失败时抛出错误
|
|
312
|
+
*/
|
|
127
313
|
export declare function composeQqOverview(images: ImageResult[]): Promise<string>;
|
|
314
|
+
/**
|
|
315
|
+
* 创建适合当前 QQ 接入方式的引用和提及消息元素
|
|
316
|
+
*
|
|
317
|
+
* @param session - 要回复的 Koishi 会话
|
|
318
|
+
* @param qqBotType - QQ 机器人接入方式
|
|
319
|
+
* @returns 官方 QQ 最多包含引用,自建 QQ 还会包含发送者提及
|
|
320
|
+
*/
|
|
128
321
|
export declare function replyElements(session: Session, qqBotType?: QqBotType): h[];
|
|
322
|
+
/**
|
|
323
|
+
* 从 Litematic 文件读取展示和限制校验所需的元数据
|
|
324
|
+
*
|
|
325
|
+
* @param data - Gzip 压缩或未压缩的 Litematic NBT 数据
|
|
326
|
+
* @returns 规范化的作者、时间、尺寸和版本信息
|
|
327
|
+
* @throws 数据不是有效 NBT 或包含不支持的标签类型时抛出错误
|
|
328
|
+
*/
|
|
129
329
|
export declare function parseLitematicMetadata(data: Buffer): LitematicMetadata;
|
|
330
|
+
/**
|
|
331
|
+
* 校验文件声明值及每个区域的实际总体积是否超过资源限制
|
|
332
|
+
*
|
|
333
|
+
* @remarks
|
|
334
|
+
* 无效的自定义限制会回退为 5000000,区域体积使用绝对尺寸计算
|
|
335
|
+
*
|
|
336
|
+
* @param data - 原始 Litematic NBT 数据
|
|
337
|
+
* @param metadata - 预先解析的 Litematic 元数据
|
|
338
|
+
* @param configuredLimit - 允许的方块数和总体积上限
|
|
339
|
+
* @returns 校验通过时无返回值
|
|
340
|
+
* @throws 声明值、区域总体积或计算结果超过限制时抛出错误
|
|
341
|
+
*/
|
|
130
342
|
export declare function validateProjectionLimits(data: Buffer, metadata: LitematicMetadata, configuredLimit?: number): void;
|
|
343
|
+
/**
|
|
344
|
+
* 将 Litematic 元数据格式化为适合聊天消息展示的多行文本
|
|
345
|
+
*
|
|
346
|
+
* @param metadata - 已解析的 Litematic 元数据
|
|
347
|
+
* @param filename - 可选的源文件名,用于显示投影名称
|
|
348
|
+
* @returns 包含作者、时间、数量、尺寸及版本的中文文本
|
|
349
|
+
*/
|
|
131
350
|
export declare function formatLitematicMetadata(metadata: LitematicMetadata, filename?: string): string;
|
|
351
|
+
/**
|
|
352
|
+
* 解析 Litematic 所有区域中的非空气方块
|
|
353
|
+
*
|
|
354
|
+
* @param data - Gzip 压缩或未压缩的 Litematic NBT 数据
|
|
355
|
+
* @returns 带世界坐标和方块名称的非空气方块列表
|
|
356
|
+
* @throws 文件缺少区域数据或不包含非空气方块时抛出错误
|
|
357
|
+
*/
|
|
132
358
|
export declare function parseLitematic(data: Buffer): Block[];
|
|
359
|
+
/**
|
|
360
|
+
* 使用内置 CPU 栅格器生成正向和反向轴测 PNG
|
|
361
|
+
*
|
|
362
|
+
* @param blocks - 要渲染的非空气方块
|
|
363
|
+
* @param config - 控制尺寸、背景和方块比例的插件配置
|
|
364
|
+
* @returns 两张带固定文件名的内存 PNG
|
|
365
|
+
*/
|
|
133
366
|
export declare function renderSchematic(blocks: Block[], config: Config): {
|
|
134
367
|
title: string;
|
|
135
368
|
png: Buffer<ArrayBuffer>;
|
|
136
369
|
}[];
|
|
370
|
+
/**
|
|
371
|
+
* 将投影的上下东西南北视图合成为一张正交总览 PNG
|
|
372
|
+
*
|
|
373
|
+
* @param blocks - 要投影的非空气方块
|
|
374
|
+
* @param config - 控制输出尺寸和背景的插件配置
|
|
375
|
+
* @param layout - 六张视图采用横向 3×2 或纵向 2×3 排列
|
|
376
|
+
* @returns 包含六个方向标签的 PNG 数据
|
|
377
|
+
* @throws 方块列表为空时抛出错误
|
|
378
|
+
*/
|
|
137
379
|
export declare function renderSixFaceOverview(blocks: Block[], config: Config, layout?: SixFaceLayout): Buffer<ArrayBuffer>;
|
|
138
380
|
export {};
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Config=exports.inject=exports.name=void 0,exports.apply=apply,exports.hashRenderConfiguration=hashRenderConfiguration,exports.patchOneBotAdapterSource=patchOneBotAdapterSource,exports.cacheNameSegment=cacheNameSegment,exports.enforceCacheLimit=enforceCacheLimit,exports.findLitematicFile=findLitematicFile,exports.supportsStandaloneJavaVersion=supportsStandaloneJavaVersion,exports.standaloneJvmOptions=standaloneJvmOptions,exports.isJavaMemoryFailure=isJavaMemoryFailure,exports.formatRenderError=formatRenderError,exports.resolveSendOptions=resolveSendOptions,exports.sendImages=sendImages,exports.composeQqOverview=composeQqOverview,exports.replyElements=replyElements,exports.parseLitematicMetadata=parseLitematicMetadata,exports.validateProjectionLimits=validateProjectionLimits,exports.formatLitematicMetadata=formatLitematicMetadata,exports.parseLitematic=parseLitematic,exports.renderSchematic=renderSchematic,exports.renderSixFaceOverview=renderSixFaceOverview;const koishi_1=require("koishi"),node_crypto_1=require("node:crypto"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_url_1=require("node:url"),node_child_process_1=require("node:child_process"),node_zlib_1=require("node:zlib"),pngjs_1=require("pngjs"),Sentry=require("@sentry/node-core/light");exports.name="litematic-renderer",exports.inject={optional:["puppeteer","server"]};const CACHE_FORMAT_VERSION=14,packageVersion=require("../package.json").version,PLUGIN_VERSION="string"==typeof packageVersion?packageVersion:"unknown";exports.Config=koishi_1.Schema.intersect([koishi_1.Schema.object({qqBotType:koishi_1.Schema.union([koishi_1.Schema.const("official").description("QQ 官方机器人"),koishi_1.Schema.const("selfHosted").description("自建 QQ(OneBot / NapCat)")]).role("radio").default("selfHosted").description("机器人接入类型;决定图片、合并转发、引用和 @ 的发送方式"),patchOneBotGroupUpload:koishi_1.Schema.boolean().default(!0).description("自建 QQ:启动时修复 OneBot 群文件上传事件;官方机器人不使用此项")}).description("机器人接入"),koishi_1.Schema.object({renderEngine:koishi_1.Schema.union([koishi_1.Schema.const("standalone"),koishi_1.Schema.const("java"),koishi_1.Schema.const("webgl"),koishi_1.Schema.const("cpu")]).default("standalone").description("渲染引擎;standalone 为无需客户端的独立 Java 资源包渲染器"),maxFileSize:koishi_1.Schema.natural().min(1).default(1024).description("自动处理的最大文件大小(KB)"),maxBlocks:koishi_1.Schema.natural().min(1).max(2e9).default(5e6).description("投影区域体积及声明方块数上限;用于拒绝尺寸异常或资源消耗过大的投影"),outputSize:koishi_1.Schema.natural().min(128).max(4096).default(1024).description("六面图和 CPU 渲染最长边的最大像素数"),background:koishi_1.Schema.string().default("#000000").description("PNG 背景颜色"),transparentBackground:koishi_1.Schema.boolean().default(!1).description("输出透明背景"),isometricFill:koishi_1.Schema.percent().default(.78).description("正二轴测主体在画布中的最大占比"),isometricRotation:koishi_1.Schema.number().min(0).max(360).step(1).default(135).description("Isometric Renders 绕竖直轴的基准旋转角"),isometricSlant:koishi_1.Schema.number().min(-90).max(90).step(1).default(36).description("Isometric Renders 俯仰角;36° 为正二轴测预设")}).description("渲染设置"),koishi_1.Schema.object({sendAsForward:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:开启为合并转发,关闭为联合发送;官方 QQ 忽略此项"),showViewTitles:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:发送图片时显示视图标题"),replyAndMention:koishi_1.Schema.boolean().default(!1).description("自建 QQ 会引用并 @ 发送者;官方 QQ 仅引用,避免显示 OpenID"),sixFaceOverview:koishi_1.Schema.boolean().default(!0).description("合并转发时生成并附加上、下、东、南、西、北六面正交合成图"),sixFaceLayout:koishi_1.Schema.union([koishi_1.Schema.const("horizontal").description("横向 3×2"),koishi_1.Schema.const("vertical").description("纵向 2×3")]).default("horizontal").description("六面正交合成图布局;最长边由 outputSize 控制"),groupSendOptions:koishi_1.Schema.array(koishi_1.Schema.object({groupId:koishi_1.Schema.string().description("QQ群号"),sendMode:koishi_1.Schema.union([koishi_1.Schema.const("forward").description("合并转发"),koishi_1.Schema.const("combined").description("联合发送(一条普通消息)")]).default("forward"),replyAndMention:koishi_1.Schema.union([koishi_1.Schema.const("inherit").description("继承全局设置"),koishi_1.Schema.const("enabled").description("开启回复 @"),koishi_1.Schema.const("disabled").description("关闭回复 @")]).default("inherit")})).default([]).description("按群覆盖发送方式和回复设置;官方 QQ 只使用引用开关,自建 QQ 同时使用发送模式和 @")}).description("发送设置"),koishi_1.Schema.object({standaloneJavaCommand:koishi_1.Schema.string().default("").description("独立渲染器使用的 Java 可执行文件;推荐 Java 21+,留空自动查找"),minecraftJarPath:koishi_1.Schema.string().default("").description("可选的 Minecraft 客户端 JAR 或基础资源包;留空使用内置 26.2 原版资源"),resourcePackPaths:koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("自定义材质包路径;越靠后优先级越高"),standaloneRendererJar:koishi_1.Schema.string().default("").description("独立渲染器 JAR;留空使用插件内置版本"),standaloneRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("独立 Java 渲染超时(毫秒)"),standaloneJavaMaxHeapMb:koishi_1.Schema.natural().min(128).max(32768).step(8).default(200).description("首次独立渲染的最大堆内存(MiB)"),standaloneJavaRetryMaxHeapMb:koishi_1.Schema.natural().min(256).max(32768).step(128).default(2048).description("内存不足时新进程重试的最大堆内存(MiB)"),standaloneJavaMemoryRestartLimit:koishi_1.Schema.natural().max(3).default(1).description("检测到内存不足后启动全新进程重试的次数"),javaResolution:koishi_1.Schema.natural().min(256).max(4096).default(1024).description("Java 渲染最终输出边长"),javaSupersampling:koishi_1.Schema.natural().min(1).max(4).default(2).description("离屏超采样倍数")}).description("独立渲染器"),koishi_1.Schema.object({renderTimeout:koishi_1.Schema.natural().min(1e3).default(3e4).description("文件下载超时(毫秒)"),cacheDirectory:koishi_1.Schema.string().default("data/litematic-renderer-cache").description("持久缓存目录;按插件版本和投影 SHA-256 分区"),cacheMaxSizeGb:koishi_1.Schema.number().min(1).max(1024).step(1).default(20).description("所有版本缓存总上限(GiB),超出后按最久未使用清理"),diagnosticsFilePath:koishi_1.Schema.string().default("data/litematic-renderer-diagnostics.json").description("渲染诊断持久化文件路径;导出地址固定为 /litematic-renderer/diagnostics"),sentryDsn:koishi_1.Schema.string().default("").description("Sentry DSN,用于上报渲染错误;留空时读取 SENTRY_DSN 环境变量").experimental()}).description("缓存与诊断"),koishi_1.Schema.object({isometricCellSize:koishi_1.Schema.natural().min(2).max(32).default(7).description("CPU 正二轴测方块菱形半宽"),javaRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("等待 Minecraft Java 渲染桥的超时(毫秒)"),webglQuality:koishi_1.Schema.union([koishi_1.Schema.const("standard"),koishi_1.Schema.const("high"),koishi_1.Schema.const("ultra")]).default("high").description("WebGL 渲染质量"),webglWidth:koishi_1.Schema.natural().min(256).max(2048).default(800).description("WebGL 单张图宽度"),webglHeight:koishi_1.Schema.natural().min(256).max(2048).default(600).description("WebGL 单张图高度"),isometricSquare:koishi_1.Schema.boolean().default(!0).description("将 WebGL 正二轴测图输出为正方形"),javaBridgeDirectory:koishi_1.Schema.string().default("").description("renderEngine=java 时使用的 Fabric Java 渲染桥任务目录"),gpuRendererCommand:koishi_1.Schema.string().default("").description("可选外部 GPU 渲染器命令;非空时优先尝试,失败后回退到 renderEngine")}).description("高级设置")]);const activeStandaloneJavaProcesses=new Set;function apply(e,t){const i=e.logger(exports.name),a=1024*t.maxFileSize,n=t.sentryDsn||process.env.SENTRY_DSN||"";n&&Sentry.init({dsn:n,tracesSampleRate:1});const r=(e,i)=>{n&&Sentry.withScope(a=>{a.setFingerprint([i]),a.setTag("entrypoint",i),a.setContext("render",{engine:t.renderEngine}),Sentry.captureException(e)})};e.on("dispose",()=>terminateActiveStandaloneJavaProcesses(i)),"selfHosted"===t.qqBotType&&ensureOneBotGroupUploadCompatibility(i,t.patchOneBotGroupUpload);const o=(0,node_path_1.resolve)(stringOrDefault(t.diagnosticsFilePath,"data/litematic-renderer-diagnostics.json")),s=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-archive"),c=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-disabled");rotateDiagnosticsFile(o,s,i);const d=(0,node_path_1.resolve)(stringOrDefault(t.cacheDirectory,"data/litematic-renderer-cache")),l=(0,node_path_1.join)(d,`v${cachePathSegment(PLUGIN_VERSION)}`),m=Math.floor(t.cacheMaxSizeGb*1024**3),u=new Map;node_fs_1.promises.mkdir(l,{recursive:!0}).then(()=>enforceCacheLimit(d,m)).catch(e=>i.warn(`缓存初始化失败:${e instanceof Error?e.message:String(e)}`));const h=async(n,r="schematic.litematic",s,c=!1)=>{const h=s??await download(e,n,a,t.renderTimeout),f=parseLitematicMetadata(h);validateProjectionLimits(h,f,t.maxBlocks);const p=formatLitematicMetadata(f,r),g=projectionNameFromFilename(r),_=(0,node_crypto_1.createHash)("sha256").update(h).digest("hex"),w="standalone"===t.renderEngine?await resolveMinecraftResources(t):"",y="standalone"===t.renderEngine?await fingerprintFiles([w,...resourcePackPaths(t.resourcePackPaths)]):[],S=hashRenderConfiguration({version:CACHE_FORMAT_VERSION,outputSize:t.outputSize,isometricCellSize:t.isometricCellSize,background:t.background,transparentBackground:t.transparentBackground,gpuRendererCommand:t.gpuRendererCommand,renderEngine:t.renderEngine,standaloneRendererJar:t.standaloneRendererJar,resourceFingerprint:y,javaResolution:t.javaResolution,javaSupersampling:t.javaSupersampling,webglQuality:t.webglQuality,webglWidth:t.webglWidth,webglHeight:t.webglHeight,isometricSquare:t.isometricSquare,isometricFill:t.isometricFill,isometricRotation:t.isometricRotation,isometricSlant:t.isometricSlant,sixFaceLayout:t.sixFaceLayout}),v=cacheNameSegment(r),b=(0,node_path_1.join)(l,`${v}-${_}-${S}`),x=(0,node_path_1.join)(b,"projection.litematic");await node_fs_1.promises.mkdir(b,{recursive:!0}),await exists(x)||await node_fs_1.promises.writeFile(x,h),await ensureCacheMetadata(b,{pluginVersion:PLUGIN_VERSION,cacheFormatVersion:CACHE_FORMAT_VERSION,contentSha256:_,renderConfigSha256:S,sourceFilename:(0,node_path_1.basename)(r),createdAt:(new Date).toISOString()});const k=["isometric.png","isometric-reverse.png"].map(e=>(0,node_path_1.join)(b,e));if((await Promise.all(k.map(exists))).every(Boolean))i.debug(`命中投影缓存:${_}`);else{let a=u.get(b);a?i.debug(`等待相同投影的缓存任务:${_}`):(a=(async()=>{try{if((await Promise.all(k.map(exists))).every(Boolean))return;if(!Boolean(t.gpuRendererCommand&&await renderWithGpu(t.gpuRendererCommand,x,b,t.renderTimeout,i))||!(await Promise.all(k.map(exists))).every(Boolean))if("standalone"===t.renderEngine)await renderWithStandalone(x,b,w,t,i),await mergeRenderDiagnostics(b,o,i);else if("java"===t.renderEngine)await renderWithJavaBridge(x,b,t);else{const a=parseLitematic(h);let n;if("webgl"===t.renderEngine&&e.puppeteer)try{n=await renderWithWebgl(e,h,t)}catch(e){i.warn(`WebGL 渲染失败,回退 CPU:${e instanceof Error?e.message:String(e)}`),n=renderSchematic(a,t)}else n=renderSchematic(a,t);await Promise.all(n.map(e=>node_fs_1.promises.writeFile((0,node_path_1.join)(b,e.title),e.png)))}if(!(await Promise.all(k.map(exists))).every(Boolean))throw new Error("渲染器没有生成两张正二轴测 PNG")}catch(e){throw await appendRenderError(b,o,t.renderEngine,e,i),e}})().finally(()=>u.delete(b)),u.set(b,a)),await a}const M=k.map((e,t)=>({title:["正二轴测","反向正二轴测(旋转 180°)"][t],path:e}));if(c){const e=(0,node_path_1.join)(b,"six-faces.png");if(!await exists(e)){const a=`${b}:six-face`;let n=u.get(a);n||(n=(async()=>{try{if("standalone"===t.renderEngine)try{await renderWithStandalone(x,b,w,t,i,"six-face")}catch(e){i.warn(`真实材质六面图生成失败,回退方块颜色投影:${e instanceof Error?e.message:String(e)}`)}if(!await exists(e)){const i=parseLitematic(h);await node_fs_1.promises.writeFile(e,renderSixFaceOverview(i,t,t.sixFaceLayout))}}catch(e){i.warn(`六面正交图生成失败,继续发送轴测图:${e instanceof Error?e.message:String(e)}`)}})().finally(()=>u.delete(a)),u.set(a,n)),await n}await exists(e)&&M.push({title:"六面正投影",path:e})}return await touchCacheEntry(b),await enforceCacheLimit(d,m,b),{images:M,metadata:p,projectionName:g}};e.middleware(async(e,n)=>{if(!e.guildId)return n();const s=findLitematicFile(e);if(!s)return n();const c=await resolveFileUrl(e,s);if(!c)return i.warn(`检测到 Litematic 文件但无法获取下载地址:${fileName(s)??"unknown.litematic"};请检查 OneBot/NapCat 的 get_group_file_url 接口`),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:"检测到投影文件,但无法获取群文件下载地址,请检查机器人文件接口"})]),n();if(isOverLimit(s.size,a))return await appendGlobalRenderError(o,`input:${(0,node_path_1.basename)(fileName(s)??"unknown.litematic")}`,"input",`file size exceeds ${t.maxFileSize} KB`,i),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,无法渲染`})]),n();if(!isUnderLimit(s.size,a))return n();try{const i=resolveSendOptions(t,e.guildId),a=await h(c,fileName(s)??"schematic.litematic",void 0,i.sixFaceOverview);await sendImages(e,a.images,a.metadata,i,a.projectionName)}catch(a){return r(a,"group-file-upload"),i.warn(a),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:formatRenderError(a,t)})]),n()}return n()}),e.command("litematic.render <url:string>","渲染可直接下载的 .litematic 文件").action(async({session:e},i)=>{if(!i)return"请提供 .litematic 文件的直链";if(!e)return"此命令只能在消息会话中执行";try{const a=resolveSendOptions(t,e.guildId),n=await h(i,"schematic.litematic",void 0,a.sixFaceOverview);await sendImages(e,n.images,n.metadata,a,n.projectionName)}catch(e){return r(e,"litematic.render"),formatRenderError(e,t)}}),e.command("litematic.cache.clear","清理投影渲染缓存",{authority:3}).action(async()=>(await node_fs_1.promises.rm(d,{recursive:!0,force:!0}),await node_fs_1.promises.mkdir(l,{recursive:!0}),"投影渲染缓存已清理")),e.command("litematic.errors.export","export render diagnostics",{authority:3}).action(async()=>`Diagnostics file: ${o}`),e.command("litematic.errors.disable","disable current render diagnostics snapshot",{authority:3}).action(async()=>disableDiagnosticsSnapshot(o,c));const f=e.server;f?.get&&f.get("/litematic-renderer/diagnostics",async e=>{e.type="application/json",e.set("Content-Disposition",'attachment; filename="litematic-renderer-diagnostics.json"'),e.body=await node_fs_1.promises.readFile(o,"utf8").catch(()=>'{"format":2,"blocks":[],"errors":[]}')})}function hashRenderConfiguration(e){return(0,node_crypto_1.createHash)("sha256").update(JSON.stringify(e)).digest("hex").slice(0,16)}function patchOneBotAdapterSource(e){if(/^\s*case ["']group_upload["']:/m.test(e))return{source:e,changed:!1,alreadyCompatible:!0};const t=e.match(/\(0,\s*(import_[\w$]+)\.hyphenate\)\(data\.sub_type\)/)?.[1]??"import_koishi2",i=/ \/\/ https:\/\/github\.com\/koishijs\/koishi-plugin-adapter-onebot\/issues\/33\r?\n \/\/ case 'offline_file':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'private'\r?\n \/\/ session\.isDirect = true\r?\n \/\/ session\.subsubtype = 'offline-file-added'\r?\n \/\/ break\r?\n \/\/ case 'group_upload':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'group'\r?\n \/\/ session\.subsubtype = 'guild-file-added'\r?\n \/\/ break/;if(!i.test(e))return{source:e,changed:!1,alreadyCompatible:!1};const a=[" // Restored by koishi-plugin-litematic-renderer so NapCat group file uploads reach middleware.",' case "offline_file":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "private";'," session.isDirect = true;",' session.subsubtype = "offline-file-added";'," break;",' case "group_upload":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "group";',' session.subsubtype = "guild-file-added";'," break;"].join("\n");return{source:e.replace(i,a),changed:!0,alreadyCompatible:!1}}async function ensureOneBotGroupUploadCompatibility(e,t){if(!t)return;let i;try{i=require.resolve("koishi-plugin-adapter-onebot/package.json",{paths:[process.cwd(),__dirname]})}catch{return void e.debug("未安装 koishi-plugin-adapter-onebot,跳过群文件上传事件兼容修复")}try{const t=(0,node_path_1.dirname)(i),a=JSON.parse(await node_fs_1.promises.readFile(i,"utf8")),n=(0,node_path_1.join)(t,"lib","index.js"),r=await node_fs_1.promises.readFile(n,"utf8"),o=patchOneBotAdapterSource(r);if(o.alreadyCompatible)return void e.debug(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 已支持 group_upload`);if(!o.changed)return void e.warn(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 未识别到可自动修复的 group_upload 代码段`);const s=`${n}.litematic-renderer.bak`;await exists(s)||await node_fs_1.promises.writeFile(s,r),await node_fs_1.promises.writeFile(n,o.source),e.warn(`已修复 koishi-plugin-adapter-onebot@${a.version??"unknown"} 的群文件上传事件映射;请重启 Koishi 让已加载的 OneBot 适配器生效`)}catch(t){e.warn(`修复 OneBot 群文件上传事件失败:${t instanceof Error?t.message:String(t)}`)}}function cachePathSegment(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")||"unknown"}function cacheNameSegment(e){return((0,node_path_1.basename)(e||"schematic.litematic",(0,node_path_1.extname)(e||"")).replace(/[<>:"/\\|?*\u0000-\u001f]/g,"_").replace(/[. ]+$/g,"").trim()||"schematic").slice(0,80)}function projectionNameFromFilename(e){return(0,node_path_1.basename)(e||"schematic.litematic").replace(/\.litematic$/i,"").trim()||"schematic"}async function ensureCacheMetadata(e,t){const i=(0,node_path_1.join)(e,"cache.json");await exists(i)||await node_fs_1.promises.writeFile(i,JSON.stringify(t,null,2)+"\n")}async function touchCacheEntry(e){const t=new Date;await node_fs_1.promises.utimes(e,t,t).catch(()=>{})}async function directorySize(e){let t=0;const i=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const a of i){const i=(0,node_path_1.join)(e,a.name);a.isDirectory()?t+=await directorySize(i):a.isFile()&&(t+=(await node_fs_1.promises.stat(i)).size)}return t}async function enforceCacheLimit(e,t,i){const a=[],n=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const t of n){if(!t.isDirectory())continue;const i=(0,node_path_1.join)(e,t.name),n=await node_fs_1.promises.readdir(i,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const t=(0,node_path_1.join)(i,e.name),[n,r]=await Promise.all([directorySize(t),node_fs_1.promises.stat(t)]);a.push({path:t,size:n,lastUsed:r.mtimeMs})}}let r=a.reduce((e,t)=>e+t.size,0),o=0,s=0;const c=i&&(0,node_path_1.resolve)(i);a.sort((e,t)=>e.lastUsed-t.lastUsed);for(const e of a){if(r<=t)break;c&&(0,node_path_1.resolve)(e.path)===c||(await node_fs_1.promises.rm(e.path,{recursive:!0,force:!0}),r-=e.size,o+=e.size,s++)}return{totalBytes:r,removedBytes:o,removedEntries:s}}function fileElements(e){return[...e.elements??[],...koishi_1.h.parse(e.content??"")]}function findLitematicFile(e){const t=fileElements(e).filter(e=>"file"===e.type),i=t.find(e=>".litematic"===(0,node_path_1.extname)(fileName(e.attrs)??"").toLowerCase());if(i?.attrs)return i.attrs;for(const t of[e.event,e._data,e.data,e.message]){const e=findLitematicInFilePayload(t);if(e)return e}const a=inferLitematicName(e.content);return a&&t[0]?.attrs?{...t[0].attrs,name:a}:void 0}function findLitematicInFilePayload(e,t=new Set,i=0){if(!e||"object"!=typeof e||i>6||t.has(e))return;t.add(e);const a=e,n=a,r=n.file&&"object"==typeof n.file?n.file:void 0;if(r&&fileName(r)&&".litematic"===(0,node_path_1.extname)(fileName(r)).toLowerCase())return{...r,id:r.id??n.id,file_id:r.file_id??n.file_id,fileId:r.fileId??n.fileId,busid:r.busid??n.busid};if(fileName(n)&&".litematic"===(0,node_path_1.extname)(fileName(n)).toLowerCase())return n;for(const[e,n]of Object.entries(a))if(n&&"object"==typeof n&&/file|upload|message|data|event/i.test(e)){const e=findLitematicInFilePayload(n,t,i+1);if(e)return e}}function inferLitematicName(e){if(!e)return;const t=e.match(/(?:file|文件)\s+(.+?\.litematic)(?=$|[\]】\r\n])/i);return t?.[1]?t[1].trim():e.match(/([^\s\]】<>"']+\.litematic)(?=$|[\s\]】])/i)?.[1]?.trim()}function fileName(e){const t=e.file&&"object"==typeof e.file?e.file:void 0,i=e.name??e.filename??e.file_name??e.fileName??t?.name??t?.filename??t?.file_name??t?.fileName;return"string"==typeof i&&i.trim()?i.trim():void 0}async function resolveFileUrl(e,t){const i=t.file&&"object"==typeof t.file?t.file:void 0,a=t.url??t.src??("string"==typeof t.file?t.file:void 0)??i?.url??i?.src;if("string"==typeof a&&a)return a;const n=t.id??t.file_id??t.fileId??i?.id??i?.file_id??i?.fileId,r=e.bot.internal;if(!e.guildId||!n||"function"!=typeof r?.getGroupFileUrl)return;const o=await r.getGroupFileUrl(e.guildId,n,Number(t.busid??i?.busid??0));return"string"==typeof o?o:o?.url}function isUnderLimit(e,t){if(null==e||""===e)return!0;const i=Number(e);return Number.isFinite(i)&&i>0&&i<=t}async function download(e,t,i,a){const n=await e.http.get(t,{responseType:"arraybuffer",timeout:a}),r=Buffer.from(n);if(r.byteLength>i)throw new Error(`文件超过 ${(i/1024/1024).toFixed(2)} MB 限制`);return r}async function exists(e){try{return await node_fs_1.promises.access(e,node_fs_1.constants.R_OK),!0}catch{return!1}}function stringOrDefault(e,t){return"string"==typeof e&&e.trim()?e:t}function resourcePackPaths(e){return Array.isArray(e)?e.filter(e=>"string"==typeof e&&Boolean(e.trim())):[]}async function fingerprintFiles(e){return Promise.all(e.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(async e=>{const t=(0,node_path_1.resolve)(e);try{const e=await node_fs_1.promises.stat(t);return{path:t,size:e.size,modified:e.mtimeMs}}catch{return{path:t,missing:!0}}}))}async function resolveStandaloneJavaCommand(e){const t=e.trim();if(t)return t;const i="win32"===process.platform?"java.exe":"java",a=new Set;if(process.env.JAVA_HOME&&a.add((0,node_path_1.join)(process.env.JAVA_HOME,"bin",i)),"win32"===process.platform){const e=new Set([(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Java"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Eclipse Adoptium"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Microsoft","jdk")]);for(const t of e)try{const e=await node_fs_1.promises.readdir(t,{withFileTypes:!0});for(const n of e)n.isDirectory()&&/(?:^|[-_])21(?:[._-]|$)/.test(n.name)&&a.add((0,node_path_1.join)(t,n.name,"bin",i))}catch{}}a.add("java");for(const e of a)if(supportsStandaloneJavaVersion(await javaMajorVersion(e)))return e;throw new Error("未找到 Java 21+;请安装 Java 21 或更高版本,设置 JAVA_HOME,或填写 standaloneJavaCommand")}function isOverLimit(e,t){if(null==e||""===e)return!1;const i=Number(e);return Number.isFinite(i)&&i>t}async function javaMajorVersion(e){return new Promise(t=>{let i="",a=!1;const n=e=>{a||(a=!0,clearTimeout(s),t(e))},r=(0,node_child_process_1.spawn)(e,["-version"],{shell:!1,windowsHide:!0}),o=e=>{i=(i+String(e)).slice(-2e3)};r.stdout?.on("data",o),r.stderr?.on("data",o),r.once("error",()=>n()),r.once("close",()=>{const e=i.match(/(?:java|openjdk) version "(\d+)/i);n(e?Number(e[1]):void 0)});const s=setTimeout(()=>{r.kill(),n()},5e3)})}function supportsStandaloneJavaVersion(e){return void 0!==e&&e>=21}function standaloneJvmOptions(e){return["-Xms128m",`-Xmx${Math.max(128,Math.min(32768,Math.floor(e)))}m`,"-XX:+UseG1GC","-XX:+UseStringDeduplication","-XX:+ExitOnOutOfMemoryError"]}function isJavaMemoryFailure(e,t){return 137===t||/outofmemoryerror|java heap space|gc overhead limit|cannot reserve enough space|native memory allocation|insufficient memory|malloc failed|os::commit_memory/i.test(e)}async function resolveMinecraftResources(e){const t="string"==typeof e.minecraftJarPath?e.minecraftJarPath.trim():"";if(t&&await exists((0,node_path_1.resolve)(t)))return(0,node_path_1.resolve)(t);const i=(0,node_path_1.resolve)((0,node_path_1.join)(__dirname,"../assets/vanilla-resources/Minecraft-26.2-Vanilla-Resources.zip"));if(!await exists(i))throw new Error(`插件内置的 Minecraft 26.2 原版资源包不存在:${i}`);return i}async function renderWithStandalone(e,t,i,a,n,r="isometric"){const o=(0,node_path_1.resolve)(a.standaloneRendererJar||(0,node_path_1.join)(__dirname,"../assets/standalone-renderer/litematic-standalone-renderer-0.2.7.jar"));if(!await exists(o))throw new Error(`独立 Java 渲染器不存在:${o}`);if(!await exists(i))throw new Error(`Minecraft 资源 JAR 不存在:${i}`);for(const e of resourcePackPaths(a.resourcePackPaths))if(!await exists((0,node_path_1.resolve)(e)))throw new Error(`材质包不存在:${e}`);const s=["-Djava.awt.headless=true","-jar",o,"--input",(0,node_path_1.resolve)(e),"--output",(0,node_path_1.resolve)(t),"--minecraft-jar",i,"--resolution",String(a.javaResolution),"--supersampling",String(a.javaSupersampling),"--rotation",String(a.isometricRotation),"--slant",String(a.isometricSlant),"--fill",String(a.isometricFill),"--background",a.background];"six-face"===r&&s.push("--six-face-only","--six-face-resolution",String(a.outputSize),"--six-face-layout",a.sixFaceLayout),a.transparentBackground&&s.push("--transparent-background");for(const e of resourcePackPaths(a.resourcePackPaths))s.push("--resource-pack",(0,node_path_1.resolve)(e));const c=await resolveStandaloneJavaCommand(a.standaloneJavaCommand);for(let e=0;e<=a.standaloneJavaMemoryRestartLimit;e++){const i=0===e?a.standaloneJavaMaxHeapMb:a.standaloneJavaRetryMaxHeapMb,o=await runStandaloneJava(c,[...standaloneJvmOptions(i),...s],a.standaloneRenderTimeout);if(0===o.code)return;const d=isJavaMemoryFailure(o.stderr,o.code);if(d&&e<a.standaloneJavaMemoryRestartLimit){n.warn(`独立 Java 渲染超出 ${a.standaloneJavaMaxHeapMb} MiB 堆内存限制;正在启动全新进程重试(${e+1}/${a.standaloneJavaMemoryRestartLimit})`);const i="six-face"===r?["six-faces.png"]:["isometric.png","isometric-reverse.png"];await Promise.all(i.map(e=>node_fs_1.promises.rm((0,node_path_1.join)(t,e),{force:!0})));continue}const l=d?`;Java 内存不足(最大堆 ${a.standaloneJavaMaxHeapMb} MiB,已重启 ${e} 次)`:"";throw new Error(`独立 Java 渲染器退出码 ${o.code}${l}${o.stderr?`:${o.stderr.trim()}`:""}`)}}async function runStandaloneJava(e,t,i){return new Promise((a,n)=>{const r=(0,node_child_process_1.spawn)(e,t,{shell:!1,windowsHide:!0});activeStandaloneJavaProcesses.add(r);let o="",s=!1;const c=()=>activeStandaloneJavaProcesses.delete(r);r.stderr?.on("data",e=>{o=(o+String(e)).slice(-6e3)});const d=setTimeout(()=>{s||(s=!0,(async()=>{await terminateStandaloneJavaProcess(r),c(),n(new Error("Standalone Java render timed out; process terminated and memory reclaimed"))})())},i);r.once("error",e=>{s||(s=!0,clearTimeout(d),terminateStandaloneJavaProcess(r),c(),n(e))}),r.once("close",e=>{s||(s=!0,clearTimeout(d),c(),a({code:e,stderr:o}))})})}async function terminateStandaloneJavaProcess(e){const t=e.pid;t&&("win32"===process.platform&&await new Promise(e=>{const i=(0,node_child_process_1.spawn)("taskkill.exe",["/PID",String(t),"/T","/F"],{windowsHide:!0});i.once("close",()=>e()),i.once("error",()=>e())}),null!=e.exitCode||e.killed||e.kill())}async function terminateActiveStandaloneJavaProcesses(e){const t=[...activeStandaloneJavaProcesses];t.length&&(await Promise.all(t.map(e=>terminateStandaloneJavaProcess(e))),e.info(`已终止 ${t.length} 个独立渲染 Java 进程并回收其子进程`))}async function renderWithJavaBridge(e,t,i){if(!i.javaBridgeDirectory.trim())throw new Error("Fabric Java 渲染桥任务目录未配置;使用 java 后端时请填写 javaBridgeDirectory");const a=(0,node_path_1.resolve)(i.javaBridgeDirectory),n=await readJson((0,node_path_1.join)(a,"status.json"));if(!n?.timestamp||Date.now()-n.timestamp>5e3)throw new Error("Minecraft Java 渲染桥未运行;请启动 1.21.1-Fabric 客户端并进入一个世界");if(!n.inWorld)throw new Error("Minecraft Java 渲染桥已启动,但客户端尚未进入世界");const r=n.resourcePacks??[];if(![["XeKr","3.6forMC1.20.2~1.21.5.zip"],["XKRDA","1.19.4~1.21snapshot.zip"]].every(e=>r.some(t=>e.every(e=>t.includes(e)))))throw new Error("Minecraft 客户端没有同时启用 XeKr 红显基础包和 XKRDA 附加包");const o=(0,node_crypto_1.randomUUID)(),s=(0,node_path_1.join)(a,"jobs"),c=(0,node_path_1.join)(a,"results",`${o}.result.json`),d=(0,node_path_1.join)(s,`${o}.job.json`),l=`${d}.tmp`;await node_fs_1.promises.mkdir(s,{recursive:!0}),await node_fs_1.promises.rm(c,{force:!0}),await node_fs_1.promises.writeFile(l,JSON.stringify({id:o,input:(0,node_path_1.resolve)(e),outputDirectory:(0,node_path_1.resolve)(t),resolution:i.javaResolution,supersampling:i.javaSupersampling,rotation:i.isometricRotation,slant:i.isometricSlant,fill:i.isometricFill,background:i.background,transparentBackground:i.transparentBackground})),await node_fs_1.promises.rename(l,d);const m=Date.now()+i.javaRenderTimeout;for(;Date.now()<m;){const e=await readJson(c);if(e){if(await node_fs_1.promises.rm(c,{force:!0}),!e.success)throw new Error(`Minecraft Java 渲染失败:${e.error??"未知错误"}`);return}await new Promise(e=>setTimeout(e,200))}throw new Error(`Minecraft Java 渲染超过 ${Math.round(i.javaRenderTimeout/1e3)} 秒`)}async function readJson(e){try{return JSON.parse(await node_fs_1.promises.readFile(e,"utf8"))}catch(e){if("ENOENT"===e?.code||e instanceof SyntaxError)return;throw e}}async function mergeRenderDiagnostics(e,t,i){const a=(0,node_path_1.join)(e,"render-diagnostics.json"),n=await readJson(a);if(!n)return;const r=(0,node_path_1.resolve)(t),o=await readJson(r)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},s=new Map;for(const e of o.blocks??[])s.set(`${e.state}\0${e.reason}`,e);for(const e of n.blocks??[]){const t=`${e.state}\0${e.reason}`,i=s.get(t);i?(i.count=Number(i.count??0)+Number(e.count??0),i.samples=[...i.samples??[],...e.samples??[]].slice(0,20)):s.set(t,{...e,samples:e.samples??[]})}const c=new Map;for(const e of o.errors??[])c.set(`${e.renderId}\0${e.engine}\0${e.message}`,e);for(const e of n.errors??[]){const t=`${e.renderId}\0${e.engine}\0${e.message}`,i=c.get(t);i?i.count=Number(i.count??0)+Number(e.count??0):c.set(t,{...e})}const d={format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[...s.values()],errors:[...c.values()]};await node_fs_1.promises.mkdir((0,node_path_1.resolve)(t,".."),{recursive:!0}).catch(()=>{}),await node_fs_1.promises.writeFile(r,JSON.stringify(d,null,2)+"\n"),i.warn(`记录 ${n.blocks?.length??0} 类无法正常渲染的方块;诊断已保存到 ${r}`)}async function rotateDiagnosticsFile(e,t,i){const a=await readJson(e);if(!a||a.pluginVersion===PLUGIN_VERSION)return;await node_fs_1.promises.mkdir(t,{recursive:!0});const n=cachePathSegment("string"==typeof a.pluginVersion?a.pluginVersion:"legacy"),r=formatDiagnosticsTimestamp(new Date),o=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-v${n}-${r}.json`);await node_fs_1.promises.rename(e,o),i.info(`已将旧版诊断汇总归档到 ${o}`)}async function disableDiagnosticsSnapshot(e,t){if(!await readJson(e))return"当前没有可禁用的诊断汇总";await node_fs_1.promises.mkdir(t,{recursive:!0});const i=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-disabled-${formatDiagnosticsTimestamp(new Date)}.json`);return await node_fs_1.promises.rename(e,i),await node_fs_1.promises.writeFile(e,JSON.stringify({format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},null,2)+"\n"),`当前诊断已移入禁用文件夹:${i}`}async function appendGlobalRenderError(e,t,i,a,n){const r=(0,node_path_1.resolve)(e),o=await readJson(r)??{format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.pluginVersion=PLUGIN_VERSION,o.errors??=[];const s=o.errors.find(e=>e.renderId===t&&e.engine===i&&e.message===a);s?s.count=Number(s.count??0)+1:o.errors.push({renderId:t,engine:i,message:a,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.mkdir((0,node_path_1.dirname)(r),{recursive:!0}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),n.warn(`渲染问题已记录到 ${r}`)}function formatDiagnosticsTimestamp(e){const t=e=>String(e).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}-${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}async function appendRenderError(e,t,i,a,n){const r=(0,node_path_1.join)(e,"render-diagnostics.json"),o=await readJson(r)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.errors??=[];const s=a instanceof Error?a.message:String(a),c=(0,node_path_1.basename)(e),d=o.errors.find(e=>e.renderId===c&&e.engine===i&&e.message===s);d?d.count=Number(d.count??0)+1:o.errors.push({renderId:c,engine:i,message:s,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),await mergeRenderDiagnostics(e,t,n)}function formatRenderError(e,t){const i=e instanceof Error?e.message:String(e);return/投影方块数或区域体积超过/i.test(i)?`${i},不渲染`:/文件超过|file size|file.*limit/i.test(i)?`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,不渲染`:/java 21\+|未找到.*java|java.*not found/i.test(i)?"未找到 Java 21+,请在插件配置中检查 Java 可执行文件设置":/standalone renderer|独立.*渲染器.*不存在|渲染器.*not exist/i.test(i)?"独立渲染器不可用,请重装插件或检查 Java 渲染配置":/minecraft.*resource|resource pack|原版资源|材质包.*不存在/i.test(i)?"Minecraft 原版资源包不可用,请检查资源包配置":"投影渲染失败,请检查渲染器配置或导出诊断文件"}async function renderWithGpu(e,t,i,a,n){try{return await new Promise((n,r)=>{const o=(0,node_child_process_1.spawn)(e,["--input",t,"--output",i,"--views","isometric,isometric-reverse"],{shell:!1,windowsHide:!0}),s=setTimeout(()=>{o.kill(),r(new Error("GPU 渲染超时"))},a);o.once("error",r),o.once("exit",e=>0===e?n():r(new Error(`GPU 渲染器退出码:${e}`))),o.once("close",()=>clearTimeout(s))}),!0}catch(e){return n.warn(`GPU 渲染器不可用,回退 CPU:${e instanceof Error?e.message:String(e)}`),!1}}async function renderWithWebgl(e,t,i){const a=await e.puppeteer.page();try{const e=(0,node_url_1.pathToFileURL)((0,node_path_1.resolve)(__dirname,"../assets/threeviews/index.html")).href;await a.goto(e,{waitUntil:"networkidle0",timeout:i.renderTimeout}),await a.waitForFunction(()=>Boolean(window.litematicViewerAPI&&window.deepslateResources),{timeout:i.renderTimeout});const n=await a.evaluate(async({base64:e,width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})=>{const c=atob(e),d=new Uint8Array(c.length);for(let e=0;e<c.length;e++)d[e]=c.charCodeAt(e);const l=new File([d],"schematic.litematic",{type:"application/octet-stream"});return await window.litematicViewerAPI.loadFromFile(l),window.litematicViewerAPI.renderIsometricViews({width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})},{base64:t.toString("base64"),width:i.webglWidth,height:i.webglHeight,quality:i.webglQuality,isometricSquare:i.isometricSquare,isometricFill:i.isometricFill,isometricRotation:i.isometricRotation,isometricSlant:i.isometricSlant});return[{title:"isometric.png",png:decodeDataUrl(n.isometricView)},{title:"isometric-reverse.png",png:decodeDataUrl(n.reverseIsometricView)}]}finally{await a.close().catch(()=>{})}}function decodeDataUrl(e){if("string"!=typeof e||!e.startsWith("data:image/"))throw new Error("WebGL 渲染器没有返回 PNG 数据");const t=e.indexOf(",");if(t<0)throw new Error("WebGL 渲染器返回了无效图像数据");return Buffer.from(e.slice(t+1),"base64")}function resolveSendOptions(e,t){const i=t?[...e.groupSendOptions].reverse().find(e=>e.groupId.trim()===t):void 0,a="enabled"===i?.replyAndMention||"disabled"!==i?.replyAndMention&&e.replyAndMention,n=i?.sendMode??(e.sendAsForward?"forward":"combined"),r=e.qqBotType??"selfHosted";return{qqBotType:r,sendMode:n,replyAndMention:a,showViewTitles:e.showViewTitles??!1,sixFaceOverview:("official"===r||"forward"===n)&&(e.sixFaceOverview??!0)}}async function sendImages(e,t,i,a,n="schematic"){if("official"===a.qqBotType){const r=await composeQqOverview(t),o=[...a.replyAndMention&&e.messageId?[(0,koishi_1.h)("quote",{id:e.messageId})]:[],koishi_1.h.image((0,node_url_1.pathToFileURL)(r).href),(0,koishi_1.h)("text",{content:`${i}\n${n} 已渲染成功,结果如上`})];return void await e.send(o)}const r=await Promise.all(t.map(async({title:e,path:t})=>({title:e,source:`base64://${(await node_fs_1.promises.readFile(t)).toString("base64")}`}))),o=r.map(({title:t,source:i})=>(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影渲染"},[...a.showViewTitles?[(0,koishi_1.h)("text",{content:t})]:[],(0,koishi_1.h)("img",{src:i})])),s=(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影信息"},[(0,koishi_1.h)("text",{content:i})]);if("forward"===a.sendMode)return void await e.send((0,koishi_1.h)("figure",{},[...o,s]));const c=a.replyAndMention?replyElements(e,a.qqBotType):[],d=r.flatMap(({title:e,source:t},i)=>[...a.showViewTitles?[(0,koishi_1.h)("text",{content:`${i?"\n":""}${e}\n`})]:[],(0,koishi_1.h)("img",{src:t})]);await e.send([...c,...d,(0,koishi_1.h)("text",{content:`\n${i}\n${n} 已渲染成功,结果如上`})])}async function composeQqOverview(e){if(!e.length)throw new Error("没有可合并的渲染图片");const t=await Promise.all(e.map(async e=>pngjs_1.PNG.sync.read(await node_fs_1.promises.readFile((0,node_path_1.resolve)(e.path))))),i=Math.max(...t.map(e=>e.width)),a=Math.max(4,Math.round(.01*i)),n=[];let r=0;const o=t.slice(0,Math.min(2,t.length)),s=1===o.length?i:Math.floor((i-a)/2),c=o.map(e=>Math.max(1,Math.round(e.height*s/e.width))),d=Math.max(...c);for(let e=0;e<o.length;e++)n.push({image:o[e],x:e*(s+a),y:Math.floor((d-c[e])/2),width:s,height:c[e]});r+=d;for(const e of t.slice(2)){r+=a;const t=Math.max(1,Math.round(e.height*i/e.width));n.push({image:e,x:0,y:r,width:i,height:t}),r+=t}const l=new pngjs_1.PNG({width:i,height:r});for(const e of n)drawScaledPng(l,e);const m=(0,node_path_1.resolve)((0,node_path_1.dirname)(e[0].path),"qq-overview.png");return await node_fs_1.promises.writeFile(m,pngjs_1.PNG.sync.write(l)),m}function drawScaledPng(e,t){const{image:i,x:a,y:n,width:r,height:o}=t;for(let t=0;t<o;t++){const s=Math.min(i.height-1,Math.floor((t+.5)*i.height/o));for(let o=0;o<r;o++){const c=Math.min(i.width-1,Math.floor((o+.5)*i.width/r)),d=4*(s*i.width+c),l=4*((n+t)*e.width+a+o);i.data.copy(e.data,l,d,d+4)}}}function replyElements(e,t=("qq"===e.platform?"official":"selfHosted")){const i=[];return e.messageId&&i.push((0,koishi_1.h)("quote",{id:e.messageId})),"selfHosted"===t&&e.userId&&(i.push((0,koishi_1.h)("at",{id:e.userId})),i.push((0,koishi_1.h)("text",{content:"\n"}))),i}class NbtReader{data;offset=0;constructor(e){this.data=e}byte(){return this.data.readInt8(this.offset++)}ubyte(){return this.data.readUInt8(this.offset++)}short(){const e=this.data.readInt16BE(this.offset);return this.offset+=2,e}int(){const e=this.data.readInt32BE(this.offset);return this.offset+=4,e}float(){const e=this.data.readFloatBE(this.offset);return this.offset+=4,e}double(){const e=this.data.readDoubleBE(this.offset);return this.offset+=8,e}long(){const e=this.data.readBigInt64BE(this.offset);return this.offset+=8,e}text(){const e=this.data.readUInt16BE(this.offset);this.offset+=2;const t=this.data.toString("utf8",this.offset,this.offset+e);return this.offset+=e,t}readNamedRoot(){if(10!==this.ubyte())throw new Error("不是 NBT Compound 数据");return this.text(),this.value(10)}value(e){switch(e){case 0:return null;case 1:return this.byte();case 2:return this.short();case 3:return this.int();case 4:return this.long();case 5:return this.float();case 6:return this.double();case 7:{const e=this.int(),t=this.data.subarray(this.offset,this.offset+e);return this.offset+=e,t}case 8:return this.text();case 9:{const e=this.ubyte(),t=this.int();return Array.from({length:t},()=>this.value(e))}case 10:{const e={};for(;;){const t=this.ubyte();if(!t)return e;e[this.text()]=this.value(t)}}case 11:{const e=this.int();return Array.from({length:e},()=>this.int())}case 12:{const e=this.int();return Array.from({length:e},()=>this.long())}default:throw new Error(`不支持的 NBT 标签类型:${e}`)}}}const MINECRAFT_DATA_VERSIONS=new Map([[4903,"26.2"],[4790,"26.1.2"],[4671,"1.21.11"],[4557,"1.21.10"],[4555,"1.21.9"],[4440,"1.21.8"],[4438,"1.21.7"],[4435,"1.21.6"],[4325,"1.21.5"],[4189,"1.21.4"],[4082,"1.21.3"],[4080,"1.21.2"],[3955,"1.21.1"],[3953,"1.21"],[3839,"1.20.6"],[3837,"1.20.5"],[3700,"1.20.4"],[3698,"1.20.3"],[3578,"1.20.2"],[3465,"1.20.1"],[3463,"1.20"],[3337,"1.19.4"],[3218,"1.19.3"],[3120,"1.19.2"],[3117,"1.19.1"],[3105,"1.19"],[2975,"1.18.2"]]);function readLitematicRoot(e){let t;try{t=(0,node_zlib_1.gunzipSync)(e)}catch{t=e}return new NbtReader(t).readNamedRoot()}function compoundValue(e){return!e||"object"!=typeof e||Array.isArray(e)||Buffer.isBuffer(e)?void 0:e}function numericValue(e){if("number"==typeof e&&Number.isFinite(e))return e;if("bigint"==typeof e){const t=Number(e);if(Number.isSafeInteger(t))return t}}function readMetadataSize(e,t){const i=compoundValue(t.EnclosingSize),a=i&&[numericValue(i.x),numericValue(i.y),numericValue(i.z)];if(a?.every(e=>null!=e))return a.map(e=>Math.abs(e));const n=compoundValue(e.Regions);if(!n)return;let r=1/0,o=1/0,s=1/0,c=-1/0,d=-1/0,l=-1/0;for(const e of Object.values(n)){const t=compoundValue(e),i=compoundValue(t?.Position),a=compoundValue(t?.Size),n=numericValue(i?.x),m=numericValue(i?.y),u=numericValue(i?.z),h=numericValue(a?.x),f=numericValue(a?.y),p=numericValue(a?.z);if([n,m,u,h,f,p].some(e=>null==e)||0===h||0===f||0===p)continue;const g=n+h-Math.sign(h),_=m+f-Math.sign(f),w=u+p-Math.sign(p);r=Math.min(r,n,g),o=Math.min(o,m,_),s=Math.min(s,u,w),c=Math.max(c,n,g),d=Math.max(d,m,_),l=Math.max(l,u,w)}return Number.isFinite(r)?[c-r+1,d-o+1,l-s+1]:void 0}function formatCreatedAt(e){if(null==e||e<=0)return"未知";const t=new Date(e<1e10?1e3*e:e);if(Number.isNaN(t.getTime()))return"未知";const i=new Intl.DateTimeFormat("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(t),a=Object.fromEntries(i.map(e=>[e.type,e.value]));return`${a.year}-${a.month}-${a.day} ${a.hour}:${a.minute}:${a.second}`}function parseLitematicMetadata(e){const t=readLitematicRoot(e),i=compoundValue(t.Metadata)??{},a=numericValue(t.MinecraftDataVersion);return{author:"string"==typeof i.Author&&i.Author.trim()?i.Author.trim():"未知",createdAt:formatCreatedAt(numericValue(i.TimeCreated)),totalBlocks:numericValue(i.TotalBlocks),totalVolume:numericValue(i.TotalVolume),size:readMetadataSize(t,i),litematicVersion:numericValue(t.Version),minecraftDataVersion:a,minecraftVersion:null==a?void 0:MINECRAFT_DATA_VERSIONS.get(a)}}function validateProjectionLimits(e,t,i){const a=Number.isSafeInteger(i)&&i>0?i:5e6,n=()=>{throw new Error(`投影方块数或区域体积超过 ${a.toLocaleString("en-US")} 限制`)};(Math.abs(t.totalBlocks??0)>a||Math.abs(t.totalVolume??0)>a)&&n();const r=compoundValue(readLitematicRoot(e).Regions);if(!r)return;let o=0;for(const e of Object.values(r)){const t=compoundValue(compoundValue(e)?.Size),i=[numericValue(t?.x),numericValue(t?.y),numericValue(t?.z)];if(i.some(e=>null==e))continue;const r=i.reduce((e,t)=>e*Math.abs(t),1);(!Number.isSafeInteger(r)||r>a-o)&&n(),o+=r}}function formatLitematicMetadata(e,t){const i=null==e.totalBlocks?"未知":String(e.totalBlocks),a=null==e.totalVolume?"未知":String(e.totalVolume),n=e.size?.join(" × ")??"未知",r=null==e.litematicVersion?"未知":String(e.litematicVersion),o=e.minecraftVersion??"未知",s=null==e.minecraftDataVersion?"":`(数据版本:${e.minecraftDataVersion})`,c="string"==typeof t&&t.trim()?(0,node_path_1.basename)(t).replace(/\.litematic$/i,""):"";return[...c?[`投影名称:${c}`]:[],`保存者游戏 ID:${e.author}`,`创建时间:${e.createdAt}`,`方块数/体积:${i}/${a}`,`尺寸:${n}`,`Litematic 版本:${r}`,`游戏版本:${o}${s}`].join("\n")}function parseLitematic(e){const t=readLitematicRoot(e).Regions;if(!t||"object"!=typeof t)throw new Error("文件不含 Litematica Regions 数据");const i=[];for(const e of Object.values(t))parseRegion(e,i);if(!i.length)throw new Error("投影中没有非空气方块");return i}function parseRegion(e,t){const i=e.Position,a=e.Size,n=e.BlockStatePalette,r=e.BlockStates;if(!(i&&a&&Array.isArray(n)&&Array.isArray(r)))return;const o=Math.abs(a.x),s=Math.abs(a.y),c=Math.abs(a.z),d=o*s*c;if(!d)return;const l=Math.max(2,Math.ceil(Math.log2(n.length))),m=a.x<0?-1:1,u=a.y<0?-1:1,h=a.z<0?-1:1;for(let e=0;e<d;e++){const a=unpackState(r,e,l),s=n[a]?.Name;if(!s||"minecraft:air"===s||"minecraft:cave_air"===s||"minecraft:void_air"===s)continue;const d=e%o,f=Math.floor(e/o)%c,p=Math.floor(e/(o*c));t.push({x:i.x+d*m,y:i.y+p*u,z:i.z+f*h,name:s})}}function unpackState(e,t,i){const a=BigInt(t*i),n=Number(a/64n),r=a%64n;if(n>=e.length)return 0;const o=(1n<<BigInt(i))-1n;let s=BigInt.asUintN(64,e[n])>>r&o;return r+BigInt(i)>64n&&n+1<e.length&&(s|=BigInt.asUintN(64,e[n+1])<<64n-r&o),Number(s)}function renderSchematic(e,t){const i=getBounds(e);return[{title:"isometric.png",png:renderIsometric(e,i,t,!1)},{title:"isometric-reverse.png",png:renderIsometric(e,i,t,!0)}]}function getBounds(e){return e.reduce((e,t)=>({minX:Math.min(e.minX,t.x),minY:Math.min(e.minY,t.y),minZ:Math.min(e.minZ,t.z),maxX:Math.max(e.maxX,t.x),maxY:Math.max(e.maxY,t.y),maxZ:Math.max(e.maxZ,t.z)}),{minX:1/0,minY:1/0,minZ:1/0,maxX:-1/0,maxY:-1/0,maxZ:-1/0})}const SIX_FACE_ORDER=["up","down","east","south","west","north"],SIX_FACE_LABELS={up:["00000100000","00000100000","00000100000","00000100000","00111111100","00000100000","00000100000","00000100000","11111111111","00000000000","00000000000"],down:["11111111111","00000100000","00000100000","00000110000","00000101000","00000100100","00000100010","00000100000","00000100000","00000100000","00000000000"],east:["00000100000","00111111100","00000100000","11111111111","10000100001","00000100000","00001110000","00010101000","00100100100","01000100010","00000100000"],south:["00000100000","11111111111","00000100000","01111111110","01000100010","01011111010","01000100010","01010101010","01001110010","01000100010","01000000110"],west:["11111111111","00010100000","01111111110","01010101010","01010101010","01010011010","01010001010","01011111010","01000000010","01111111110","00000000000"],north:["00100001000","00100001000","00101111000","00110001000","11100001000","00100001000","00100001010","00100001100","00100101000","00111001000","00100000111"]};function renderSixFaceOverview(e,t,i=t.sixFaceLayout??"horizontal"){if(!e.length)throw new Error("投影中没有可生成六面图的方块");const a=Math.max(128,Math.floor(t.outputSize)),n=Math.max(4,Math.round(.01*a)),r="vertical"===i?2:3,o="vertical"===i?3:2,s=Math.floor((a-n*("vertical"===i?o+1:r+1))/("vertical"===i?o:r)),c=new Raster("vertical"===i?r*s+n*(r+1):a,"vertical"===i?a:o*s+n*(o+1),t),d=getBounds(e);for(const[t,i]of SIX_FACE_ORDER.entries()){const a=n+t%r*(s+n),o=n+Math.floor(t/r)*(s+n);drawOrthographicFace(c,e,d,i,a,o,s,n),drawFaceLabel(c,i,a,o,s,n)}return c.png()}function drawOrthographicFace(e,t,i,a,n,r,o,s){const c="east"===a||"west"===a?i.maxZ-i.minZ+1:i.maxX-i.minX+1,d="up"===a||"down"===a?i.maxZ-i.minZ+1:i.maxY-i.minY+1,l=new Map;for(const e of t){const t=orthographicCoordinates(e,i,a),n=`${t.a},${t.b}`,r=l.get(n);(!r||t.depth>r.depth)&&l.set(n,{block:e,...t})}const m=Math.max(2,Math.floor(s/2)),u=Math.max(1,o-2*m),h=Math.min(u/c,u/d),f=n+m+(u-c*h)/2,p=r+m+(u-d*h)/2;for(const{block:t,a:i,b:a}of[...l.values()].sort((e,t)=>e.b-t.b||e.a-t.a)){const n=Math.floor(f+i*h),r=Math.floor(p+a*h),o=Math.max(n+1,Math.ceil(f+(i+1)*h)),s=Math.max(r+1,Math.ceil(p+(a+1)*h));e.rect(n,r,o-n,s-r,colorFor(t.name))}}function orthographicCoordinates(e,t,i){switch(i){case"up":return{a:e.x-t.minX,b:e.z-t.minZ,depth:e.y};case"down":return{a:t.maxX-e.x,b:e.z-t.minZ,depth:-e.y};case"east":return{a:t.maxZ-e.z,b:t.maxY-e.y,depth:e.x};case"south":return{a:e.x-t.minX,b:t.maxY-e.y,depth:e.z};case"west":return{a:e.z-t.minZ,b:t.maxY-e.y,depth:-e.x};case"north":return{a:t.maxX-e.x,b:t.maxY-e.y,depth:-e.z}}}function drawFaceLabel(e,t,i,a,n,r){const o=SIX_FACE_LABELS[t],s=Math.max(1,Math.min(6,Math.floor(n/80))),c=Math.max(1,Math.floor(s/2)),d=i+Math.max(2,Math.floor(r/2)),l=a+Math.max(2,Math.floor(r/2));e.rect(d,l,o[0].length*s+2*c,o.length*s+2*c,"#000000");for(let t=0;t<o.length;t++)for(let i=0;i<o[t].length;i++)"1"===o[t][i]&&e.rect(d+c+i*s,l+c+t*s,s,s,"#ffffff")}function renderIsometric(e,t,i,a){const n=i.isometricCellSize,r=Math.ceil(n/2),o=(t.maxX-t.minX+t.maxZ-t.minZ+2)*n+4,s=(t.maxX-t.minX+t.maxZ-t.minZ+2)*r+(t.maxY-t.minY+2)*n+4,c=Math.max(1,Math.min(1,i.outputSize/Math.max(o,s))),d=new Raster(Math.ceil(o*c),Math.ceil(s*c),i),l=e=>({x:a?t.maxX-e.x:e.x-t.minX,z:a?t.maxZ-e.z:e.z-t.minZ}),m=[...e].sort((e,t)=>{const i=l(e),a=l(t);return i.x+i.z+e.y-(a.x+a.z+t.y)});for(const e of m){const i=l(e),a=(i.x-i.z)*n+o/2,s=(i.x+i.z)*r+(t.maxY-e.y+1)*n;d.voxel(Math.round(a*c),Math.round(s*c),Math.max(2,Math.round(n*c)),colorFor(e.name))}return d.png()}const COLORS={stone:"#777777",cobblestone:"#707070",deepslate:"#4b4b50",dirt:"#855b37",grass_block:"#63913d",sand:"#dbca85",glass:"#a7d9df",water:"#3f76e4",lava:"#e05b22",oak_planks:"#b9915a",spruce_planks:"#765033",birch_planks:"#d9c485",iron_block:"#d7d7d7",gold_block:"#f6c543",diamond_block:"#5ed6c7",redstone_block:"#b21d1d",netherrack:"#823332",quartz_block:"#e9e4dc",obsidian:"#211d2d",white_wool:"#e9e9e9",black_wool:"#1d1d1d",red_wool:"#a12722",blue_wool:"#3549a7",green_wool:"#4c702c",yellow_wool:"#e5c440"};function colorFor(e){const t=e.replace(/^minecraft:/,"");if(COLORS[t])return COLORS[t];for(const[e,i]of Object.entries(COLORS))if(t.includes(e))return i;const i=(0,node_crypto_1.createHash)("md5").update(t).digest();return`#${(90+i[0]%120).toString(16).padStart(2,"0")}${(90+i[1]%120).toString(16).padStart(2,"0")}${(90+i[2]%120).toString(16).padStart(2,"0")}`}class Raster{width;height;data;background;constructor(e,t,i){this.width=e,this.height=t,this.background=parseColor(i.background,i.transparentBackground?0:255),this.data=Buffer.alloc(e*t*4);for(let e=0;e<this.data.length;e+=4)this.data.set(this.background,e)}rect(e,t,i,a,n){for(let r=Math.max(0,t);r<Math.min(this.height,t+a);r++)for(let t=Math.max(0,e);t<Math.min(this.width,e+i);t++)this.pixel(t,r,n)}pixel(e,t,i){e<0||t<0||e>=this.width||t>=this.height||this.data.set(parseColor(i),4*(t*this.width+e))}voxel(e,t,i,a){const n=adjust(a,1.15),r=adjust(a,.78),o=adjust(a,.6),s=Math.ceil(i/2);for(let a=0;a<s;a++)for(let r=-i+a;r<=i-a;r++)this.pixel(e+r,t-s+a,n);for(let a=0;a<i;a++)for(let n=-i+Math.floor(a/2);n<=0;n++)this.pixel(e+n,t+a,r);for(let a=0;a<i;a++)for(let n=0;n<=i-Math.floor(a/2);n++)this.pixel(e+n,t+a,o)}png(){const e=Buffer.alloc((4*this.width+1)*this.height);for(let t=0;t<this.height;t++)e[t*(4*this.width+1)]=0,this.data.copy(e,t*(4*this.width+1)+1,t*this.width*4,(t+1)*this.width*4);const t=Buffer.alloc(13);return t.writeUInt32BE(this.width,0),t.writeUInt32BE(this.height,4),t.set([8,6,0,0,0],8),Buffer.concat([Buffer.from("PNG\r\n\n","binary"),chunk("IHDR",t),chunk("IDAT",(0,node_zlib_1.deflateSync)(e)),chunk("IEND",Buffer.alloc(0))])}}function parseColor(e,t=255){const i=/^#?([0-9a-f]{6})$/i.exec(e)?.[1]??"182026";return[parseInt(i.slice(0,2),16),parseInt(i.slice(2,4),16),parseInt(i.slice(4,6),16),t]}function adjust(e,t){const[i,a,n]=parseColor(e);return`#${[i,a,n].map(e=>Math.round(Math.min(255,e*t)).toString(16).padStart(2,"0")).join("")}`}function chunk(e,t){const i=Buffer.from(e),a=Buffer.alloc(4);a.writeUInt32BE(t.length);const n=Buffer.alloc(4);return n.writeUInt32BE(crc32(Buffer.concat([i,t]))>>>0),Buffer.concat([a,i,t,n])}const CRC_TABLE=Array.from({length:256},(e,t)=>{let i=t;for(let e=0;e<8;e++)i=1&i?3988292384^i>>>1:i>>>1;return i>>>0});function crc32(e){let t=4294967295;for(const i of e)t=CRC_TABLE[255&(t^i)]^t>>>8;return(4294967295^t)>>>0}
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.Config=exports.inject=exports.name=void 0,exports.apply=apply,exports.hashRenderConfiguration=hashRenderConfiguration,exports.patchOneBotAdapterSource=patchOneBotAdapterSource,exports.cacheNameSegment=cacheNameSegment,exports.enforceCacheLimit=enforceCacheLimit,exports.findLitematicFile=findLitematicFile,exports.supportsStandaloneJavaVersion=supportsStandaloneJavaVersion,exports.standaloneJvmOptions=standaloneJvmOptions,exports.isJavaMemoryFailure=isJavaMemoryFailure,exports.mergeRenderDiagnostics=mergeRenderDiagnostics,exports.formatRenderError=formatRenderError,exports.resolveSendOptions=resolveSendOptions,exports.sendImages=sendImages,exports.composeQqOverview=composeQqOverview,exports.replyElements=replyElements,exports.parseLitematicMetadata=parseLitematicMetadata,exports.validateProjectionLimits=validateProjectionLimits,exports.formatLitematicMetadata=formatLitematicMetadata,exports.parseLitematic=parseLitematic,exports.renderSchematic=renderSchematic,exports.renderSixFaceOverview=renderSixFaceOverview;const koishi_1=require("koishi"),node_crypto_1=require("node:crypto"),node_fs_1=require("node:fs"),node_path_1=require("node:path"),node_url_1=require("node:url"),node_child_process_1=require("node:child_process"),node_zlib_1=require("node:zlib"),pngjs_1=require("pngjs"),Sentry=require("@sentry/node-core/light");exports.name="litematic-renderer",exports.inject={optional:["puppeteer","server"]};const CACHE_FORMAT_VERSION=14,packageVersion=require("../package.json").version,PLUGIN_VERSION="string"==typeof packageVersion?packageVersion:"unknown";exports.Config=koishi_1.Schema.intersect([koishi_1.Schema.object({qqBotType:koishi_1.Schema.union([koishi_1.Schema.const("official").description("QQ 官方机器人"),koishi_1.Schema.const("selfHosted").description("自建 QQ(OneBot / NapCat)")]).role("radio").default("selfHosted").description("机器人接入类型;决定图片、合并转发、引用和 @ 的发送方式"),patchOneBotGroupUpload:koishi_1.Schema.boolean().default(!0).description("自建 QQ:启动时修复 OneBot 群文件上传事件;官方机器人不使用此项")}).description("机器人接入"),koishi_1.Schema.object({renderEngine:koishi_1.Schema.union([koishi_1.Schema.const("standalone"),koishi_1.Schema.const("java"),koishi_1.Schema.const("webgl"),koishi_1.Schema.const("cpu")]).default("standalone").description("渲染引擎;standalone 为无需客户端的独立 Java 资源包渲染器"),maxFileSize:koishi_1.Schema.natural().min(1).default(1024).description("自动处理的最大文件大小(KB)"),maxBlocks:koishi_1.Schema.natural().min(1).max(2e9).default(5e6).description("投影区域体积及声明方块数上限;用于拒绝尺寸异常或资源消耗过大的投影"),outputSize:koishi_1.Schema.natural().min(128).max(4096).default(1024).description("六面图和 CPU 渲染最长边的最大像素数"),background:koishi_1.Schema.string().default("#000000").description("PNG 背景颜色"),transparentBackground:koishi_1.Schema.boolean().default(!1).description("输出透明背景"),isometricFill:koishi_1.Schema.percent().default(.78).description("正二轴测主体在画布中的最大占比"),isometricRotation:koishi_1.Schema.number().min(0).max(360).step(1).default(135).description("Isometric Renders 绕竖直轴的基准旋转角"),isometricSlant:koishi_1.Schema.number().min(-90).max(90).step(1).default(36).description("Isometric Renders 俯仰角;36° 为正二轴测预设")}).description("渲染设置"),koishi_1.Schema.object({sendAsForward:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:开启为合并转发,关闭为联合发送;官方 QQ 忽略此项"),showViewTitles:koishi_1.Schema.boolean().default(!1).description("仅自建 QQ:发送图片时显示视图标题"),replyAndMention:koishi_1.Schema.boolean().default(!1).description("自建 QQ 会引用并 @ 发送者;官方 QQ 仅引用,避免显示 OpenID"),sixFaceOverview:koishi_1.Schema.boolean().default(!0).description("合并转发时生成并附加上、下、东、南、西、北六面正交合成图"),sixFaceLayout:koishi_1.Schema.union([koishi_1.Schema.const("horizontal").description("横向 3×2"),koishi_1.Schema.const("vertical").description("纵向 2×3")]).default("horizontal").description("六面正交合成图布局;最长边由 outputSize 控制"),groupSendOptions:koishi_1.Schema.array(koishi_1.Schema.object({groupId:koishi_1.Schema.string().description("QQ群号"),sendMode:koishi_1.Schema.union([koishi_1.Schema.const("forward").description("合并转发"),koishi_1.Schema.const("combined").description("联合发送(一条普通消息)")]).default("forward"),replyAndMention:koishi_1.Schema.union([koishi_1.Schema.const("inherit").description("继承全局设置"),koishi_1.Schema.const("enabled").description("开启回复 @"),koishi_1.Schema.const("disabled").description("关闭回复 @")]).default("inherit")})).default([]).description("按群覆盖发送方式和回复设置;官方 QQ 只使用引用开关,自建 QQ 同时使用发送模式和 @")}).description("发送设置"),koishi_1.Schema.object({standaloneJavaCommand:koishi_1.Schema.string().default("").description("独立渲染器使用的 Java 可执行文件;推荐 Java 21+,留空自动查找"),minecraftJarPath:koishi_1.Schema.string().default("").description("可选的 Minecraft 客户端 JAR 或基础资源包;留空使用内置 26.2 原版资源"),resourcePackPaths:koishi_1.Schema.array(koishi_1.Schema.string()).default([]).description("自定义材质包路径;越靠后优先级越高"),standaloneRendererJar:koishi_1.Schema.string().default("").description("独立渲染器 JAR;留空使用插件内置版本"),standaloneRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("独立 Java 渲染超时(毫秒)"),standaloneJavaMaxHeapMb:koishi_1.Schema.natural().min(128).max(32768).step(8).default(200).description("首次独立渲染的最大堆内存(MiB)"),standaloneJavaRetryMaxHeapMb:koishi_1.Schema.natural().min(256).max(32768).step(128).default(2048).description("内存不足时新进程重试的最大堆内存(MiB)"),standaloneJavaMemoryRestartLimit:koishi_1.Schema.natural().max(3).default(1).description("检测到内存不足后启动全新进程重试的次数"),javaResolution:koishi_1.Schema.natural().min(256).max(4096).default(1024).description("Java 渲染最终输出边长"),javaSupersampling:koishi_1.Schema.natural().min(1).max(4).default(2).description("离屏超采样倍数")}).description("独立渲染器"),koishi_1.Schema.object({renderTimeout:koishi_1.Schema.natural().min(1e3).default(3e4).description("文件下载超时(毫秒)"),cacheDirectory:koishi_1.Schema.string().default("data/litematic-renderer-cache").description("持久缓存目录;按插件版本和投影 SHA-256 分区"),cacheMaxSizeGb:koishi_1.Schema.number().min(1).max(1024).step(1).default(20).description("所有版本缓存总上限(GiB),超出后按最久未使用清理"),diagnosticsFilePath:koishi_1.Schema.string().default("data/litematic-renderer-diagnostics.json").description("渲染诊断持久化文件路径;导出地址固定为 /litematic-renderer/diagnostics"),sentryDsn:koishi_1.Schema.string().default("").description("Sentry DSN,用于上报渲染错误;留空时读取 SENTRY_DSN 环境变量").experimental()}).description("缓存与诊断"),koishi_1.Schema.object({isometricCellSize:koishi_1.Schema.natural().min(2).max(32).default(7).description("CPU 正二轴测方块菱形半宽"),javaRenderTimeout:koishi_1.Schema.natural().min(1e4).default(18e4).description("等待 Minecraft Java 渲染桥的超时(毫秒)"),webglQuality:koishi_1.Schema.union([koishi_1.Schema.const("standard"),koishi_1.Schema.const("high"),koishi_1.Schema.const("ultra")]).default("high").description("WebGL 渲染质量"),webglWidth:koishi_1.Schema.natural().min(256).max(2048).default(800).description("WebGL 单张图宽度"),webglHeight:koishi_1.Schema.natural().min(256).max(2048).default(600).description("WebGL 单张图高度"),isometricSquare:koishi_1.Schema.boolean().default(!0).description("将 WebGL 正二轴测图输出为正方形"),javaBridgeDirectory:koishi_1.Schema.string().default("").description("renderEngine=java 时使用的 Fabric Java 渲染桥任务目录"),gpuRendererCommand:koishi_1.Schema.string().default("").description("可选外部 GPU 渲染器命令;非空时优先尝试,失败后回退到 renderEngine")}).description("高级设置")]);const activeStandaloneJavaProcesses=new Set;function apply(e,t){const i=e.logger(exports.name),a=1024*t.maxFileSize,n=t.sentryDsn||process.env.SENTRY_DSN||"";n&&Sentry.init({dsn:n,tracesSampleRate:1});const r=(e,i)=>{n&&Sentry.withScope(a=>{a.setFingerprint([i]),a.setTag("entrypoint",i),a.setContext("render",{engine:t.renderEngine}),Sentry.captureException(e)})};e.on("dispose",()=>terminateActiveStandaloneJavaProcesses(i)),"selfHosted"===t.qqBotType&&ensureOneBotGroupUploadCompatibility(i,t.patchOneBotGroupUpload);const o=(0,node_path_1.resolve)(stringOrDefault(t.diagnosticsFilePath,"data/litematic-renderer-diagnostics.json")),s=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-archive"),c=(0,node_path_1.join)((0,node_path_1.dirname)(o),"litematic-renderer-diagnostics-disabled");rotateDiagnosticsFile(o,s,i);const d=(0,node_path_1.resolve)(stringOrDefault(t.cacheDirectory,"data/litematic-renderer-cache")),l=(0,node_path_1.join)(d,`v${cachePathSegment(PLUGIN_VERSION)}`),m=Math.floor(t.cacheMaxSizeGb*1024**3),u=new Map;node_fs_1.promises.mkdir(l,{recursive:!0}).then(()=>enforceCacheLimit(d,m)).catch(e=>i.warn(`缓存初始化失败:${e instanceof Error?e.message:String(e)}`));const h=async(n,r="schematic.litematic",s,c=!1)=>{const h=s??await download(e,n,a,t.renderTimeout),f=parseLitematicMetadata(h);validateProjectionLimits(h,f,t.maxBlocks);const p=formatLitematicMetadata(f,r),g=projectionNameFromFilename(r),_=(0,node_crypto_1.createHash)("sha256").update(h).digest("hex"),w="standalone"===t.renderEngine?await resolveMinecraftResources(t):"",y="standalone"===t.renderEngine?await fingerprintFiles([w,...resourcePackPaths(t.resourcePackPaths)]):[],S=hashRenderConfiguration({version:CACHE_FORMAT_VERSION,outputSize:t.outputSize,isometricCellSize:t.isometricCellSize,background:t.background,transparentBackground:t.transparentBackground,gpuRendererCommand:t.gpuRendererCommand,renderEngine:t.renderEngine,standaloneRendererJar:t.standaloneRendererJar,resourceFingerprint:y,javaResolution:t.javaResolution,javaSupersampling:t.javaSupersampling,webglQuality:t.webglQuality,webglWidth:t.webglWidth,webglHeight:t.webglHeight,isometricSquare:t.isometricSquare,isometricFill:t.isometricFill,isometricRotation:t.isometricRotation,isometricSlant:t.isometricSlant,sixFaceLayout:t.sixFaceLayout}),v=cacheNameSegment(r),b=(0,node_path_1.join)(l,`${v}-${_}-${S}`),x=(0,node_path_1.join)(b,"projection.litematic");await node_fs_1.promises.mkdir(b,{recursive:!0}),await exists(x)||await node_fs_1.promises.writeFile(x,h),await ensureCacheMetadata(b,{pluginVersion:PLUGIN_VERSION,cacheFormatVersion:CACHE_FORMAT_VERSION,contentSha256:_,renderConfigSha256:S,sourceFilename:(0,node_path_1.basename)(r),createdAt:(new Date).toISOString()});const k=["isometric.png","isometric-reverse.png"].map(e=>(0,node_path_1.join)(b,e));if((await Promise.all(k.map(exists))).every(Boolean))i.debug(`命中投影缓存:${_}`);else{let a=u.get(b);a?i.debug(`等待相同投影的缓存任务:${_}`):(a=(async()=>{try{if((await Promise.all(k.map(exists))).every(Boolean))return;if(!Boolean(t.gpuRendererCommand&&await renderWithGpu(t.gpuRendererCommand,x,b,t.renderTimeout,i))||!(await Promise.all(k.map(exists))).every(Boolean))if("standalone"===t.renderEngine)await renderWithStandalone(x,b,w,t,i),await mergeRenderDiagnostics(b,o,i);else if("java"===t.renderEngine)await renderWithJavaBridge(x,b,t);else{const a=parseLitematic(h);let n;if("webgl"===t.renderEngine&&e.puppeteer)try{n=await renderWithWebgl(e,h,t)}catch(e){i.warn(`WebGL 渲染失败,回退 CPU:${e instanceof Error?e.message:String(e)}`),n=renderSchematic(a,t)}else n=renderSchematic(a,t);await Promise.all(n.map(e=>node_fs_1.promises.writeFile((0,node_path_1.join)(b,e.title),e.png)))}if(!(await Promise.all(k.map(exists))).every(Boolean))throw new Error("渲染器没有生成两张正二轴测 PNG")}catch(e){throw await appendRenderError(b,o,t.renderEngine,e,i),e}})().finally(()=>u.delete(b)),u.set(b,a)),await a}const M=k.map((e,t)=>({title:["正二轴测","反向正二轴测(旋转 180°)"][t],path:e}));if(c){const e=(0,node_path_1.join)(b,"six-faces.png");if(!await exists(e)){const a=`${b}:six-face`;let n=u.get(a);n||(n=(async()=>{try{if("standalone"===t.renderEngine)try{await renderWithStandalone(x,b,w,t,i,"six-face")}catch(e){i.warn(`真实材质六面图生成失败,回退方块颜色投影:${e instanceof Error?e.message:String(e)}`)}if(!await exists(e)){const i=parseLitematic(h);await node_fs_1.promises.writeFile(e,renderSixFaceOverview(i,t,t.sixFaceLayout))}}catch(e){i.warn(`六面正交图生成失败,继续发送轴测图:${e instanceof Error?e.message:String(e)}`)}})().finally(()=>u.delete(a)),u.set(a,n)),await n}await exists(e)&&M.push({title:"六面正投影",path:e})}return await touchCacheEntry(b),await enforceCacheLimit(d,m,b),{images:M,metadata:p,projectionName:g}};e.middleware(async(e,n)=>{if(!e.guildId)return n();const s=findLitematicFile(e);if(!s)return n();const c=await resolveFileUrl(e,s);if(!c)return i.warn(`检测到 Litematic 文件但无法获取下载地址:${fileName(s)??"unknown.litematic"};请检查 OneBot/NapCat 的 get_group_file_url 接口`),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:"检测到投影文件,但无法获取群文件下载地址,请检查机器人文件接口"})]),n();if(isOverLimit(s.size,a))return await appendGlobalRenderError(o,`input:${(0,node_path_1.basename)(fileName(s)??"unknown.litematic")}`,"input",`file size exceeds ${t.maxFileSize} KB`,i),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,无法渲染`})]),n();if(!isUnderLimit(s.size,a))return n();try{const i=resolveSendOptions(t,e.guildId),a=await h(c,fileName(s)??"schematic.litematic",void 0,i.sixFaceOverview);await sendImages(e,a.images,a.metadata,i,a.projectionName)}catch(a){return r(a,"group-file-upload"),i.warn(a),await e.send([...replyElements(e,t.qqBotType),(0,koishi_1.h)("text",{content:formatRenderError(a,t)})]),n()}return n()}),e.command("litematic.render <url:string>","渲染可直接下载的 .litematic 文件").action(async({session:e},i)=>{if(!i)return"请提供 .litematic 文件的直链";if(!e)return"此命令只能在消息会话中执行";try{const a=resolveSendOptions(t,e.guildId),n=await h(i,"schematic.litematic",void 0,a.sixFaceOverview);await sendImages(e,n.images,n.metadata,a,n.projectionName)}catch(e){return r(e,"litematic.render"),formatRenderError(e,t)}}),e.command("litematic.cache.clear","清理投影渲染缓存",{authority:3}).action(async()=>(await node_fs_1.promises.rm(d,{recursive:!0,force:!0}),await node_fs_1.promises.mkdir(l,{recursive:!0}),"投影渲染缓存已清理")),e.command("litematic.errors.export","export render diagnostics",{authority:3}).action(async()=>`Diagnostics file: ${o}`),e.command("litematic.errors.disable","disable current render diagnostics snapshot",{authority:3}).action(async()=>disableDiagnosticsSnapshot(o,c));const f=e.server;f?.get&&f.get("/litematic-renderer/diagnostics",async e=>{e.type="application/json",e.set("Content-Disposition",'attachment; filename="litematic-renderer-diagnostics.json"'),e.body=await node_fs_1.promises.readFile(o,"utf8").catch(()=>'{"format":2,"blocks":[],"errors":[]}')})}function hashRenderConfiguration(e){return(0,node_crypto_1.createHash)("sha256").update(JSON.stringify(e)).digest("hex").slice(0,16)}function patchOneBotAdapterSource(e){if(/^\s*case ["']group_upload["']:/m.test(e))return{source:e,changed:!1,alreadyCompatible:!0};const t=e.match(/\(0,\s*(import_[\w$]+)\.hyphenate\)\(data\.sub_type\)/)?.[1]??"import_koishi2",i=/ \/\/ https:\/\/github\.com\/koishijs\/koishi-plugin-adapter-onebot\/issues\/33\r?\n \/\/ case 'offline_file':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'private'\r?\n \/\/ session\.isDirect = true\r?\n \/\/ session\.subsubtype = 'offline-file-added'\r?\n \/\/ break\r?\n \/\/ case 'group_upload':\r?\n \/\/ session\.elements = \[h\('file', data\.file\)\]\r?\n \/\/ session\.type = 'message'\r?\n \/\/ session\.subtype = 'group'\r?\n \/\/ session\.subsubtype = 'guild-file-added'\r?\n \/\/ break/;if(!i.test(e))return{source:e,changed:!1,alreadyCompatible:!1};const a=[" // Restored by koishi-plugin-litematic-renderer so NapCat group file uploads reach middleware",' case "offline_file":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "private";'," session.isDirect = true;",' session.subsubtype = "offline-file-added";'," break;",' case "group_upload":',` session.elements = [(0, ${t}.h)("file", data.file)];`,' session.type = "message";',' session.subtype = "group";',' session.subsubtype = "guild-file-added";'," break;"].join("\n");return{source:e.replace(i,a),changed:!0,alreadyCompatible:!1}}async function ensureOneBotGroupUploadCompatibility(e,t){if(!t)return;let i;try{i=require.resolve("koishi-plugin-adapter-onebot/package.json",{paths:[process.cwd(),__dirname]})}catch{return void e.debug("未安装 koishi-plugin-adapter-onebot,跳过群文件上传事件兼容修复")}try{const t=(0,node_path_1.dirname)(i),a=JSON.parse(await node_fs_1.promises.readFile(i,"utf8")),n=(0,node_path_1.join)(t,"lib","index.js"),r=await node_fs_1.promises.readFile(n,"utf8"),o=patchOneBotAdapterSource(r);if(o.alreadyCompatible)return void e.debug(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 已支持 group_upload`);if(!o.changed)return void e.warn(`koishi-plugin-adapter-onebot@${a.version??"unknown"} 未识别到可自动修复的 group_upload 代码段`);const s=`${n}.litematic-renderer.bak`;await exists(s)||await node_fs_1.promises.writeFile(s,r),await node_fs_1.promises.writeFile(n,o.source),e.warn(`已修复 koishi-plugin-adapter-onebot@${a.version??"unknown"} 的群文件上传事件映射;请重启 Koishi 让已加载的 OneBot 适配器生效`)}catch(t){e.warn(`修复 OneBot 群文件上传事件失败:${t instanceof Error?t.message:String(t)}`)}}function cachePathSegment(e){return e.replace(/[^a-zA-Z0-9._-]/g,"_")||"unknown"}function cacheNameSegment(e){return((0,node_path_1.basename)(e||"schematic.litematic",(0,node_path_1.extname)(e||"")).replace(/[<>:"/\\|?*\u0000-\u001f]/g,"_").replace(/[. ]+$/g,"").trim()||"schematic").slice(0,80)}function projectionNameFromFilename(e){return(0,node_path_1.basename)(e||"schematic.litematic").replace(/\.litematic$/i,"").trim()||"schematic"}async function ensureCacheMetadata(e,t){const i=(0,node_path_1.join)(e,"cache.json");await exists(i)||await node_fs_1.promises.writeFile(i,JSON.stringify(t,null,2)+"\n")}async function touchCacheEntry(e){const t=new Date;await node_fs_1.promises.utimes(e,t,t).catch(()=>{})}async function directorySize(e){let t=0;const i=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const a of i){const i=(0,node_path_1.join)(e,a.name);a.isDirectory()?t+=await directorySize(i):a.isFile()&&(t+=(await node_fs_1.promises.stat(i)).size)}return t}async function enforceCacheLimit(e,t,i){const a=[],n=await node_fs_1.promises.readdir(e,{withFileTypes:!0}).catch(e=>{if("ENOENT"===e?.code)return[];throw e});for(const t of n){if(!t.isDirectory())continue;const i=(0,node_path_1.join)(e,t.name),n=await node_fs_1.promises.readdir(i,{withFileTypes:!0});for(const e of n){if(!e.isDirectory())continue;const t=(0,node_path_1.join)(i,e.name),[n,r]=await Promise.all([directorySize(t),node_fs_1.promises.stat(t)]);a.push({path:t,size:n,lastUsed:r.mtimeMs})}}let r=a.reduce((e,t)=>e+t.size,0),o=0,s=0;const c=i&&(0,node_path_1.resolve)(i);a.sort((e,t)=>e.lastUsed-t.lastUsed);for(const e of a){if(r<=t)break;c&&(0,node_path_1.resolve)(e.path)===c||(await node_fs_1.promises.rm(e.path,{recursive:!0,force:!0}),r-=e.size,o+=e.size,s++)}return{totalBytes:r,removedBytes:o,removedEntries:s}}function fileElements(e){return[...e.elements??[],...koishi_1.h.parse(e.content??"")]}function findLitematicFile(e){const t=fileElements(e).filter(e=>"file"===e.type),i=t.find(e=>".litematic"===(0,node_path_1.extname)(fileName(e.attrs)??"").toLowerCase());if(i?.attrs)return i.attrs;for(const t of[e.event,e._data,e.data,e.message]){const e=findLitematicInFilePayload(t);if(e)return e}const a=inferLitematicName(e.content);return a&&t[0]?.attrs?{...t[0].attrs,name:a}:void 0}function findLitematicInFilePayload(e,t=new Set,i=0){if(!e||"object"!=typeof e||i>6||t.has(e))return;t.add(e);const a=e,n=a,r=n.file&&"object"==typeof n.file?n.file:void 0;if(r&&fileName(r)&&".litematic"===(0,node_path_1.extname)(fileName(r)).toLowerCase())return{...r,id:r.id??n.id,file_id:r.file_id??n.file_id,fileId:r.fileId??n.fileId,busid:r.busid??n.busid};if(fileName(n)&&".litematic"===(0,node_path_1.extname)(fileName(n)).toLowerCase())return n;for(const[e,n]of Object.entries(a))if(n&&"object"==typeof n&&/file|upload|message|data|event/i.test(e)){const e=findLitematicInFilePayload(n,t,i+1);if(e)return e}}function inferLitematicName(e){if(!e)return;const t=e.match(/(?:file|文件)\s+(.+?\.litematic)(?=$|[\]】\r\n])/i);return t?.[1]?t[1].trim():e.match(/([^\s\]】<>"']+\.litematic)(?=$|[\s\]】])/i)?.[1]?.trim()}function fileName(e){const t=e.file&&"object"==typeof e.file?e.file:void 0,i=e.name??e.filename??e.file_name??e.fileName??t?.name??t?.filename??t?.file_name??t?.fileName;return"string"==typeof i&&i.trim()?i.trim():void 0}async function resolveFileUrl(e,t){const i=t.file&&"object"==typeof t.file?t.file:void 0,a=t.url??t.src??("string"==typeof t.file?t.file:void 0)??i?.url??i?.src;if("string"==typeof a&&a)return a;const n=t.id??t.file_id??t.fileId??i?.id??i?.file_id??i?.fileId,r=e.bot.internal;if(!e.guildId||!n||"function"!=typeof r?.getGroupFileUrl)return;const o=await r.getGroupFileUrl(e.guildId,n,Number(t.busid??i?.busid??0));return"string"==typeof o?o:o?.url}function isUnderLimit(e,t){if(null==e||""===e)return!0;const i=Number(e);return Number.isFinite(i)&&i>0&&i<=t}async function download(e,t,i,a){const n=await e.http.get(t,{responseType:"arraybuffer",timeout:a}),r=Buffer.from(n);if(r.byteLength>i)throw new Error(`文件超过 ${(i/1024/1024).toFixed(2)} MB 限制`);return r}async function exists(e){try{return await node_fs_1.promises.access(e,node_fs_1.constants.R_OK),!0}catch{return!1}}function stringOrDefault(e,t){return"string"==typeof e&&e.trim()?e:t}function resourcePackPaths(e){return Array.isArray(e)?e.filter(e=>"string"==typeof e&&Boolean(e.trim())):[]}async function fingerprintFiles(e){return Promise.all(e.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(async e=>{const t=(0,node_path_1.resolve)(e);try{const e=await node_fs_1.promises.stat(t);return{path:t,size:e.size,modified:e.mtimeMs}}catch{return{path:t,missing:!0}}}))}async function resolveStandaloneJavaCommand(e){const t=e.trim();if(t)return t;const i="win32"===process.platform?"java.exe":"java",a=new Set;if(process.env.JAVA_HOME&&a.add((0,node_path_1.join)(process.env.JAVA_HOME,"bin",i)),"win32"===process.platform){const e=new Set([(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Java"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Eclipse Adoptium"),(0,node_path_1.join)(process.env.ProgramFiles??"C:/Program Files","Microsoft","jdk")]);for(const t of e)try{const e=await node_fs_1.promises.readdir(t,{withFileTypes:!0});for(const n of e)n.isDirectory()&&/(?:^|[-_])21(?:[._-]|$)/.test(n.name)&&a.add((0,node_path_1.join)(t,n.name,"bin",i))}catch{}}a.add("java");for(const e of a)if(supportsStandaloneJavaVersion(await javaMajorVersion(e)))return e;throw new Error("未找到 Java 21+;请安装 Java 21 或更高版本,设置 JAVA_HOME,或填写 standaloneJavaCommand")}function isOverLimit(e,t){if(null==e||""===e)return!1;const i=Number(e);return Number.isFinite(i)&&i>t}async function javaMajorVersion(e){return new Promise(t=>{let i="",a=!1;const n=e=>{a||(a=!0,clearTimeout(s),t(e))},r=(0,node_child_process_1.spawn)(e,["-version"],{shell:!1,windowsHide:!0}),o=e=>{i=(i+String(e)).slice(-2e3)};r.stdout?.on("data",o),r.stderr?.on("data",o),r.once("error",()=>n()),r.once("close",()=>{const e=i.match(/(?:java|openjdk) version "(\d+)/i);n(e?Number(e[1]):void 0)});const s=setTimeout(()=>{r.kill(),n()},5e3)})}function supportsStandaloneJavaVersion(e){return void 0!==e&&e>=21}function standaloneJvmOptions(e){return["-Xms128m",`-Xmx${Math.max(128,Math.min(32768,Math.floor(e)))}m`,"-XX:+UseG1GC","-XX:+UseStringDeduplication","-XX:+ExitOnOutOfMemoryError"]}function isJavaMemoryFailure(e,t){return 137===t||/outofmemoryerror|java heap space|gc overhead limit|cannot reserve enough space|native memory allocation|insufficient memory|malloc failed|os::commit_memory/i.test(e)}async function resolveMinecraftResources(e){const t="string"==typeof e.minecraftJarPath?e.minecraftJarPath.trim():"";if(t&&await exists((0,node_path_1.resolve)(t)))return(0,node_path_1.resolve)(t);const i=(0,node_path_1.resolve)((0,node_path_1.join)(__dirname,"../assets/vanilla-resources/Minecraft-26.2-Vanilla-Resources.zip"));if(!await exists(i))throw new Error(`插件内置的 Minecraft 26.2 原版资源包不存在:${i}`);return i}async function renderWithStandalone(e,t,i,a,n,r="isometric"){const o=(0,node_path_1.resolve)(a.standaloneRendererJar||(0,node_path_1.join)(__dirname,"../assets/standalone-renderer/litematic-standalone-renderer-0.2.7.jar"));if(!await exists(o))throw new Error(`独立 Java 渲染器不存在:${o}`);if(!await exists(i))throw new Error(`Minecraft 资源 JAR 不存在:${i}`);for(const e of resourcePackPaths(a.resourcePackPaths))if(!await exists((0,node_path_1.resolve)(e)))throw new Error(`材质包不存在:${e}`);const s=["-Djava.awt.headless=true","-jar",o,"--input",(0,node_path_1.resolve)(e),"--output",(0,node_path_1.resolve)(t),"--minecraft-jar",i,"--resolution",String(a.javaResolution),"--supersampling",String(a.javaSupersampling),"--rotation",String(a.isometricRotation),"--slant",String(a.isometricSlant),"--fill",String(a.isometricFill),"--background",a.background];"six-face"===r&&s.push("--six-face-only","--six-face-resolution",String(a.outputSize),"--six-face-layout",a.sixFaceLayout),a.transparentBackground&&s.push("--transparent-background");for(const e of resourcePackPaths(a.resourcePackPaths))s.push("--resource-pack",(0,node_path_1.resolve)(e));const c=await resolveStandaloneJavaCommand(a.standaloneJavaCommand);for(let e=0;e<=a.standaloneJavaMemoryRestartLimit;e++){const i=0===e?a.standaloneJavaMaxHeapMb:a.standaloneJavaRetryMaxHeapMb,o=await runStandaloneJava(c,[...standaloneJvmOptions(i),...s],a.standaloneRenderTimeout);if(0===o.code)return;const d=isJavaMemoryFailure(o.stderr,o.code);if(d&&e<a.standaloneJavaMemoryRestartLimit){n.warn(`独立 Java 渲染超出 ${a.standaloneJavaMaxHeapMb} MiB 堆内存限制;正在启动全新进程重试(${e+1}/${a.standaloneJavaMemoryRestartLimit})`);const i="six-face"===r?["six-faces.png"]:["isometric.png","isometric-reverse.png"];await Promise.all(i.map(e=>node_fs_1.promises.rm((0,node_path_1.join)(t,e),{force:!0})));continue}const l=d?`;Java 内存不足(最大堆 ${a.standaloneJavaMaxHeapMb} MiB,已重启 ${e} 次)`:"";throw new Error(`独立 Java 渲染器退出码 ${o.code}${l}${o.stderr?`:${o.stderr.trim()}`:""}`)}}async function runStandaloneJava(e,t,i){return new Promise((a,n)=>{const r=(0,node_child_process_1.spawn)(e,t,{shell:!1,windowsHide:!0});activeStandaloneJavaProcesses.add(r);let o="",s=!1;const c=()=>activeStandaloneJavaProcesses.delete(r);r.stderr?.on("data",e=>{o=(o+String(e)).slice(-6e3)});const d=setTimeout(()=>{s||(s=!0,(async()=>{await terminateStandaloneJavaProcess(r),c(),n(new Error("Standalone Java render timed out; process terminated and memory reclaimed"))})())},i);r.once("error",e=>{s||(s=!0,clearTimeout(d),terminateStandaloneJavaProcess(r),c(),n(e))}),r.once("close",e=>{s||(s=!0,clearTimeout(d),c(),a({code:e,stderr:o}))})})}async function terminateStandaloneJavaProcess(e){const t=e.pid;t&&("win32"===process.platform&&await new Promise(e=>{const i=(0,node_child_process_1.spawn)("taskkill.exe",["/PID",String(t),"/T","/F"],{windowsHide:!0});i.once("close",()=>e()),i.once("error",()=>e())}),null!=e.exitCode||e.killed||e.kill())}async function terminateActiveStandaloneJavaProcesses(e){const t=[...activeStandaloneJavaProcesses];t.length&&(await Promise.all(t.map(e=>terminateStandaloneJavaProcess(e))),e.info(`已终止 ${t.length} 个独立渲染 Java 进程并回收其子进程`))}async function renderWithJavaBridge(e,t,i){if(!i.javaBridgeDirectory.trim())throw new Error("Fabric Java 渲染桥任务目录未配置;使用 java 后端时请填写 javaBridgeDirectory");const a=(0,node_path_1.resolve)(i.javaBridgeDirectory),n=await readJson((0,node_path_1.join)(a,"status.json"));if(!n?.timestamp||Date.now()-n.timestamp>5e3)throw new Error("Minecraft Java 渲染桥未运行;请启动 1.21.1-Fabric 客户端并进入一个世界");if(!n.inWorld)throw new Error("Minecraft Java 渲染桥已启动,但客户端尚未进入世界");const r=n.resourcePacks??[];if(![["XeKr","3.6forMC1.20.2~1.21.5.zip"],["XKRDA","1.19.4~1.21snapshot.zip"]].every(e=>r.some(t=>e.every(e=>t.includes(e)))))throw new Error("Minecraft 客户端没有同时启用 XeKr 红显基础包和 XKRDA 附加包");const o=(0,node_crypto_1.randomUUID)(),s=(0,node_path_1.join)(a,"jobs"),c=(0,node_path_1.join)(a,"results",`${o}.result.json`),d=(0,node_path_1.join)(s,`${o}.job.json`),l=`${d}.tmp`;await node_fs_1.promises.mkdir(s,{recursive:!0}),await node_fs_1.promises.rm(c,{force:!0}),await node_fs_1.promises.writeFile(l,JSON.stringify({id:o,input:(0,node_path_1.resolve)(e),outputDirectory:(0,node_path_1.resolve)(t),resolution:i.javaResolution,supersampling:i.javaSupersampling,rotation:i.isometricRotation,slant:i.isometricSlant,fill:i.isometricFill,background:i.background,transparentBackground:i.transparentBackground})),await node_fs_1.promises.rename(l,d);const m=Date.now()+i.javaRenderTimeout;for(;Date.now()<m;){const e=await readJson(c);if(e){if(await node_fs_1.promises.rm(c,{force:!0}),!e.success)throw new Error(`Minecraft Java 渲染失败:${e.error??"未知错误"}`);return}await new Promise(e=>setTimeout(e,200))}throw new Error(`Minecraft Java 渲染超过 ${Math.round(i.javaRenderTimeout/1e3)} 秒`)}async function readJson(e){try{return JSON.parse(await node_fs_1.promises.readFile(e,"utf8"))}catch(e){if("ENOENT"===e?.code||e instanceof SyntaxError)return;throw e}}async function mergeRenderDiagnostics(e,t,i){const a=(0,node_path_1.join)(e,"render-diagnostics.json"),n=await readJson(a);if(!n)return;const r=Array.isArray(n.blocks)?n.blocks:[],o=Array.isArray(n.errors)?n.errors:[];if(!r.length&&!o.length)return;const s=(0,node_path_1.resolve)(t),c=await readJson(s)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},d=new Map;for(const e of c.blocks??[])d.set(`${e.state}\0${e.reason}`,e);for(const e of r){const t=`${e.state}\0${e.reason}`,i=d.get(t);i?(i.count=Number(i.count??0)+Number(e.count??0),i.samples=[...i.samples??[],...e.samples??[]].slice(0,20)):d.set(t,{...e,samples:e.samples??[]})}const l=new Map;for(const e of c.errors??[])l.set(`${e.renderId}\0${e.engine}\0${e.message}`,e);for(const e of o){const t=`${e.renderId}\0${e.engine}\0${e.message}`,i=l.get(t);i?i.count=Number(i.count??0)+Number(e.count??0):l.set(t,{...e})}const m={format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[...d.values()],errors:[...l.values()]};await node_fs_1.promises.mkdir((0,node_path_1.resolve)(t,".."),{recursive:!0}).catch(()=>{}),await node_fs_1.promises.writeFile(s,JSON.stringify(m,null,2)+"\n"),r.length&&i.warn(`记录 ${r.length} 类无法正常渲染的方块;诊断已保存到 ${s}`),o.length&&i.warn(`记录 ${o.length} 类渲染错误;诊断已保存到 ${s}`)}async function rotateDiagnosticsFile(e,t,i){const a=await readJson(e);if(!a||a.pluginVersion===PLUGIN_VERSION)return;await node_fs_1.promises.mkdir(t,{recursive:!0});const n=cachePathSegment("string"==typeof a.pluginVersion?a.pluginVersion:"legacy"),r=formatDiagnosticsTimestamp(new Date),o=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-v${n}-${r}.json`);await node_fs_1.promises.rename(e,o),i.info(`已将旧版诊断汇总归档到 ${o}`)}async function disableDiagnosticsSnapshot(e,t){if(!await readJson(e))return"当前没有可禁用的诊断汇总";await node_fs_1.promises.mkdir(t,{recursive:!0});const i=(0,node_path_1.join)(t,`litematic-renderer-diagnostics-disabled-${formatDiagnosticsTimestamp(new Date)}.json`);return await node_fs_1.promises.rename(e,i),await node_fs_1.promises.writeFile(e,JSON.stringify({format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]},null,2)+"\n"),`当前诊断已移入禁用文件夹:${i}`}async function appendGlobalRenderError(e,t,i,a,n){const r=(0,node_path_1.resolve)(e),o=await readJson(r)??{format:2,pluginVersion:PLUGIN_VERSION,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.pluginVersion=PLUGIN_VERSION,o.errors??=[];const s=o.errors.find(e=>e.renderId===t&&e.engine===i&&e.message===a);s?s.count=Number(s.count??0)+1:o.errors.push({renderId:t,engine:i,message:a,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.mkdir((0,node_path_1.dirname)(r),{recursive:!0}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),n.warn(`渲染问题已记录到 ${r}`)}function formatDiagnosticsTimestamp(e){const t=e=>String(e).padStart(2,"0");return`${e.getFullYear()}${t(e.getMonth()+1)}${t(e.getDate())}-${t(e.getHours())}${t(e.getMinutes())}${t(e.getSeconds())}`}async function appendRenderError(e,t,i,a,n){const r=(0,node_path_1.join)(e,"render-diagnostics.json"),o=await readJson(r)??{format:2,generatedAt:(new Date).toISOString(),blocks:[],errors:[]};o.format=2,o.errors??=[];const s=a instanceof Error?a.message:String(a),c=(0,node_path_1.basename)(e),d=o.errors.find(e=>e.renderId===c&&e.engine===i&&e.message===s);d?d.count=Number(d.count??0)+1:o.errors.push({renderId:c,engine:i,message:s,count:1,generatedAt:(new Date).toISOString()}),await node_fs_1.promises.writeFile(r,JSON.stringify(o,null,2)+"\n"),await mergeRenderDiagnostics(e,t,n)}function formatRenderError(e,t){const i=e instanceof Error?e.message:String(e);return/投影方块数或区域体积超过/i.test(i)?`${i},不渲染`:/文件超过|file size|file.*limit/i.test(i)?`文件大小超过 ${(t.maxFileSize/1024).toFixed(2)} MB,不渲染`:/java 21\+|未找到.*java|java.*not found/i.test(i)?"未找到 Java 21+,请在插件配置中检查 Java 可执行文件设置":/standalone renderer|独立.*渲染器.*不存在|渲染器.*not exist/i.test(i)?"独立渲染器不可用,请重装插件或检查 Java 渲染配置":/minecraft.*resource|resource pack|原版资源|材质包.*不存在/i.test(i)?"Minecraft 原版资源包不可用,请检查资源包配置":"投影渲染失败,请检查渲染器配置或导出诊断文件"}async function renderWithGpu(e,t,i,a,n){try{return await new Promise((n,r)=>{const o=(0,node_child_process_1.spawn)(e,["--input",t,"--output",i,"--views","isometric,isometric-reverse"],{shell:!1,windowsHide:!0}),s=setTimeout(()=>{o.kill(),r(new Error("GPU 渲染超时"))},a);o.once("error",r),o.once("exit",e=>0===e?n():r(new Error(`GPU 渲染器退出码:${e}`))),o.once("close",()=>clearTimeout(s))}),!0}catch(e){return n.warn(`GPU 渲染器不可用,回退 CPU:${e instanceof Error?e.message:String(e)}`),!1}}async function renderWithWebgl(e,t,i){const a=await e.puppeteer.page();try{const e=(0,node_url_1.pathToFileURL)((0,node_path_1.resolve)(__dirname,"../assets/threeviews/index.html")).href;await a.goto(e,{waitUntil:"networkidle0",timeout:i.renderTimeout}),await a.waitForFunction(()=>Boolean(window.litematicViewerAPI&&window.deepslateResources),{timeout:i.renderTimeout});const n=await a.evaluate(async({base64:e,width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})=>{const c=atob(e),d=new Uint8Array(c.length);for(let e=0;e<c.length;e++)d[e]=c.charCodeAt(e);const l=new File([d],"schematic.litematic",{type:"application/octet-stream"});return await window.litematicViewerAPI.loadFromFile(l),window.litematicViewerAPI.renderIsometricViews({width:t,height:i,quality:a,isometricSquare:n,isometricFill:r,isometricRotation:o,isometricSlant:s})},{base64:t.toString("base64"),width:i.webglWidth,height:i.webglHeight,quality:i.webglQuality,isometricSquare:i.isometricSquare,isometricFill:i.isometricFill,isometricRotation:i.isometricRotation,isometricSlant:i.isometricSlant});return[{title:"isometric.png",png:decodeDataUrl(n.isometricView)},{title:"isometric-reverse.png",png:decodeDataUrl(n.reverseIsometricView)}]}finally{await a.close().catch(()=>{})}}function decodeDataUrl(e){if("string"!=typeof e||!e.startsWith("data:image/"))throw new Error("WebGL 渲染器没有返回 PNG 数据");const t=e.indexOf(",");if(t<0)throw new Error("WebGL 渲染器返回了无效图像数据");return Buffer.from(e.slice(t+1),"base64")}function resolveSendOptions(e,t){const i=t?[...e.groupSendOptions].reverse().find(e=>e.groupId.trim()===t):void 0,a="enabled"===i?.replyAndMention||"disabled"!==i?.replyAndMention&&e.replyAndMention,n=i?.sendMode??(e.sendAsForward?"forward":"combined"),r=e.qqBotType??"selfHosted";return{qqBotType:r,sendMode:n,replyAndMention:a,showViewTitles:e.showViewTitles??!1,sixFaceOverview:("official"===r||"forward"===n)&&(e.sixFaceOverview??!0)}}async function sendImages(e,t,i,a,n="schematic"){if("official"===a.qqBotType){const r=await composeQqOverview(t),o=[...a.replyAndMention&&e.messageId?[(0,koishi_1.h)("quote",{id:e.messageId})]:[],koishi_1.h.image((0,node_url_1.pathToFileURL)(r).href),(0,koishi_1.h)("text",{content:`${i}\n${n} 已渲染成功,结果如上`})];return void await e.send(o)}const r=await Promise.all(t.map(async({title:e,path:t})=>({title:e,source:`base64://${(await node_fs_1.promises.readFile(t)).toString("base64")}`}))),o=r.map(({title:t,source:i})=>(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影渲染"},[...a.showViewTitles?[(0,koishi_1.h)("text",{content:t})]:[],(0,koishi_1.h)("img",{src:i})])),s=(0,koishi_1.h)("message",{userId:e.selfId,nickname:"投影信息"},[(0,koishi_1.h)("text",{content:i})]);if("forward"===a.sendMode)return void await e.send((0,koishi_1.h)("figure",{},[...o,s]));const c=a.replyAndMention?replyElements(e,a.qqBotType):[],d=r.flatMap(({title:e,source:t},i)=>[...a.showViewTitles?[(0,koishi_1.h)("text",{content:`${i?"\n":""}${e}\n`})]:[],(0,koishi_1.h)("img",{src:t})]);await e.send([...c,...d,(0,koishi_1.h)("text",{content:`\n${i}\n${n} 已渲染成功,结果如上`})])}async function composeQqOverview(e){if(!e.length)throw new Error("没有可合并的渲染图片");const t=await Promise.all(e.map(async e=>pngjs_1.PNG.sync.read(await node_fs_1.promises.readFile((0,node_path_1.resolve)(e.path))))),i=Math.max(...t.map(e=>e.width)),a=Math.max(4,Math.round(.01*i)),n=[];let r=0;const o=t.slice(0,Math.min(2,t.length)),s=1===o.length?i:Math.floor((i-a)/2),c=o.map(e=>Math.max(1,Math.round(e.height*s/e.width))),d=Math.max(...c);for(let e=0;e<o.length;e++)n.push({image:o[e],x:e*(s+a),y:Math.floor((d-c[e])/2),width:s,height:c[e]});r+=d;for(const e of t.slice(2)){r+=a;const t=Math.max(1,Math.round(e.height*i/e.width));n.push({image:e,x:0,y:r,width:i,height:t}),r+=t}const l=new pngjs_1.PNG({width:i,height:r});for(const e of n)drawScaledPng(l,e);const m=(0,node_path_1.resolve)((0,node_path_1.dirname)(e[0].path),"qq-overview.png");return await node_fs_1.promises.writeFile(m,pngjs_1.PNG.sync.write(l)),m}function drawScaledPng(e,t){const{image:i,x:a,y:n,width:r,height:o}=t;for(let t=0;t<o;t++){const s=Math.min(i.height-1,Math.floor((t+.5)*i.height/o));for(let o=0;o<r;o++){const c=Math.min(i.width-1,Math.floor((o+.5)*i.width/r)),d=4*(s*i.width+c),l=4*((n+t)*e.width+a+o);i.data.copy(e.data,l,d,d+4)}}}function replyElements(e,t=("qq"===e.platform?"official":"selfHosted")){const i=[];return e.messageId&&i.push((0,koishi_1.h)("quote",{id:e.messageId})),"selfHosted"===t&&e.userId&&(i.push((0,koishi_1.h)("at",{id:e.userId})),i.push((0,koishi_1.h)("text",{content:"\n"}))),i}class NbtReader{data;offset=0;constructor(e){this.data=e}byte(){return this.data.readInt8(this.offset++)}ubyte(){return this.data.readUInt8(this.offset++)}short(){const e=this.data.readInt16BE(this.offset);return this.offset+=2,e}int(){const e=this.data.readInt32BE(this.offset);return this.offset+=4,e}float(){const e=this.data.readFloatBE(this.offset);return this.offset+=4,e}double(){const e=this.data.readDoubleBE(this.offset);return this.offset+=8,e}long(){const e=this.data.readBigInt64BE(this.offset);return this.offset+=8,e}text(){const e=this.data.readUInt16BE(this.offset);this.offset+=2;const t=this.data.toString("utf8",this.offset,this.offset+e);return this.offset+=e,t}readNamedRoot(){if(10!==this.ubyte())throw new Error("不是 NBT Compound 数据");return this.text(),this.value(10)}value(e){switch(e){case 0:return null;case 1:return this.byte();case 2:return this.short();case 3:return this.int();case 4:return this.long();case 5:return this.float();case 6:return this.double();case 7:{const e=this.int(),t=this.data.subarray(this.offset,this.offset+e);return this.offset+=e,t}case 8:return this.text();case 9:{const e=this.ubyte(),t=this.int();return Array.from({length:t},()=>this.value(e))}case 10:{const e={};for(;;){const t=this.ubyte();if(!t)return e;e[this.text()]=this.value(t)}}case 11:{const e=this.int();return Array.from({length:e},()=>this.int())}case 12:{const e=this.int();return Array.from({length:e},()=>this.long())}default:throw new Error(`不支持的 NBT 标签类型:${e}`)}}}const MINECRAFT_DATA_VERSIONS=new Map([[4903,"26.2"],[4790,"26.1.2"],[4671,"1.21.11"],[4557,"1.21.10"],[4555,"1.21.9"],[4440,"1.21.8"],[4438,"1.21.7"],[4435,"1.21.6"],[4325,"1.21.5"],[4189,"1.21.4"],[4082,"1.21.3"],[4080,"1.21.2"],[3955,"1.21.1"],[3953,"1.21"],[3839,"1.20.6"],[3837,"1.20.5"],[3700,"1.20.4"],[3698,"1.20.3"],[3578,"1.20.2"],[3465,"1.20.1"],[3463,"1.20"],[3337,"1.19.4"],[3218,"1.19.3"],[3120,"1.19.2"],[3117,"1.19.1"],[3105,"1.19"],[2975,"1.18.2"]]);function readLitematicRoot(e){let t;try{t=(0,node_zlib_1.gunzipSync)(e)}catch{t=e}return new NbtReader(t).readNamedRoot()}function compoundValue(e){return!e||"object"!=typeof e||Array.isArray(e)||Buffer.isBuffer(e)?void 0:e}function numericValue(e){if("number"==typeof e&&Number.isFinite(e))return e;if("bigint"==typeof e){const t=Number(e);if(Number.isSafeInteger(t))return t}}function readMetadataSize(e,t){const i=compoundValue(t.EnclosingSize),a=i&&[numericValue(i.x),numericValue(i.y),numericValue(i.z)];if(a?.every(e=>null!=e))return a.map(e=>Math.abs(e));const n=compoundValue(e.Regions);if(!n)return;let r=1/0,o=1/0,s=1/0,c=-1/0,d=-1/0,l=-1/0;for(const e of Object.values(n)){const t=compoundValue(e),i=compoundValue(t?.Position),a=compoundValue(t?.Size),n=numericValue(i?.x),m=numericValue(i?.y),u=numericValue(i?.z),h=numericValue(a?.x),f=numericValue(a?.y),p=numericValue(a?.z);if([n,m,u,h,f,p].some(e=>null==e)||0===h||0===f||0===p)continue;const g=n+h-Math.sign(h),_=m+f-Math.sign(f),w=u+p-Math.sign(p);r=Math.min(r,n,g),o=Math.min(o,m,_),s=Math.min(s,u,w),c=Math.max(c,n,g),d=Math.max(d,m,_),l=Math.max(l,u,w)}return Number.isFinite(r)?[c-r+1,d-o+1,l-s+1]:void 0}function formatCreatedAt(e){if(null==e||e<=0)return"未知";const t=new Date(e<1e10?1e3*e:e);if(Number.isNaN(t.getTime()))return"未知";const i=new Intl.DateTimeFormat("zh-CN",{timeZone:"Asia/Shanghai",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",hourCycle:"h23"}).formatToParts(t),a=Object.fromEntries(i.map(e=>[e.type,e.value]));return`${a.year}-${a.month}-${a.day} ${a.hour}:${a.minute}:${a.second}`}function parseLitematicMetadata(e){const t=readLitematicRoot(e),i=compoundValue(t.Metadata)??{},a=numericValue(t.MinecraftDataVersion);return{author:"string"==typeof i.Author&&i.Author.trim()?i.Author.trim():"未知",createdAt:formatCreatedAt(numericValue(i.TimeCreated)),totalBlocks:numericValue(i.TotalBlocks),totalVolume:numericValue(i.TotalVolume),size:readMetadataSize(t,i),litematicVersion:numericValue(t.Version),minecraftDataVersion:a,minecraftVersion:null==a?void 0:MINECRAFT_DATA_VERSIONS.get(a)}}function validateProjectionLimits(e,t,i){const a=Number.isSafeInteger(i)&&i>0?i:5e6,n=()=>{throw new Error(`投影方块数或区域体积超过 ${a.toLocaleString("en-US")} 限制`)};(Math.abs(t.totalBlocks??0)>a||Math.abs(t.totalVolume??0)>a)&&n();const r=compoundValue(readLitematicRoot(e).Regions);if(!r)return;let o=0;for(const e of Object.values(r)){const t=compoundValue(compoundValue(e)?.Size),i=[numericValue(t?.x),numericValue(t?.y),numericValue(t?.z)];if(i.some(e=>null==e))continue;const r=i.reduce((e,t)=>e*Math.abs(t),1);(!Number.isSafeInteger(r)||r>a-o)&&n(),o+=r}}function formatLitematicMetadata(e,t){const i=null==e.totalBlocks?"未知":String(e.totalBlocks),a=null==e.totalVolume?"未知":String(e.totalVolume),n=e.size?.join(" × ")??"未知",r=null==e.litematicVersion?"未知":String(e.litematicVersion),o=e.minecraftVersion??"未知",s=null==e.minecraftDataVersion?"":`(数据版本:${e.minecraftDataVersion})`,c="string"==typeof t&&t.trim()?(0,node_path_1.basename)(t).replace(/\.litematic$/i,""):"";return[...c?[`投影名称:${c}`]:[],`保存者游戏 ID:${e.author}`,`创建时间:${e.createdAt}`,`方块数/体积:${i}/${a}`,`尺寸:${n}`,`Litematic 版本:${r}`,`游戏版本:${o}${s}`].join("\n")}function parseLitematic(e){const t=readLitematicRoot(e).Regions;if(!t||"object"!=typeof t)throw new Error("文件不含 Litematica Regions 数据");const i=[];for(const e of Object.values(t))parseRegion(e,i);if(!i.length)throw new Error("投影中没有非空气方块");return i}function parseRegion(e,t){const i=e.Position,a=e.Size,n=e.BlockStatePalette,r=e.BlockStates;if(!(i&&a&&Array.isArray(n)&&Array.isArray(r)))return;const o=Math.abs(a.x),s=Math.abs(a.y),c=Math.abs(a.z),d=o*s*c;if(!d)return;const l=Math.max(2,Math.ceil(Math.log2(n.length))),m=a.x<0?-1:1,u=a.y<0?-1:1,h=a.z<0?-1:1;for(let e=0;e<d;e++){const a=unpackState(r,e,l),s=n[a]?.Name;if(!s||"minecraft:air"===s||"minecraft:cave_air"===s||"minecraft:void_air"===s)continue;const d=e%o,f=Math.floor(e/o)%c,p=Math.floor(e/(o*c));t.push({x:i.x+d*m,y:i.y+p*u,z:i.z+f*h,name:s})}}function unpackState(e,t,i){const a=BigInt(t*i),n=Number(a/64n),r=a%64n;if(n>=e.length)return 0;const o=(1n<<BigInt(i))-1n;let s=BigInt.asUintN(64,e[n])>>r&o;return r+BigInt(i)>64n&&n+1<e.length&&(s|=BigInt.asUintN(64,e[n+1])<<64n-r&o),Number(s)}function renderSchematic(e,t){const i=getBounds(e);return[{title:"isometric.png",png:renderIsometric(e,i,t,!1)},{title:"isometric-reverse.png",png:renderIsometric(e,i,t,!0)}]}function getBounds(e){return e.reduce((e,t)=>({minX:Math.min(e.minX,t.x),minY:Math.min(e.minY,t.y),minZ:Math.min(e.minZ,t.z),maxX:Math.max(e.maxX,t.x),maxY:Math.max(e.maxY,t.y),maxZ:Math.max(e.maxZ,t.z)}),{minX:1/0,minY:1/0,minZ:1/0,maxX:-1/0,maxY:-1/0,maxZ:-1/0})}const SIX_FACE_ORDER=["up","down","east","south","west","north"],SIX_FACE_LABELS={up:["00000100000","00000100000","00000100000","00000100000","00000111110","00000100000","00000100000","00000100000","00000100000","00000100000","11111111111"],down:["00000000000","11111111111","00000100000","00000100000","00000110000","00000101000","00000100100","00000100100","00000100000","00000100000","00000100000"],east:["00000100000","00000100000","01111111111","00001010000","00010010000","00111111110","00000010000","00010010100","00100010010","01000010001","00001110000"],south:["00000100000","11111111111","00000100000","01111111110","01010001010","01001010010","01111111110","01000100010","01111111110","01000100010","01000100110"],west:["00000000000","11111111111","00001010000","01111111110","01001010010","01001010010","01010010010","01100001110","01000000010","01111111110","01000000010"],north:["00010010000","00010010000","00010010000","11110010011","00010011100","00010010000","00010010000","00110010000","01010010001","10010010001","00010001111"]};function renderSixFaceOverview(e,t,i=t.sixFaceLayout??"horizontal"){if(!e.length)throw new Error("投影中没有可生成六面图的方块");const a=Math.max(128,Math.floor(t.outputSize)),n=Math.max(4,Math.round(.01*a)),r="vertical"===i?2:3,o="vertical"===i?3:2,s=Math.floor((a-n*("vertical"===i?o+1:r+1))/("vertical"===i?o:r)),c=new Raster("vertical"===i?r*s+n*(r+1):a,"vertical"===i?a:o*s+n*(o+1),t),d=getBounds(e);for(const[t,i]of SIX_FACE_ORDER.entries()){const a=n+t%r*(s+n),o=n+Math.floor(t/r)*(s+n);drawOrthographicFace(c,e,d,i,a,o,s,n),drawFaceLabel(c,i,a,o,s,n)}return c.png()}function drawOrthographicFace(e,t,i,a,n,r,o,s){const c="east"===a||"west"===a?i.maxZ-i.minZ+1:i.maxX-i.minX+1,d="up"===a||"down"===a?i.maxZ-i.minZ+1:i.maxY-i.minY+1,l=new Map;for(const e of t){const t=orthographicCoordinates(e,i,a),n=`${t.a},${t.b}`,r=l.get(n);(!r||t.depth>r.depth)&&l.set(n,{block:e,...t})}const m=Math.max(2,Math.floor(s/2)),u=Math.max(1,o-2*m),h=Math.min(u/c,u/d),f=n+m+(u-c*h)/2,p=r+m+(u-d*h)/2;for(const{block:t,a:i,b:a}of[...l.values()].sort((e,t)=>e.b-t.b||e.a-t.a)){const n=Math.floor(f+i*h),r=Math.floor(p+a*h),o=Math.max(n+1,Math.ceil(f+(i+1)*h)),s=Math.max(r+1,Math.ceil(p+(a+1)*h));e.rect(n,r,o-n,s-r,colorFor(t.name))}}function orthographicCoordinates(e,t,i){switch(i){case"up":return{a:e.x-t.minX,b:e.z-t.minZ,depth:e.y};case"down":return{a:t.maxX-e.x,b:e.z-t.minZ,depth:-e.y};case"east":return{a:t.maxZ-e.z,b:t.maxY-e.y,depth:e.x};case"south":return{a:e.x-t.minX,b:t.maxY-e.y,depth:e.z};case"west":return{a:e.z-t.minZ,b:t.maxY-e.y,depth:-e.x};case"north":return{a:t.maxX-e.x,b:t.maxY-e.y,depth:-e.z}}}function drawFaceLabel(e,t,i,a,n,r){const o=SIX_FACE_LABELS[t],s=Math.max(1,Math.min(6,Math.floor(n/80))),c=Math.max(1,Math.floor(s/2)),d=i+Math.max(2,Math.floor(r/2)),l=a+Math.max(2,Math.floor(r/2));e.rect(d,l,o[0].length*s+2*c,o.length*s+2*c,"#000000");for(let t=0;t<o.length;t++)for(let i=0;i<o[t].length;i++)"1"===o[t][i]&&e.rect(d+c+i*s,l+c+t*s,s,s,"#ffffff")}function renderIsometric(e,t,i,a){const n=i.isometricCellSize,r=Math.ceil(n/2),o=(t.maxX-t.minX+t.maxZ-t.minZ+2)*n+4,s=(t.maxX-t.minX+t.maxZ-t.minZ+2)*r+(t.maxY-t.minY+2)*n+4,c=Math.max(1,Math.min(1,i.outputSize/Math.max(o,s))),d=new Raster(Math.ceil(o*c),Math.ceil(s*c),i),l=e=>({x:a?t.maxX-e.x:e.x-t.minX,z:a?t.maxZ-e.z:e.z-t.minZ}),m=[...e].sort((e,t)=>{const i=l(e),a=l(t);return i.x+i.z+e.y-(a.x+a.z+t.y)});for(const e of m){const i=l(e),a=(i.x-i.z)*n+o/2,s=(i.x+i.z)*r+(t.maxY-e.y+1)*n;d.voxel(Math.round(a*c),Math.round(s*c),Math.max(2,Math.round(n*c)),colorFor(e.name))}return d.png()}const COLORS={stone:"#777777",cobblestone:"#707070",deepslate:"#4b4b50",dirt:"#855b37",grass_block:"#63913d",sand:"#dbca85",glass:"#a7d9df",water:"#3f76e4",lava:"#e05b22",oak_planks:"#b9915a",spruce_planks:"#765033",birch_planks:"#d9c485",iron_block:"#d7d7d7",gold_block:"#f6c543",diamond_block:"#5ed6c7",redstone_block:"#b21d1d",netherrack:"#823332",quartz_block:"#e9e4dc",obsidian:"#211d2d",white_wool:"#e9e9e9",black_wool:"#1d1d1d",red_wool:"#a12722",blue_wool:"#3549a7",green_wool:"#4c702c",yellow_wool:"#e5c440"};function colorFor(e){const t=e.replace(/^minecraft:/,"");if(COLORS[t])return COLORS[t];for(const[e,i]of Object.entries(COLORS))if(t.includes(e))return i;const i=(0,node_crypto_1.createHash)("md5").update(t).digest();return`#${(90+i[0]%120).toString(16).padStart(2,"0")}${(90+i[1]%120).toString(16).padStart(2,"0")}${(90+i[2]%120).toString(16).padStart(2,"0")}`}class Raster{width;height;data;background;constructor(e,t,i){this.width=e,this.height=t,this.background=parseColor(i.background,i.transparentBackground?0:255),this.data=Buffer.alloc(e*t*4);for(let e=0;e<this.data.length;e+=4)this.data.set(this.background,e)}rect(e,t,i,a,n){for(let r=Math.max(0,t);r<Math.min(this.height,t+a);r++)for(let t=Math.max(0,e);t<Math.min(this.width,e+i);t++)this.pixel(t,r,n)}pixel(e,t,i){e<0||t<0||e>=this.width||t>=this.height||this.data.set(parseColor(i),4*(t*this.width+e))}voxel(e,t,i,a){const n=adjust(a,1.15),r=adjust(a,.78),o=adjust(a,.6),s=Math.ceil(i/2);for(let a=0;a<s;a++)for(let r=-i+a;r<=i-a;r++)this.pixel(e+r,t-s+a,n);for(let a=0;a<i;a++)for(let n=-i+Math.floor(a/2);n<=0;n++)this.pixel(e+n,t+a,r);for(let a=0;a<i;a++)for(let n=0;n<=i-Math.floor(a/2);n++)this.pixel(e+n,t+a,o)}png(){const e=Buffer.alloc((4*this.width+1)*this.height);for(let t=0;t<this.height;t++)e[t*(4*this.width+1)]=0,this.data.copy(e,t*(4*this.width+1)+1,t*this.width*4,(t+1)*this.width*4);const t=Buffer.alloc(13);return t.writeUInt32BE(this.width,0),t.writeUInt32BE(this.height,4),t.set([8,6,0,0,0],8),Buffer.concat([Buffer.from("PNG\r\n\n","binary"),chunk("IHDR",t),chunk("IDAT",(0,node_zlib_1.deflateSync)(e)),chunk("IEND",Buffer.alloc(0))])}}function parseColor(e,t=255){const i=/^#?([0-9a-f]{6})$/i.exec(e)?.[1]??"182026";return[parseInt(i.slice(0,2),16),parseInt(i.slice(2,4),16),parseInt(i.slice(4,6),16),t]}function adjust(e,t){const[i,a,n]=parseColor(e);return`#${[i,a,n].map(e=>Math.round(Math.min(255,e*t)).toString(16).padStart(2,"0")).join("")}`}function chunk(e,t){const i=Buffer.from(e),a=Buffer.alloc(4);a.writeUInt32BE(t.length);const n=Buffer.alloc(4);return n.writeUInt32BE(crc32(Buffer.concat([i,t]))>>>0),Buffer.concat([a,i,t,n])}const CRC_TABLE=Array.from({length:256},(e,t)=>{let i=t;for(let e=0;e<8;e++)i=1&i?3988292384^i>>>1:i>>>1;return i>>>0});function crc32(e){let t=4294967295;for(const i of e)t=CRC_TABLE[255&(t^i)]^t>>>8;return(4294967295^t)>>>0}
|