@hato810424/mc-resources-plugin 0.0.0-beta.11
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/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +43 -0
- package/packages/mc-resources-plugin/dist/core-BHoK8fPV.mjs +2300 -0
- package/packages/mc-resources-plugin/dist/core-BP2mwKSk.cjs +2333 -0
- package/packages/mc-resources-plugin/dist/docusaurus.cjs +54 -0
- package/packages/mc-resources-plugin/dist/docusaurus.d.cts +6 -0
- package/packages/mc-resources-plugin/dist/docusaurus.d.mts +7 -0
- package/packages/mc-resources-plugin/dist/docusaurus.mjs +54 -0
- package/packages/mc-resources-plugin/dist/types-BEv9afFy.d.mts +21 -0
- package/packages/mc-resources-plugin/dist/types-BGQHsCJS.d.cts +21 -0
- package/packages/mc-resources-plugin/dist/vite.cjs +56 -0
- package/packages/mc-resources-plugin/dist/vite.d.cts +12 -0
- package/packages/mc-resources-plugin/dist/vite.d.mts +13 -0
- package/packages/mc-resources-plugin/dist/vite.mjs +56 -0
- package/packages/mc-resources-plugin/dist/webpack.cjs +83 -0
- package/packages/mc-resources-plugin/dist/webpack.d.cts +15 -0
- package/packages/mc-resources-plugin/dist/webpack.d.mts +16 -0
- package/packages/mc-resources-plugin/dist/webpack.mjs +83 -0
|
@@ -0,0 +1,2300 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import findCacheDirectory from "find-cache-directory";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import path, { extname, join, relative } from "node:path";
|
|
6
|
+
import { format } from "node:util";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
9
|
+
import { dirname, join as join$1 } from "path";
|
|
10
|
+
import StreamZip from "node-stream-zip";
|
|
11
|
+
import { mkdir, readFile, readdir } from "fs/promises";
|
|
12
|
+
import { createCanvas, loadImage } from "canvas";
|
|
13
|
+
import sharp from "sharp";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
//#region rolldown:runtime
|
|
17
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/env.ts
|
|
21
|
+
const CONFIG = {
|
|
22
|
+
OUTPUT_DIR: "./mcpacks",
|
|
23
|
+
EMPTY_OUT_DIR: false,
|
|
24
|
+
INCLUDE: [
|
|
25
|
+
"**/*.ts",
|
|
26
|
+
"**/*.tsx",
|
|
27
|
+
"**/*.js",
|
|
28
|
+
"**/*.jsx"
|
|
29
|
+
],
|
|
30
|
+
EXCLUDE: [],
|
|
31
|
+
CACHE_DIR: findCacheDirectory({
|
|
32
|
+
name: "@hato810424/mc-resources-plugin",
|
|
33
|
+
create: true
|
|
34
|
+
}),
|
|
35
|
+
START_UP_RENDER_CACHE_REFRESH: false,
|
|
36
|
+
TEXTURE_SIZE: 16,
|
|
37
|
+
WIDTH: 128,
|
|
38
|
+
HEIGHT: 128,
|
|
39
|
+
ROTATION: [
|
|
40
|
+
-30,
|
|
41
|
+
45,
|
|
42
|
+
0
|
|
43
|
+
],
|
|
44
|
+
LOG_LEVEL: "info"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/types.ts
|
|
49
|
+
const PluginOptionsSchema = z.object({
|
|
50
|
+
mcVersion: z.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, "有効なバージョン形式 (X.Y.Z) を入力してください"),
|
|
51
|
+
resourcePackPath: z.string(),
|
|
52
|
+
outputPath: z.string().optional(),
|
|
53
|
+
emptyOutDir: z.boolean().optional(),
|
|
54
|
+
include: z.array(z.string()).optional(),
|
|
55
|
+
exclude: z.array(z.string()).optional(),
|
|
56
|
+
cacheDir: z.string().optional(),
|
|
57
|
+
startUpRenderCacheRefresh: z.boolean().optional(),
|
|
58
|
+
logLevel: z.enum([
|
|
59
|
+
"info",
|
|
60
|
+
"debug",
|
|
61
|
+
"error"
|
|
62
|
+
]).optional()
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/filesystem.ts
|
|
67
|
+
/**
|
|
68
|
+
* resourcePackPath/assets/minecraft 内のすべての画像ファイルを再帰的に取得
|
|
69
|
+
*/
|
|
70
|
+
function getAllImages(resourcePackPath) {
|
|
71
|
+
const images = [];
|
|
72
|
+
const minecraftPath = join(resourcePackPath, "assets", "minecraft");
|
|
73
|
+
function walkDir(currentPath, basePath) {
|
|
74
|
+
try {
|
|
75
|
+
const entries = readdirSync(currentPath);
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = join(currentPath, entry);
|
|
78
|
+
if (statSync(fullPath).isDirectory()) walkDir(fullPath, basePath);
|
|
79
|
+
else {
|
|
80
|
+
const ext = extname(entry).toLowerCase();
|
|
81
|
+
if ([
|
|
82
|
+
".png",
|
|
83
|
+
".jpg",
|
|
84
|
+
".jpeg",
|
|
85
|
+
".gif",
|
|
86
|
+
".webp"
|
|
87
|
+
].includes(ext)) {
|
|
88
|
+
const relativePath = relative(basePath, fullPath).replace(/\\/g, "/");
|
|
89
|
+
images.push({
|
|
90
|
+
path: `/${relativePath}`,
|
|
91
|
+
relativePath
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
walkDir(minecraftPath, minecraftPath);
|
|
99
|
+
return images;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 出力ディレクトリを初期化
|
|
103
|
+
*/
|
|
104
|
+
function initializeOutputDirectory(outputPath, emptyOutDir) {
|
|
105
|
+
if (emptyOutDir && existsSync(outputPath)) {
|
|
106
|
+
rmSync(outputPath, { recursive: true });
|
|
107
|
+
mkdirSync(outputPath, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
mkdirSync(outputPath, { recursive: true });
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* ファイルをディスクに書き込む
|
|
115
|
+
*/
|
|
116
|
+
function writeFiles(outputPath, jsCode, tsCode) {
|
|
117
|
+
writeFileSync(join(outputPath, "resourcepack.mjs"), jsCode, "utf-8");
|
|
118
|
+
writeFileSync(join(outputPath, "resourcepack.d.ts"), tsCode, "utf-8");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/codeGenerator.ts
|
|
123
|
+
/**
|
|
124
|
+
* 画像情報から getMcResources 関数を生成
|
|
125
|
+
*/
|
|
126
|
+
async function generateGetResourcePackCode({ images, usedIds, itemManager, versionId, itemsUrlMap }) {
|
|
127
|
+
let filteredImages = usedIds ? images.filter((img) => {
|
|
128
|
+
const itemId = "minecraft:" + img.path.split("/").pop()?.replace(/\.[^.]+$/, "");
|
|
129
|
+
return usedIds.has(itemId);
|
|
130
|
+
}) : images;
|
|
131
|
+
const items = /* @__PURE__ */ new Set();
|
|
132
|
+
if (itemManager) {
|
|
133
|
+
(await itemManager.get3DItemsLazy(versionId)).forEach((id) => items.add(id));
|
|
134
|
+
(await itemManager.getItemIds(versionId)).forEach((id) => items.add(id));
|
|
135
|
+
}
|
|
136
|
+
let itemsImports = "";
|
|
137
|
+
const itemHashMap = /* @__PURE__ */ new Map();
|
|
138
|
+
if (itemsUrlMap) {
|
|
139
|
+
let importIndex = 0;
|
|
140
|
+
for (const [key, renderedPath] of itemsUrlMap) {
|
|
141
|
+
const lastUnderscore = key.lastIndexOf("_");
|
|
142
|
+
let itemId;
|
|
143
|
+
let optionHash;
|
|
144
|
+
if (lastUnderscore > 0) {
|
|
145
|
+
const possibleHash = key.substring(lastUnderscore + 1);
|
|
146
|
+
if (!possibleHash.includes(":")) {
|
|
147
|
+
itemId = key.substring(0, lastUnderscore);
|
|
148
|
+
optionHash = possibleHash;
|
|
149
|
+
} else {
|
|
150
|
+
itemId = key;
|
|
151
|
+
optionHash = "default";
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
itemId = key;
|
|
155
|
+
optionHash = "default";
|
|
156
|
+
}
|
|
157
|
+
const importVarName = `_r${importIndex}`;
|
|
158
|
+
itemsImports += `import ${importVarName} from "${renderedPath}";\n`;
|
|
159
|
+
if (!itemHashMap.has(itemId)) itemHashMap.set(itemId, /* @__PURE__ */ new Map());
|
|
160
|
+
itemHashMap.get(itemId).set(optionHash, importVarName);
|
|
161
|
+
importIndex++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const mapEntries = [];
|
|
165
|
+
items.forEach((itemId) => {
|
|
166
|
+
const hashMap = itemHashMap.get(itemId);
|
|
167
|
+
if (hashMap && hashMap.has("default")) mapEntries.push(` "${itemId}": ${hashMap.get("default")}`);
|
|
168
|
+
else mapEntries.push(` "${itemId}": "/@hato810424:mc-resources-plugin/minecraft:${itemId.replace("minecraft:", "")}"`);
|
|
169
|
+
});
|
|
170
|
+
if (itemHashMap.size > 0) {
|
|
171
|
+
for (const [itemId, hashMap] of itemHashMap) if (!filteredImages.some((img) => `minecraft:${img.path.split("/").pop()?.replace(/\.[^.]+$/, "")}` === itemId)) for (const [optionHash, importVar] of hashMap) if (optionHash === "default") mapEntries.push(` "${itemId}": ${importVar}`);
|
|
172
|
+
else mapEntries.push(` "${itemId}_${optionHash}": ${importVar}`);
|
|
173
|
+
}
|
|
174
|
+
const finalMap = mapEntries.join(",\n");
|
|
175
|
+
return `${itemsImports}
|
|
176
|
+
|
|
177
|
+
const resourcePack = {
|
|
178
|
+
${finalMap}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
function buildQueryString(params) {
|
|
182
|
+
return new URLSearchParams(
|
|
183
|
+
Object.entries(params).reduce((acc, [key, value]) => {
|
|
184
|
+
if (value !== undefined && value !== null) {
|
|
185
|
+
acc[key] = String(value);
|
|
186
|
+
}
|
|
187
|
+
return acc;
|
|
188
|
+
}, {})
|
|
189
|
+
).toString();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function generateOptionHash(width, height, scale) {
|
|
193
|
+
const parts = [];
|
|
194
|
+
if (width !== undefined) parts.push(\`w\${width}\`);
|
|
195
|
+
if (height !== undefined) parts.push(\`h\${height}\`);
|
|
196
|
+
if (scale !== undefined) parts.push(\`s\${scale}\`);
|
|
197
|
+
return parts.join('_');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getResourcePack(itemId, options = {}) {
|
|
201
|
+
// ビルド時にレンダリングされた画像を優先的に使用
|
|
202
|
+
if (options.width || options.height || options.scale) {
|
|
203
|
+
const optionHash = generateOptionHash(options.width, options.height, options.scale);
|
|
204
|
+
const hashKey = \`\${itemId}_\${optionHash}\`;
|
|
205
|
+
const hashedUrl = resourcePack[hashKey];
|
|
206
|
+
if (hashedUrl) {
|
|
207
|
+
return hashedUrl;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const resourceUrl = resourcePack[itemId] ?? null;
|
|
212
|
+
if (!resourceUrl) return null;
|
|
213
|
+
|
|
214
|
+
const queryString = buildQueryString(options);
|
|
215
|
+
return queryString ? \`\${resourceUrl}?\${queryString}\` : resourceUrl;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default resourcePack;`;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* TypeScript型定義を生成
|
|
222
|
+
*/
|
|
223
|
+
async function generateTypeDefinitions({ images, itemManager, versionId }) {
|
|
224
|
+
const filteredImages = images;
|
|
225
|
+
const FunctionOptions = `
|
|
226
|
+
type FunctionOptions = {
|
|
227
|
+
width: number;
|
|
228
|
+
height?: number;
|
|
229
|
+
scale?: number;
|
|
230
|
+
};
|
|
231
|
+
`.replace(/^\n/, "").replace(/[ \t]+$/, "");
|
|
232
|
+
if (itemManager && versionId) {
|
|
233
|
+
let itemMap = /* @__PURE__ */ new Set();
|
|
234
|
+
let items = /* @__PURE__ */ new Set();
|
|
235
|
+
(await itemManager.get3DItemsLazy(versionId)).forEach((id) => items.add(id));
|
|
236
|
+
(await itemManager.getItemIds(versionId)).forEach((id) => items.add(id));
|
|
237
|
+
const itemMapPromises = filteredImages.map(async (img) => {
|
|
238
|
+
const itemId = "minecraft:" + img.path.split("/").pop()?.replace(/\.[^.]+$/, "");
|
|
239
|
+
if (itemId) try {
|
|
240
|
+
if (await itemManager.getItemTexturePath(versionId, itemId)) return itemId;
|
|
241
|
+
} catch (error) {}
|
|
242
|
+
return null;
|
|
243
|
+
});
|
|
244
|
+
const results = await Promise.all(itemMapPromises);
|
|
245
|
+
for (const itemId of results) if (itemId) itemMap.add(itemId);
|
|
246
|
+
const allItems = new Set([...itemMap, ...items]);
|
|
247
|
+
const Items = itemMap;
|
|
248
|
+
const renderItems = items;
|
|
249
|
+
const hasFunctionSignature = allItems.size > 0;
|
|
250
|
+
return format(`
|
|
251
|
+
type ItemId = %s;
|
|
252
|
+
type RenderingItemId = %s;
|
|
253
|
+
%s
|
|
254
|
+
%s
|
|
255
|
+
export const resourcePack: Readonly<Record<ItemId | RenderingItemId, string>>;
|
|
256
|
+
export default resourcePack;
|
|
257
|
+
`.replace(/^\n/, "").replace(/[ \t]+$/, ""), Items.size > 0 ? Array.from(Items).map((item) => `"${item}"`).join(" | ") : "\"\"", renderItems.size > 0 ? Array.from(renderItems).map((item) => `"${item}"`).join(" | ") : "\"\"", FunctionOptions, (hasFunctionSignature ? `
|
|
258
|
+
export function getResourcePack(itemId: ItemId): string;
|
|
259
|
+
export function getResourcePack(itemId: RenderingItemId, options?: FunctionOptions): string;
|
|
260
|
+
` : "").replace(/^\n/, "").replace(/[ \t]+$/, ""));
|
|
261
|
+
} else return format(`
|
|
262
|
+
type ItemId = %s;
|
|
263
|
+
%s
|
|
264
|
+
%s
|
|
265
|
+
export const resourcePack: Readonly<Record<ItemId, string>>;
|
|
266
|
+
export default resourcePack;
|
|
267
|
+
`.replace(/^\n/, "").replace(/[ \t]+$/, ""), filteredImages.length > 0 ? filteredImages.map((img) => `"${img.path}"`).join(" | ") : "\"\"", FunctionOptions, filteredImages.length > 0 ? "export function getResourcePack(path: ItemId): string;" : "");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
//#region package.json
|
|
272
|
+
var package_default = {
|
|
273
|
+
name: "@hato810424/mc-resources-plugin",
|
|
274
|
+
type: "module",
|
|
275
|
+
scripts: {
|
|
276
|
+
"build": "tsdown",
|
|
277
|
+
"dev": "concurrently -c cyan,yellow --names esm,cjs \"tsdown --watch --format esm\" \"tsdown --watch --format cjs\"",
|
|
278
|
+
"test": "vitest",
|
|
279
|
+
"typecheck": "tsc --noEmit"
|
|
280
|
+
},
|
|
281
|
+
devDependencies: {
|
|
282
|
+
"@docusaurus/types": "^3.9.2",
|
|
283
|
+
"@types/node": "^25.0.10",
|
|
284
|
+
"concurrently": "^9.2.1",
|
|
285
|
+
"tsdown": "^0.18.4",
|
|
286
|
+
"tsx": "^4.21.0",
|
|
287
|
+
"typescript": "^5.9.3",
|
|
288
|
+
"vite": "^7.3.1",
|
|
289
|
+
"vitest": "^4.0.16",
|
|
290
|
+
"webpack": "^5.104.1",
|
|
291
|
+
"webpack-dev-server": "^5.2.3"
|
|
292
|
+
},
|
|
293
|
+
dependencies: {
|
|
294
|
+
"canvas": "^3.2.1",
|
|
295
|
+
"chalk": "^5.6.2",
|
|
296
|
+
"find-cache-directory": "^6.0.0",
|
|
297
|
+
"node-stream-zip": "^1.15.0",
|
|
298
|
+
"sharp": "^0.34.5",
|
|
299
|
+
"zod": "^4.3.6"
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
//#endregion
|
|
304
|
+
//#region src/logger.ts
|
|
305
|
+
const PREFIX = chalk.cyan(`[mc-resources-plugin ${package_default.version}]`) + " ";
|
|
306
|
+
let logLevel = CONFIG.LOG_LEVEL;
|
|
307
|
+
const logger = {
|
|
308
|
+
setLogLevel: (level) => {
|
|
309
|
+
logLevel = level === void 0 ? CONFIG.LOG_LEVEL : level;
|
|
310
|
+
},
|
|
311
|
+
info: (message) => {
|
|
312
|
+
if (logLevel === "error") return;
|
|
313
|
+
console.log(PREFIX + message);
|
|
314
|
+
},
|
|
315
|
+
warn: (message) => {
|
|
316
|
+
console.warn(PREFIX + chalk.bgRed("WARN") + " " + message);
|
|
317
|
+
},
|
|
318
|
+
debug: (message) => {
|
|
319
|
+
if (logLevel === "debug") console.debug(PREFIX + chalk.bgBlue("DEBUG") + " " + message);
|
|
320
|
+
},
|
|
321
|
+
error: (message) => {
|
|
322
|
+
console.error(PREFIX + chalk.bgRed("ERROR") + " " + message);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
var logger_default = logger;
|
|
326
|
+
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/mojang/minecraftVersionManager.ts
|
|
329
|
+
const MOJANG_PATHS = {
|
|
330
|
+
manifest: "https://launchermeta.mojang.com/mc/game/version_manifest.json",
|
|
331
|
+
versionDetails: "version_details",
|
|
332
|
+
clientJars: "version_details",
|
|
333
|
+
langFiles: "lang_files"
|
|
334
|
+
};
|
|
335
|
+
const CACHE_EXPIRY_MS = 1e3 * 60 * 60 * 24 * 30;
|
|
336
|
+
var MinecraftVersionManager = class {
|
|
337
|
+
cacheDir;
|
|
338
|
+
assetsFetchingTasks = /* @__PURE__ */ new Map();
|
|
339
|
+
constructor(cacheDir) {
|
|
340
|
+
this.cacheDir = cacheDir;
|
|
341
|
+
this.ensureCacheDir();
|
|
342
|
+
}
|
|
343
|
+
ensureCacheDir() {
|
|
344
|
+
if (!existsSync$1(this.cacheDir)) mkdirSync$1(this.cacheDir, { recursive: true });
|
|
345
|
+
const detailsDir = join$1(this.cacheDir, MOJANG_PATHS.versionDetails);
|
|
346
|
+
if (!existsSync$1(detailsDir)) mkdirSync$1(detailsDir, { recursive: true });
|
|
347
|
+
const clientJarsDir = join$1(this.cacheDir, MOJANG_PATHS.clientJars);
|
|
348
|
+
if (!existsSync$1(clientJarsDir)) mkdirSync$1(clientJarsDir, { recursive: true });
|
|
349
|
+
}
|
|
350
|
+
getManifestCachePath() {
|
|
351
|
+
return join$1(this.cacheDir, "version_manifest.json");
|
|
352
|
+
}
|
|
353
|
+
isCacheExpired(filePath) {
|
|
354
|
+
try {
|
|
355
|
+
const stats = __require("fs").statSync(filePath);
|
|
356
|
+
return Date.now() - stats.mtimeMs > CACHE_EXPIRY_MS;
|
|
357
|
+
} catch {
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async getVersionManifest(forceRefresh = false) {
|
|
362
|
+
const cachePath = this.getManifestCachePath();
|
|
363
|
+
if (!forceRefresh && existsSync$1(cachePath) && !this.isCacheExpired(cachePath)) try {
|
|
364
|
+
const cachedData = readFileSync$1(cachePath, "utf-8");
|
|
365
|
+
return JSON.parse(cachedData);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
logger_default.warn(`Failed to read version manifest cache: ${error}`);
|
|
368
|
+
}
|
|
369
|
+
try {
|
|
370
|
+
logger_default.info("Fetching version manifest from Mojang...");
|
|
371
|
+
const response = await fetch(MOJANG_PATHS.manifest);
|
|
372
|
+
if (!response.ok) throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
373
|
+
const manifest = await response.json();
|
|
374
|
+
writeFileSync$1(cachePath, JSON.stringify(manifest, null, 2));
|
|
375
|
+
logger_default.info("Version manifest cached successfully");
|
|
376
|
+
return manifest;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger_default.error(`Failed to fetch version manifest: ${error}`);
|
|
379
|
+
if (existsSync$1(cachePath)) try {
|
|
380
|
+
const cachedData = readFileSync$1(cachePath, "utf-8");
|
|
381
|
+
logger_default.warn("Using stale cache due to fetch failure");
|
|
382
|
+
return JSON.parse(cachedData);
|
|
383
|
+
} catch {
|
|
384
|
+
throw new Error("Failed to fetch manifest and no valid cache available");
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async getVersionDetails(versionId, forceRefresh = false) {
|
|
390
|
+
const manifest = await this.getVersionManifest();
|
|
391
|
+
if (versionId === "latest") versionId = manifest.latest.release;
|
|
392
|
+
const versionInfo = manifest.versions.find((v) => v.id === versionId);
|
|
393
|
+
if (!versionInfo) throw new Error(`Version ${versionId} not found in manifest`);
|
|
394
|
+
const cachePath = join$1(this.cacheDir, MOJANG_PATHS.versionDetails, `${versionId}.json`);
|
|
395
|
+
if (!forceRefresh && existsSync$1(cachePath) && !this.isCacheExpired(cachePath)) try {
|
|
396
|
+
const cachedData = readFileSync$1(cachePath, "utf-8");
|
|
397
|
+
return JSON.parse(cachedData);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
logger_default.warn(`Failed to read version details cache: ${error}`);
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
logger_default.info(`Fetching details for version ${versionId}...`);
|
|
403
|
+
const response = await fetch(versionInfo.url);
|
|
404
|
+
if (!response.ok) throw new Error(`Failed to fetch version details: ${response.statusText}`);
|
|
405
|
+
const details = await response.json();
|
|
406
|
+
writeFileSync$1(cachePath, JSON.stringify(details, null, 2));
|
|
407
|
+
logger_default.info(`Version details for ${versionId} cached successfully`);
|
|
408
|
+
return details;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
logger_default.error(`Failed to fetch version details for ${versionId}: ${error}`);
|
|
411
|
+
if (existsSync$1(cachePath)) try {
|
|
412
|
+
const cachedData = readFileSync$1(cachePath, "utf-8");
|
|
413
|
+
logger_default.warn(`Using stale cache for ${versionId} due to fetch failure`);
|
|
414
|
+
return JSON.parse(cachedData);
|
|
415
|
+
} catch {
|
|
416
|
+
throw new Error(`Failed to fetch version details and no valid cache available for ${versionId}`);
|
|
417
|
+
}
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async getClientJar(versionId, forceRefresh = false) {
|
|
422
|
+
const versionDetails = await this.getVersionDetails(versionId);
|
|
423
|
+
const clientJarPath = join$1(this.cacheDir, MOJANG_PATHS.clientJars, `${versionDetails.id}.jar`);
|
|
424
|
+
if (!forceRefresh && existsSync$1(clientJarPath) && !this.isCacheExpired(clientJarPath)) return clientJarPath;
|
|
425
|
+
try {
|
|
426
|
+
logger_default.info(`Downloading client jar for version ${versionDetails.id}...`);
|
|
427
|
+
const clientJarUrl = versionDetails.downloads.client.url;
|
|
428
|
+
const response = await fetch(clientJarUrl);
|
|
429
|
+
if (!response.ok) throw new Error(`Failed to download client jar: ${response.statusText}`);
|
|
430
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
431
|
+
writeFileSync$1(clientJarPath, Buffer.from(arrayBuffer));
|
|
432
|
+
logger_default.info(`Client jar for version ${versionDetails.id} cached successfully`);
|
|
433
|
+
return clientJarPath;
|
|
434
|
+
} catch (error) {
|
|
435
|
+
logger_default.error(`Failed to download client jar for ${versionDetails.id}: ${error}`);
|
|
436
|
+
if (existsSync$1(clientJarPath)) {
|
|
437
|
+
logger_default.warn(`Using stale cache for ${versionDetails.id} due to download failure`);
|
|
438
|
+
return clientJarPath;
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async getAssets(versionId, forceRefresh = false) {
|
|
444
|
+
const taskKey = `${versionId}:${forceRefresh}`;
|
|
445
|
+
if (this.assetsFetchingTasks.has(taskKey)) return this.assetsFetchingTasks.get(taskKey);
|
|
446
|
+
const assetsPromise = (async () => {
|
|
447
|
+
const versionDetails = await this.getVersionDetails(versionId);
|
|
448
|
+
const assetsDirPath = join$1(this.cacheDir, MOJANG_PATHS.versionDetails, versionDetails.id);
|
|
449
|
+
if (!forceRefresh && existsSync$1(assetsDirPath) && !this.isCacheExpired(assetsDirPath)) return assetsDirPath;
|
|
450
|
+
try {
|
|
451
|
+
const jarPath = await this.getClientJar(versionId, forceRefresh);
|
|
452
|
+
const zip = new StreamZip.async({ file: jarPath });
|
|
453
|
+
if (existsSync$1(assetsDirPath)) rmSync$1(assetsDirPath, { recursive: true });
|
|
454
|
+
mkdirSync$1(assetsDirPath, { recursive: true });
|
|
455
|
+
logger_default.info(`Extracting assets for version ${versionDetails.id}...`);
|
|
456
|
+
const entries = await zip.entries();
|
|
457
|
+
for (const entry of Object.values(entries)) if (entry.name.startsWith("assets/minecraft")) {
|
|
458
|
+
const destPath = join$1(assetsDirPath, entry.name);
|
|
459
|
+
if (entry.isDirectory) mkdirSync$1(destPath, { recursive: true });
|
|
460
|
+
else {
|
|
461
|
+
mkdirSync$1(dirname(destPath), { recursive: true });
|
|
462
|
+
await zip.extract(entry.name, destPath);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
await zip.close();
|
|
466
|
+
logger_default.info(`Assets for version ${versionDetails.id} extracted successfully`);
|
|
467
|
+
return assetsDirPath;
|
|
468
|
+
} catch (error) {
|
|
469
|
+
logger_default.error(`Failed to extract assets for ${versionDetails.id}: ${error}`);
|
|
470
|
+
if (existsSync$1(assetsDirPath)) {
|
|
471
|
+
logger_default.warn(`Using stale assets cache for ${versionDetails.id} due to extraction failure`);
|
|
472
|
+
return assetsDirPath;
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
})().finally(() => {
|
|
477
|
+
this.assetsFetchingTasks.delete(taskKey);
|
|
478
|
+
});
|
|
479
|
+
this.assetsFetchingTasks.set(taskKey, assetsPromise);
|
|
480
|
+
return assetsPromise;
|
|
481
|
+
}
|
|
482
|
+
async getLangFile(versionId, lang) {
|
|
483
|
+
if (lang === "en_us") return join$1(await this.getAssets(versionId), `assets/minecraft/lang/en_us.json`);
|
|
484
|
+
const langFilePath = join$1(this.cacheDir, MOJANG_PATHS.langFiles, `${versionId}_${lang}.json`);
|
|
485
|
+
if (existsSync$1(langFilePath) && !this.isCacheExpired(langFilePath)) return langFilePath;
|
|
486
|
+
try {
|
|
487
|
+
logger_default.info(`Downloading language file: ${versionId}/${lang}`);
|
|
488
|
+
const assetIndexUrl = (await this.getVersionDetails(versionId)).assetIndex.url;
|
|
489
|
+
const assetIndexResponse = await fetch(assetIndexUrl);
|
|
490
|
+
if (!assetIndexResponse.ok) throw new Error(`Failed to fetch asset index: ${assetIndexResponse.statusText}`);
|
|
491
|
+
const assetIndex = await assetIndexResponse.json();
|
|
492
|
+
const langKey = `minecraft/lang/${lang}.json`;
|
|
493
|
+
const langObject = assetIndex.objects[langKey];
|
|
494
|
+
if (!langObject) throw new Error(`Language file not found in asset index: ${langKey}`);
|
|
495
|
+
const langHash = langObject.hash;
|
|
496
|
+
const langFileUrl = `https://resources.download.minecraft.net/${langHash.substring(0, 2)}/${langHash}`;
|
|
497
|
+
const langResponse = await fetch(langFileUrl);
|
|
498
|
+
if (!langResponse.ok) throw new Error(`Failed to download language file: ${langResponse.statusText}`);
|
|
499
|
+
const langDir = join$1(this.cacheDir, "lang_files");
|
|
500
|
+
if (!existsSync$1(langDir)) mkdirSync$1(langDir, { recursive: true });
|
|
501
|
+
writeFileSync$1(langFilePath, await langResponse.text());
|
|
502
|
+
logger_default.info(`Language file cached: ${versionId}/${lang}`);
|
|
503
|
+
return langFilePath;
|
|
504
|
+
} catch (error) {
|
|
505
|
+
logger_default.error(`Failed to download language file ${lang} for ${versionId}: ${error}`);
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async getLatestRelease() {
|
|
510
|
+
return (await this.getVersionManifest()).latest.release;
|
|
511
|
+
}
|
|
512
|
+
async getLatestSnapshot() {
|
|
513
|
+
return (await this.getVersionManifest()).latest.snapshot;
|
|
514
|
+
}
|
|
515
|
+
clearCache() {
|
|
516
|
+
const cachePath = this.getManifestCachePath();
|
|
517
|
+
if (existsSync$1(cachePath)) {
|
|
518
|
+
__require("fs").unlinkSync(cachePath);
|
|
519
|
+
logger_default.info("Version manifest cache cleared");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
function createVersionManager(cacheDir) {
|
|
524
|
+
return new MinecraftVersionManager(cacheDir);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/render/paths.ts
|
|
529
|
+
/**
|
|
530
|
+
* Minecraft リソースパックの標準ディレクトリ構造定義
|
|
531
|
+
*/
|
|
532
|
+
const MINECRAFT_PATHS = {
|
|
533
|
+
assets: "assets",
|
|
534
|
+
minecraft: "assets/minecraft",
|
|
535
|
+
models: "assets/minecraft/models",
|
|
536
|
+
modelBlocks: "assets/minecraft/models/block",
|
|
537
|
+
textures: "assets/minecraft/textures",
|
|
538
|
+
textureBlocks: "assets/minecraft/textures/block",
|
|
539
|
+
textureItems: "assets/minecraft/textures/item",
|
|
540
|
+
items: "assets/minecraft/items"
|
|
541
|
+
};
|
|
542
|
+
const TEXTURE_EXTENSIONS = [
|
|
543
|
+
".png",
|
|
544
|
+
".jpg",
|
|
545
|
+
".jpeg"
|
|
546
|
+
];
|
|
547
|
+
/**
|
|
548
|
+
* Minecraft パス正規化・解析のユーティリティ
|
|
549
|
+
*/
|
|
550
|
+
var MinecraftPathResolver = class {
|
|
551
|
+
constructor(resourcePackPath) {
|
|
552
|
+
this.resourcePackPath = resourcePackPath;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* モデルパスを正規化
|
|
556
|
+
* 例: minecraft:block/cube -> block/cube
|
|
557
|
+
* block/cube -> block/cube
|
|
558
|
+
* /path/to/block/cube.json -> block/cube
|
|
559
|
+
*/
|
|
560
|
+
normalizeModelPath(modelPath) {
|
|
561
|
+
let normalized = modelPath.replace(/^minecraft:/, "").replace(/\.json$/, "");
|
|
562
|
+
if (!normalized.startsWith("block/") && !normalized.startsWith("item/")) normalized = `block/${normalized}`;
|
|
563
|
+
return normalized;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* テクスチャ参照を正規化(相対パスのみ返す)
|
|
567
|
+
* 例: minecraft:block/stone -> block/stone.png
|
|
568
|
+
* stone -> block/stone.png
|
|
569
|
+
* item/apple -> item/apple.png
|
|
570
|
+
*/
|
|
571
|
+
normalizeTexturePath(texturePath) {
|
|
572
|
+
let normalized = texturePath.replace(/^minecraft:/, "").replace(/^textures\//, "");
|
|
573
|
+
if (!normalized.startsWith("block/") && !normalized.startsWith("item/")) normalized = `block/${normalized}`;
|
|
574
|
+
if (!TEXTURE_EXTENSIONS.some((ext) => normalized.endsWith(ext))) normalized += ".png";
|
|
575
|
+
return normalized;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* リソースパック内の完全なモデルファイルパスを取得
|
|
579
|
+
*/
|
|
580
|
+
getModelFilePath(modelPath) {
|
|
581
|
+
const normalized = this.normalizeModelPath(modelPath);
|
|
582
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.models, `${normalized}.json`);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* リソースパック内の完全なテクスチャファイルパスを取得
|
|
586
|
+
*/
|
|
587
|
+
getTextureFilePath(texturePath) {
|
|
588
|
+
const normalized = this.normalizeTexturePath(texturePath);
|
|
589
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.textures, normalized);
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* ブロックモデルディレクトリの完全パスを取得
|
|
593
|
+
*/
|
|
594
|
+
getBlockModelsDir() {
|
|
595
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.modelBlocks);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* アイテム定義ディレクトリの完全パスを取得
|
|
599
|
+
*/
|
|
600
|
+
getItemsDir() {
|
|
601
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.items);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* モデルの基本ディレクトリを取得
|
|
605
|
+
*/
|
|
606
|
+
getModelsBaseDir() {
|
|
607
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.models);
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* テクスチャの基本ディレクトリを取得
|
|
611
|
+
*/
|
|
612
|
+
getTexturesBaseDir() {
|
|
613
|
+
return join$1(this.resourcePackPath, MINECRAFT_PATHS.textures);
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/render/Renderer.ts
|
|
619
|
+
/**
|
|
620
|
+
* Minecraft のティントカラーマップ(ブロック別)
|
|
621
|
+
* tintindex に対応するRGB値を定義
|
|
622
|
+
*/
|
|
623
|
+
const TINT_COLORS = {
|
|
624
|
+
grass_block: { 0: [
|
|
625
|
+
127,
|
|
626
|
+
178,
|
|
627
|
+
56
|
|
628
|
+
] },
|
|
629
|
+
grass: { 0: [
|
|
630
|
+
127,
|
|
631
|
+
178,
|
|
632
|
+
56
|
|
633
|
+
] },
|
|
634
|
+
tall_grass: { 0: [
|
|
635
|
+
127,
|
|
636
|
+
178,
|
|
637
|
+
56
|
|
638
|
+
] },
|
|
639
|
+
seagrass: { 0: [
|
|
640
|
+
127,
|
|
641
|
+
178,
|
|
642
|
+
56
|
|
643
|
+
] },
|
|
644
|
+
vine: { 0: [
|
|
645
|
+
127,
|
|
646
|
+
178,
|
|
647
|
+
56
|
|
648
|
+
] },
|
|
649
|
+
oak_leaves: { 0: [
|
|
650
|
+
127,
|
|
651
|
+
178,
|
|
652
|
+
56
|
|
653
|
+
] },
|
|
654
|
+
birch_leaves: { 0: [
|
|
655
|
+
128,
|
|
656
|
+
168,
|
|
657
|
+
63
|
|
658
|
+
] },
|
|
659
|
+
spruce_leaves: { 0: [
|
|
660
|
+
95,
|
|
661
|
+
130,
|
|
662
|
+
60
|
|
663
|
+
] },
|
|
664
|
+
jungle_leaves: { 0: [
|
|
665
|
+
97,
|
|
666
|
+
163,
|
|
667
|
+
43
|
|
668
|
+
] },
|
|
669
|
+
acacia_leaves: { 0: [
|
|
670
|
+
155,
|
|
671
|
+
178,
|
|
672
|
+
33
|
|
673
|
+
] },
|
|
674
|
+
dark_oak_leaves: { 0: [
|
|
675
|
+
103,
|
|
676
|
+
117,
|
|
677
|
+
53
|
|
678
|
+
] },
|
|
679
|
+
cocoa: { 0: [
|
|
680
|
+
128,
|
|
681
|
+
92,
|
|
682
|
+
63
|
|
683
|
+
] },
|
|
684
|
+
cactus: { 0: [
|
|
685
|
+
95,
|
|
686
|
+
160,
|
|
687
|
+
54
|
|
688
|
+
] },
|
|
689
|
+
water: { 0: [
|
|
690
|
+
63,
|
|
691
|
+
127,
|
|
692
|
+
255
|
|
693
|
+
] },
|
|
694
|
+
water_cauldron: { 0: [
|
|
695
|
+
63,
|
|
696
|
+
127,
|
|
697
|
+
255
|
|
698
|
+
] },
|
|
699
|
+
potion: { 0: [
|
|
700
|
+
127,
|
|
701
|
+
178,
|
|
702
|
+
56
|
|
703
|
+
] },
|
|
704
|
+
tipped_arrow: { 0: [
|
|
705
|
+
127,
|
|
706
|
+
178,
|
|
707
|
+
56
|
|
708
|
+
] },
|
|
709
|
+
spawn_egg: { 0: [
|
|
710
|
+
78,
|
|
711
|
+
78,
|
|
712
|
+
78
|
|
713
|
+
] }
|
|
714
|
+
};
|
|
715
|
+
var MinecraftBlockRenderer = class {
|
|
716
|
+
modelsCache = /* @__PURE__ */ new Map();
|
|
717
|
+
texturesCache = /* @__PURE__ */ new Map();
|
|
718
|
+
tintedTexturesCache = /* @__PURE__ */ new Map();
|
|
719
|
+
resourcePackPathResolver;
|
|
720
|
+
modelPathResolver;
|
|
721
|
+
constructor(resourcePackPath, modelPath) {
|
|
722
|
+
this.resourcePackPathResolver = new MinecraftPathResolver(resourcePackPath);
|
|
723
|
+
this.modelPathResolver = new MinecraftPathResolver(modelPath ?? resourcePackPath);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* ブロック名からモデルパスを解決
|
|
727
|
+
*/
|
|
728
|
+
async resolveBlockModelPath(blockName) {
|
|
729
|
+
return `block/${blockName.replace(/^minecraft:/, "")}`;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* モデルファイルを読み込んで、parent継承を解決する
|
|
733
|
+
*/
|
|
734
|
+
async loadModel(modelPath) {
|
|
735
|
+
const fullPath = this.modelPathResolver.getModelFilePath(modelPath);
|
|
736
|
+
if (modelPath === "builtin/generated" || modelPath === "builtin/entity") return {};
|
|
737
|
+
if (this.modelsCache.has(fullPath)) return this.modelsCache.get(fullPath);
|
|
738
|
+
const content = await readFile(fullPath, "utf-8");
|
|
739
|
+
const model = JSON.parse(content);
|
|
740
|
+
if (model.parent) {
|
|
741
|
+
if (model.parent === "builtin/generated" || model.parent === "builtin/entity") {
|
|
742
|
+
this.modelsCache.set(fullPath, model);
|
|
743
|
+
return model;
|
|
744
|
+
}
|
|
745
|
+
const parentModel = await this.loadModel(model.parent);
|
|
746
|
+
const merged = {
|
|
747
|
+
...parentModel,
|
|
748
|
+
...model,
|
|
749
|
+
textures: {
|
|
750
|
+
...parentModel.textures,
|
|
751
|
+
...model.textures
|
|
752
|
+
},
|
|
753
|
+
elements: model.elements || parentModel.elements
|
|
754
|
+
};
|
|
755
|
+
this.modelsCache.set(fullPath, merged);
|
|
756
|
+
return merged;
|
|
757
|
+
}
|
|
758
|
+
this.modelsCache.set(fullPath, model);
|
|
759
|
+
return model;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* テクスチャ参照を解決(#texture_nameのような参照を実際のパスに変換)
|
|
763
|
+
*/
|
|
764
|
+
resolveTexture(texture, textures, visited = /* @__PURE__ */ new Set()) {
|
|
765
|
+
if (texture.startsWith("#")) {
|
|
766
|
+
const key = texture.slice(1);
|
|
767
|
+
if (visited.has(key)) return texture;
|
|
768
|
+
visited.add(key);
|
|
769
|
+
if (textures[key]) return this.resolveTexture(textures[key], textures, visited);
|
|
770
|
+
}
|
|
771
|
+
return this.resourcePackPathResolver.normalizeTexturePath(texture);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* テクスチャ画像を読み込む(上下面は水平反転、壁面は反転なし)
|
|
775
|
+
*/
|
|
776
|
+
async loadTexture(texturePath, faceName) {
|
|
777
|
+
const shouldFlipX = faceName === "up" || faceName === "down";
|
|
778
|
+
const cacheKey = shouldFlipX ? `${texturePath}:flipX` : texturePath;
|
|
779
|
+
if (this.texturesCache.has(cacheKey)) return this.texturesCache.get(cacheKey);
|
|
780
|
+
const fullPath = this.resourcePackPathResolver.getTextureFilePath(texturePath);
|
|
781
|
+
try {
|
|
782
|
+
const image = await loadImage(fullPath);
|
|
783
|
+
const canvas = createCanvas(image.width, image.height);
|
|
784
|
+
const ctx = canvas.getContext("2d");
|
|
785
|
+
if (shouldFlipX) {
|
|
786
|
+
ctx.scale(-1, 1);
|
|
787
|
+
ctx.drawImage(image, -image.width, 0);
|
|
788
|
+
} else ctx.drawImage(image, 0, 0);
|
|
789
|
+
this.texturesCache.set(cacheKey, canvas);
|
|
790
|
+
return canvas;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.warn(`Failed to load texture: ${fullPath}`);
|
|
793
|
+
const canvas = createCanvas(16, 16);
|
|
794
|
+
const ctx = canvas.getContext("2d");
|
|
795
|
+
ctx.fillStyle = "#f800f8";
|
|
796
|
+
ctx.fillRect(0, 0, 16, 16);
|
|
797
|
+
ctx.fillStyle = "#000000";
|
|
798
|
+
ctx.fillRect(0, 0, 8, 8);
|
|
799
|
+
ctx.fillRect(8, 8, 8, 8);
|
|
800
|
+
this.texturesCache.set(cacheKey, canvas);
|
|
801
|
+
return canvas;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 3D座標を2D画面座標に投影(アイソメトリック風)
|
|
806
|
+
*/
|
|
807
|
+
project(point, cosX, sinX, cosY, sinY, cosZ, sinZ, scale) {
|
|
808
|
+
let { x, y, z: z$1 } = point;
|
|
809
|
+
const x1 = x * cosY - z$1 * sinY;
|
|
810
|
+
const z1 = x * sinY + z$1 * cosY;
|
|
811
|
+
const y1 = y * cosX - z1 * sinX;
|
|
812
|
+
const z2 = y * sinX + z1 * cosX;
|
|
813
|
+
const x2 = x1 * cosZ - y1 * sinZ;
|
|
814
|
+
const y2 = x1 * sinZ + y1 * cosZ;
|
|
815
|
+
return {
|
|
816
|
+
x: x2 * scale,
|
|
817
|
+
y: y2 * scale,
|
|
818
|
+
z: z2
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* テクスチャ付きの四角形を描画する(4頂点パス)
|
|
823
|
+
* @param ctx Canvas 2D コンテキスト
|
|
824
|
+
* @param centerX キャンバス中心X
|
|
825
|
+
* @param centerY キャンバス中心Y
|
|
826
|
+
* @param texture ロード済みのImageオブジェクト
|
|
827
|
+
* @param vertices 投影済みの2D頂点 4個 (z含む)
|
|
828
|
+
* @param uvCoords UV座標 4個 (0-16)
|
|
829
|
+
* @param passes テクスチャ貼り付けのパス数(1 or 2)
|
|
830
|
+
*/
|
|
831
|
+
drawTexturedQuad(ctx, centerX, centerY, texture, vertices, uvCoords, passes = 1) {
|
|
832
|
+
if (vertices.length !== 4 || uvCoords.length !== 4) return;
|
|
833
|
+
ctx.save();
|
|
834
|
+
const offset = .3;
|
|
835
|
+
const quadCenterX = vertices.reduce((sum, p) => sum + p.x, 0) / 4;
|
|
836
|
+
const quadCenterY = vertices.reduce((sum, p) => sum + p.y, 0) / 4;
|
|
837
|
+
const expand = (p) => {
|
|
838
|
+
const dx = p.x - quadCenterX;
|
|
839
|
+
const dy = p.y - quadCenterY;
|
|
840
|
+
const mag = Math.sqrt(dx * dx + dy * dy);
|
|
841
|
+
return {
|
|
842
|
+
x: p.x + dx / mag * offset,
|
|
843
|
+
y: p.y + dy / mag * offset * 3
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
const expandedVertices = vertices.map(expand);
|
|
847
|
+
ctx.beginPath();
|
|
848
|
+
ctx.moveTo(centerX + expandedVertices[0].x, centerY - expandedVertices[0].y);
|
|
849
|
+
ctx.lineTo(centerX + expandedVertices[1].x, centerY - expandedVertices[1].y);
|
|
850
|
+
ctx.lineTo(centerX + expandedVertices[2].x, centerY - expandedVertices[2].y);
|
|
851
|
+
ctx.lineTo(centerX + expandedVertices[3].x, centerY - expandedVertices[3].y);
|
|
852
|
+
ctx.closePath();
|
|
853
|
+
ctx.clip();
|
|
854
|
+
const p0 = vertices[0], p1 = vertices[1], p2 = vertices[2];
|
|
855
|
+
const u0 = uvCoords[0].u, v0 = uvCoords[0].v;
|
|
856
|
+
const u1 = uvCoords[1].u, v1 = uvCoords[1].v;
|
|
857
|
+
const u2 = uvCoords[2].u, v2 = uvCoords[2].v;
|
|
858
|
+
const delta = (u1 - u0) * (v2 - v0) - (u2 - u0) * (v1 - v0);
|
|
859
|
+
if (Math.abs(delta) > 1e-4) {
|
|
860
|
+
const m11 = ((p1.x - p0.x) * (v2 - v0) - (p2.x - p0.x) * (v1 - v0)) / delta;
|
|
861
|
+
const m12 = -((p1.y - p0.y) * (v2 - v0) - (p2.y - p0.y) * (v1 - v0)) / delta;
|
|
862
|
+
const m21 = ((p2.x - p0.x) * (u1 - u0) - (p1.x - p0.x) * (u2 - u0)) / delta;
|
|
863
|
+
const m22 = -((p2.y - p0.y) * (u1 - u0) - (p1.y - p0.y) * (u2 - u0)) / delta;
|
|
864
|
+
const dx = centerX + p0.x - (m11 * u0 + m21 * v0);
|
|
865
|
+
const dy = centerY - p0.y - (m12 * u0 + m22 * v0);
|
|
866
|
+
ctx.setTransform(m11, m12, m21, m22, dx, dy);
|
|
867
|
+
ctx.drawImage(texture, 0, 0);
|
|
868
|
+
if (passes === 2) {
|
|
869
|
+
const p3 = vertices[3];
|
|
870
|
+
const u3 = uvCoords[3].u, v3 = uvCoords[3].v;
|
|
871
|
+
const delta2 = (u2 - u0) * (v3 - v0) - (u3 - u0) * (v2 - v0);
|
|
872
|
+
if (Math.abs(delta2) > 1e-4) {
|
|
873
|
+
const m11_2 = ((p2.x - p0.x) * (v3 - v0) - (p3.x - p0.x) * (v2 - v0)) / delta2;
|
|
874
|
+
const m12_2 = -((p2.y - p0.y) * (v3 - v0) - (p3.y - p0.y) * (v2 - v0)) / delta2;
|
|
875
|
+
const m21_2 = ((p3.x - p0.x) * (u2 - u0) - (p2.x - p0.x) * (u3 - u0)) / delta2;
|
|
876
|
+
const m22_2 = -((p3.y - p0.y) * (u2 - u0) - (p2.y - p0.y) * (u3 - u0)) / delta2;
|
|
877
|
+
const dx2 = centerX + p0.x - (m11_2 * u0 + m21_2 * v0);
|
|
878
|
+
const dy2 = centerY - p0.y - (m12_2 * u0 + m22_2 * v0);
|
|
879
|
+
ctx.setTransform(m11_2, m12_2, m21_2, m22_2, dx2, dy2);
|
|
880
|
+
ctx.drawImage(texture, 0, 0);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
ctx.restore();
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* テクスチャにティント色を適用(透明部分は保護)
|
|
888
|
+
*/
|
|
889
|
+
async applyTint(texture, tintColor) {
|
|
890
|
+
const [r, g, b] = tintColor;
|
|
891
|
+
const tintedCanvas = createCanvas(texture.width, texture.height);
|
|
892
|
+
const tintCtx = tintedCanvas.getContext("2d");
|
|
893
|
+
tintCtx.fillStyle = `rgb(${r},${g},${b})`;
|
|
894
|
+
tintCtx.fillRect(0, 0, texture.width, texture.height);
|
|
895
|
+
tintCtx.globalCompositeOperation = "multiply";
|
|
896
|
+
tintCtx.drawImage(texture, 0, 0);
|
|
897
|
+
const finalCanvas = createCanvas(texture.width, texture.height);
|
|
898
|
+
const finalCtx = finalCanvas.getContext("2d");
|
|
899
|
+
finalCtx.drawImage(texture, 0, 0);
|
|
900
|
+
finalCtx.globalCompositeOperation = "source-in";
|
|
901
|
+
finalCtx.drawImage(tintedCanvas, 0, 0);
|
|
902
|
+
return finalCanvas;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* モデル名からティントカラーを取得
|
|
906
|
+
* 未知のブロックの場合はデフォルト緑色を返す
|
|
907
|
+
*/
|
|
908
|
+
getTintColor(modelPath, tintindex) {
|
|
909
|
+
if (tintindex === void 0) return null;
|
|
910
|
+
const nameParts = modelPath.split("/");
|
|
911
|
+
const type$1 = nameParts[0];
|
|
912
|
+
const baseName = nameParts.pop()?.replace(/\.json$/, "");
|
|
913
|
+
if (!baseName) return null;
|
|
914
|
+
const colorMap = TINT_COLORS[baseName];
|
|
915
|
+
if (!colorMap) {
|
|
916
|
+
logger_default.debug(`[Tint] Unknown ${type$1} "${baseName}" with tintindex ${tintindex}, using default green tint`);
|
|
917
|
+
return [
|
|
918
|
+
127,
|
|
919
|
+
178,
|
|
920
|
+
56
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
return colorMap[tintindex] || null;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* ブロックモデルをレンダリング
|
|
927
|
+
*/
|
|
928
|
+
async renderBlock(modelPath, outputPath, options) {
|
|
929
|
+
const { width, height, rotation = CONFIG.ROTATION } = options;
|
|
930
|
+
const scale = options.scale ?? Math.round((height > width ? width : height) / 25.6);
|
|
931
|
+
let resolvedModelPath = modelPath;
|
|
932
|
+
if (modelPath.startsWith("block/")) {
|
|
933
|
+
const blockName = modelPath.replace(/^block\//, "");
|
|
934
|
+
const resolvedPath = await this.resolveBlockModelPath(blockName);
|
|
935
|
+
if (resolvedPath && resolvedPath !== `block/${blockName}`) resolvedModelPath = resolvedPath;
|
|
936
|
+
}
|
|
937
|
+
const normalizedModelPath = this.modelPathResolver.normalizeModelPath(resolvedModelPath);
|
|
938
|
+
const model = await this.loadModel(normalizedModelPath);
|
|
939
|
+
if (!model.elements) throw new Error("Model has no elements to render");
|
|
940
|
+
const canvas = createCanvas(width, height);
|
|
941
|
+
const ctx = canvas.getContext("2d");
|
|
942
|
+
ctx.imageSmoothingEnabled = false;
|
|
943
|
+
ctx.clearRect(0, 0, width, height);
|
|
944
|
+
const centerX = width / 2;
|
|
945
|
+
const centerY = height / 2;
|
|
946
|
+
const [rotX, rotY, rotZ] = rotation.map((deg) => deg * Math.PI / 180);
|
|
947
|
+
const cosX = Math.cos(rotX), sinX = Math.sin(rotX);
|
|
948
|
+
const cosY = Math.cos(rotY), sinY = Math.sin(rotY);
|
|
949
|
+
const cosZ = Math.cos(rotZ), sinZ = Math.sin(rotZ);
|
|
950
|
+
const allFacesToRender = [];
|
|
951
|
+
for (const element of model.elements) for (const faceName of [
|
|
952
|
+
"down",
|
|
953
|
+
"up",
|
|
954
|
+
"north",
|
|
955
|
+
"south",
|
|
956
|
+
"west",
|
|
957
|
+
"east"
|
|
958
|
+
]) {
|
|
959
|
+
const face = element.faces[faceName];
|
|
960
|
+
if (!face) continue;
|
|
961
|
+
const from = element.from.map((v) => v - 8);
|
|
962
|
+
const to = element.to.map((v) => v - 8);
|
|
963
|
+
let vertices = [];
|
|
964
|
+
switch (faceName) {
|
|
965
|
+
case "up":
|
|
966
|
+
vertices = [
|
|
967
|
+
{
|
|
968
|
+
x: from[0],
|
|
969
|
+
y: to[1],
|
|
970
|
+
z: to[2]
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
x: to[0],
|
|
974
|
+
y: to[1],
|
|
975
|
+
z: to[2]
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
x: to[0],
|
|
979
|
+
y: to[1],
|
|
980
|
+
z: from[2]
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
x: from[0],
|
|
984
|
+
y: to[1],
|
|
985
|
+
z: from[2]
|
|
986
|
+
}
|
|
987
|
+
];
|
|
988
|
+
break;
|
|
989
|
+
case "down":
|
|
990
|
+
vertices = [
|
|
991
|
+
{
|
|
992
|
+
x: from[0],
|
|
993
|
+
y: from[1],
|
|
994
|
+
z: from[2]
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
x: to[0],
|
|
998
|
+
y: from[1],
|
|
999
|
+
z: from[2]
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
x: to[0],
|
|
1003
|
+
y: from[1],
|
|
1004
|
+
z: to[2]
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
x: from[0],
|
|
1008
|
+
y: from[1],
|
|
1009
|
+
z: to[2]
|
|
1010
|
+
}
|
|
1011
|
+
];
|
|
1012
|
+
break;
|
|
1013
|
+
case "north":
|
|
1014
|
+
vertices = [
|
|
1015
|
+
{
|
|
1016
|
+
x: to[0],
|
|
1017
|
+
y: from[1],
|
|
1018
|
+
z: from[2]
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
x: from[0],
|
|
1022
|
+
y: from[1],
|
|
1023
|
+
z: from[2]
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
x: from[0],
|
|
1027
|
+
y: to[1],
|
|
1028
|
+
z: from[2]
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
x: to[0],
|
|
1032
|
+
y: to[1],
|
|
1033
|
+
z: from[2]
|
|
1034
|
+
}
|
|
1035
|
+
];
|
|
1036
|
+
break;
|
|
1037
|
+
case "south":
|
|
1038
|
+
vertices = [
|
|
1039
|
+
{
|
|
1040
|
+
x: from[0],
|
|
1041
|
+
y: from[1],
|
|
1042
|
+
z: to[2]
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
x: to[0],
|
|
1046
|
+
y: from[1],
|
|
1047
|
+
z: to[2]
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
x: to[0],
|
|
1051
|
+
y: to[1],
|
|
1052
|
+
z: to[2]
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
x: from[0],
|
|
1056
|
+
y: to[1],
|
|
1057
|
+
z: to[2]
|
|
1058
|
+
}
|
|
1059
|
+
];
|
|
1060
|
+
break;
|
|
1061
|
+
case "west":
|
|
1062
|
+
vertices = [
|
|
1063
|
+
{
|
|
1064
|
+
x: from[0],
|
|
1065
|
+
y: from[1],
|
|
1066
|
+
z: from[2]
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
x: from[0],
|
|
1070
|
+
y: from[1],
|
|
1071
|
+
z: to[2]
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
x: from[0],
|
|
1075
|
+
y: to[1],
|
|
1076
|
+
z: to[2]
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
x: from[0],
|
|
1080
|
+
y: to[1],
|
|
1081
|
+
z: from[2]
|
|
1082
|
+
}
|
|
1083
|
+
];
|
|
1084
|
+
break;
|
|
1085
|
+
case "east":
|
|
1086
|
+
vertices = [
|
|
1087
|
+
{
|
|
1088
|
+
x: to[0],
|
|
1089
|
+
y: from[1],
|
|
1090
|
+
z: to[2]
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
x: to[0],
|
|
1094
|
+
y: from[1],
|
|
1095
|
+
z: from[2]
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
x: to[0],
|
|
1099
|
+
y: to[1],
|
|
1100
|
+
z: from[2]
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
x: to[0],
|
|
1104
|
+
y: to[1],
|
|
1105
|
+
z: to[2]
|
|
1106
|
+
}
|
|
1107
|
+
];
|
|
1108
|
+
break;
|
|
1109
|
+
}
|
|
1110
|
+
const projected = vertices.map((v) => this.project(v, cosX, sinX, cosY, sinY, cosZ, sinZ, scale));
|
|
1111
|
+
const v0 = projected[0], v1 = projected[1], v2 = projected[2];
|
|
1112
|
+
if ((v1.x - v0.x) * (v2.y - v0.y) - (v1.y - v0.y) * (v2.x - v0.x) >= 0) continue;
|
|
1113
|
+
const avgZ = projected.reduce((sum, p) => sum + p.z, 0) / 4;
|
|
1114
|
+
const minZ = Math.min(...projected.map((p) => p.z));
|
|
1115
|
+
allFacesToRender.push({
|
|
1116
|
+
faceName,
|
|
1117
|
+
face,
|
|
1118
|
+
projected,
|
|
1119
|
+
avgZ,
|
|
1120
|
+
minZ
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
allFacesToRender.sort((a, b) => b.avgZ - a.avgZ);
|
|
1124
|
+
for (const renderData of allFacesToRender) {
|
|
1125
|
+
const { faceName, face, projected } = renderData;
|
|
1126
|
+
const texturePath = this.resolveTexture(face.texture, model.textures || {});
|
|
1127
|
+
let texture = await this.loadTexture(texturePath, faceName);
|
|
1128
|
+
if (face.tintindex !== void 0) {
|
|
1129
|
+
const tintColor = this.getTintColor(normalizedModelPath, face.tintindex);
|
|
1130
|
+
if (tintColor) {
|
|
1131
|
+
const tintCacheKey = `${texturePath}:${faceName}:${tintColor.join(",")}`;
|
|
1132
|
+
if (this.tintedTexturesCache.has(tintCacheKey)) texture = this.tintedTexturesCache.get(tintCacheKey);
|
|
1133
|
+
else {
|
|
1134
|
+
texture = await this.applyTint(texture, tintColor);
|
|
1135
|
+
this.tintedTexturesCache.set(tintCacheKey, texture);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
ctx.save();
|
|
1140
|
+
const uv = face.uv || [
|
|
1141
|
+
0,
|
|
1142
|
+
0,
|
|
1143
|
+
16,
|
|
1144
|
+
16
|
|
1145
|
+
];
|
|
1146
|
+
const uMin = uv[0], vMin = uv[1];
|
|
1147
|
+
const uMax = uv[2], vMax = uv[3];
|
|
1148
|
+
let uvCoords = [];
|
|
1149
|
+
const rotateUVCoords = (coords, rotation$1) => {
|
|
1150
|
+
const steps = ((rotation$1 || 0) % 360 + 360) % 360 / 90;
|
|
1151
|
+
let result = [...coords];
|
|
1152
|
+
for (let i = 0; i < steps; i++) result = [
|
|
1153
|
+
result[3],
|
|
1154
|
+
result[0],
|
|
1155
|
+
result[1],
|
|
1156
|
+
result[2]
|
|
1157
|
+
];
|
|
1158
|
+
return result;
|
|
1159
|
+
};
|
|
1160
|
+
switch (faceName) {
|
|
1161
|
+
case "up":
|
|
1162
|
+
case "down":
|
|
1163
|
+
uvCoords = [
|
|
1164
|
+
{
|
|
1165
|
+
u: uMin,
|
|
1166
|
+
v: vMax
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
u: uMax,
|
|
1170
|
+
v: vMax
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
u: uMax,
|
|
1174
|
+
v: vMin
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
u: uMin,
|
|
1178
|
+
v: vMin
|
|
1179
|
+
}
|
|
1180
|
+
];
|
|
1181
|
+
break;
|
|
1182
|
+
default:
|
|
1183
|
+
uvCoords = [
|
|
1184
|
+
{
|
|
1185
|
+
u: uMax,
|
|
1186
|
+
v: vMax
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
u: uMin,
|
|
1190
|
+
v: vMax
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
u: uMin,
|
|
1194
|
+
v: vMin
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
u: uMax,
|
|
1198
|
+
v: vMin
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1203
|
+
uvCoords = rotateUVCoords(uvCoords, face.rotation);
|
|
1204
|
+
this.drawTexturedQuad(ctx, centerX, centerY, texture, [
|
|
1205
|
+
{
|
|
1206
|
+
x: projected[0].x,
|
|
1207
|
+
y: projected[0].y
|
|
1208
|
+
},
|
|
1209
|
+
{
|
|
1210
|
+
x: projected[1].x,
|
|
1211
|
+
y: projected[1].y
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
x: projected[2].x,
|
|
1215
|
+
y: projected[2].y
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
x: projected[3].x,
|
|
1219
|
+
y: projected[3].y
|
|
1220
|
+
}
|
|
1221
|
+
], uvCoords, 2);
|
|
1222
|
+
ctx.save();
|
|
1223
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
1224
|
+
ctx.beginPath();
|
|
1225
|
+
ctx.moveTo(centerX + projected[0].x, centerY - projected[0].y);
|
|
1226
|
+
ctx.lineTo(centerX + projected[1].x, centerY - projected[1].y);
|
|
1227
|
+
ctx.lineTo(centerX + projected[2].x, centerY - projected[2].y);
|
|
1228
|
+
ctx.lineTo(centerX + projected[3].x, centerY - projected[3].y);
|
|
1229
|
+
ctx.closePath();
|
|
1230
|
+
let shade = 1;
|
|
1231
|
+
switch (faceName) {
|
|
1232
|
+
case "up":
|
|
1233
|
+
shade = 1;
|
|
1234
|
+
break;
|
|
1235
|
+
case "down":
|
|
1236
|
+
shade = .5;
|
|
1237
|
+
break;
|
|
1238
|
+
case "north":
|
|
1239
|
+
case "south":
|
|
1240
|
+
shade = .4;
|
|
1241
|
+
break;
|
|
1242
|
+
case "west":
|
|
1243
|
+
case "east":
|
|
1244
|
+
shade = .7;
|
|
1245
|
+
break;
|
|
1246
|
+
}
|
|
1247
|
+
const shadowColor = `rgba(0, 0, 0, ${1 - shade})`;
|
|
1248
|
+
ctx.globalCompositeOperation = "source-atop";
|
|
1249
|
+
ctx.fillStyle = shadowColor;
|
|
1250
|
+
ctx.fill();
|
|
1251
|
+
ctx.strokeStyle = shadowColor;
|
|
1252
|
+
ctx.lineWidth = .4;
|
|
1253
|
+
ctx.lineJoin = "round";
|
|
1254
|
+
ctx.stroke();
|
|
1255
|
+
ctx.restore();
|
|
1256
|
+
}
|
|
1257
|
+
const buffer = canvas.toBuffer("image/png");
|
|
1258
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1259
|
+
await sharp(buffer).png().toFile(outputPath);
|
|
1260
|
+
return outputPath;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* アイテムモデルをレンダリング
|
|
1264
|
+
*/
|
|
1265
|
+
async renderItem(modelId, outputPath, options) {
|
|
1266
|
+
const { width = CONFIG.TEXTURE_SIZE, height = CONFIG.TEXTURE_SIZE, scale = CONFIG.TEXTURE_SIZE } = options ?? {};
|
|
1267
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1268
|
+
const model = await this.loadModel(modelId);
|
|
1269
|
+
if (!model) throw new Error(`Item model not found: ${modelId}`);
|
|
1270
|
+
const resolvedScale = scale / 16;
|
|
1271
|
+
const textures = [];
|
|
1272
|
+
let currentModel = model;
|
|
1273
|
+
let depth = 0;
|
|
1274
|
+
while (currentModel && depth < 10) {
|
|
1275
|
+
if (currentModel.textures) {
|
|
1276
|
+
for (const key in currentModel.textures) if (key.startsWith("layer")) textures.push({
|
|
1277
|
+
layer: currentModel.textures[key].replace("minecraft:", ""),
|
|
1278
|
+
tintindex: currentModel.overrides?.[0]?.predicate?.custom_model_data !== void 0 ? 0 : void 0
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
if (currentModel.parent) {
|
|
1282
|
+
const parentId = currentModel.parent.replace("minecraft:", "");
|
|
1283
|
+
if (parentId === "builtin/entity") currentModel = null;
|
|
1284
|
+
else currentModel = await this.loadModel(parentId);
|
|
1285
|
+
} else currentModel = null;
|
|
1286
|
+
depth++;
|
|
1287
|
+
}
|
|
1288
|
+
if (textures.length === 0) throw new Error(`No textures found for item model: ${modelId}`);
|
|
1289
|
+
let composedImage;
|
|
1290
|
+
for (const { layer, tintindex } of textures) {
|
|
1291
|
+
const texturePath = `textures/${layer}.png`;
|
|
1292
|
+
let textureCanvas = await this.loadTexture(texturePath);
|
|
1293
|
+
const tintColor = TINT_COLORS[modelId.replace("item/", "")]?.[tintindex ?? 0] || TINT_COLORS[modelId]?.[tintindex ?? 0];
|
|
1294
|
+
if (tintColor) {
|
|
1295
|
+
const tintCacheKey = `${texturePath}:item:${tintColor.join(",")}`;
|
|
1296
|
+
if (this.tintedTexturesCache.has(tintCacheKey)) textureCanvas = this.tintedTexturesCache.get(tintCacheKey);
|
|
1297
|
+
else {
|
|
1298
|
+
textureCanvas = await this.applyTint(textureCanvas, tintColor);
|
|
1299
|
+
this.tintedTexturesCache.set(tintCacheKey, textureCanvas);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
let sharpImage = sharp(textureCanvas.toBuffer("image/png"));
|
|
1303
|
+
if (resolvedScale !== 1) sharpImage = sharpImage.resize(Math.round(16 * resolvedScale), Math.round(16 * resolvedScale), { kernel: sharp.kernel.nearest });
|
|
1304
|
+
const processedBuffer = await sharpImage.toBuffer();
|
|
1305
|
+
if (!composedImage) composedImage = sharp(processedBuffer);
|
|
1306
|
+
else composedImage = composedImage.composite([{ input: processedBuffer }]);
|
|
1307
|
+
}
|
|
1308
|
+
if (!composedImage) throw new Error(`Failed to compose image for item model: ${modelId}`);
|
|
1309
|
+
writeFileSync$1(outputPath, await composedImage.resize(width, height, {
|
|
1310
|
+
fit: "contain",
|
|
1311
|
+
background: {
|
|
1312
|
+
r: 0,
|
|
1313
|
+
g: 0,
|
|
1314
|
+
b: 0,
|
|
1315
|
+
alpha: 0
|
|
1316
|
+
}
|
|
1317
|
+
}).toBuffer());
|
|
1318
|
+
return outputPath;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* modelsディレクトリ内のすべてのモデルをレンダリング
|
|
1322
|
+
*/
|
|
1323
|
+
async renderAllModels(outputDir, options) {
|
|
1324
|
+
const files = await readdir(this.modelPathResolver.getBlockModelsDir());
|
|
1325
|
+
for (const file of files) {
|
|
1326
|
+
if (!file.endsWith(".json")) continue;
|
|
1327
|
+
const modelPath = file.replace(".json", "");
|
|
1328
|
+
const outputPath = dirname(outputDir) + "/" + modelPath.split("/").pop() + ".png";
|
|
1329
|
+
console.log(`Rendering ${file}...`);
|
|
1330
|
+
try {
|
|
1331
|
+
await this.renderBlock(modelPath, outputPath, options);
|
|
1332
|
+
console.log(`✓ Saved to ${outputPath}`);
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
console.error(`✗ Failed to render ${file}:`, error);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
//#endregion
|
|
1341
|
+
//#region src/mojang/itemManager.ts
|
|
1342
|
+
var ItemManager = class {
|
|
1343
|
+
resourcePackPath;
|
|
1344
|
+
versionManager;
|
|
1345
|
+
items3dCache = /* @__PURE__ */ new Map();
|
|
1346
|
+
itemIdsCache = /* @__PURE__ */ new Map();
|
|
1347
|
+
displayTypeCache = /* @__PURE__ */ new Map();
|
|
1348
|
+
constructor(resourcePackPath, versionManager) {
|
|
1349
|
+
this.resourcePackPath = resourcePackPath;
|
|
1350
|
+
this.versionManager = versionManager;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* en_us.json からアイテムID一覧を取得
|
|
1354
|
+
*/
|
|
1355
|
+
async getItemIds(versionId) {
|
|
1356
|
+
if (this.itemIdsCache.has(versionId)) return this.itemIdsCache.get(versionId);
|
|
1357
|
+
try {
|
|
1358
|
+
const langData = await this.getLangFile(versionId, "en_us");
|
|
1359
|
+
const itemIds = [];
|
|
1360
|
+
for (const key in langData) {
|
|
1361
|
+
const parts = key.split(".");
|
|
1362
|
+
if (parts.length === 3) {
|
|
1363
|
+
const [category, namespace, id] = parts;
|
|
1364
|
+
if ([
|
|
1365
|
+
"item",
|
|
1366
|
+
"block",
|
|
1367
|
+
"entity"
|
|
1368
|
+
].includes(category) && namespace === "minecraft") {
|
|
1369
|
+
const fullId = `minecraft:${id}`;
|
|
1370
|
+
itemIds.push(fullId);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
logger_default.debug(`Extracted ${itemIds.length} item IDs from ${versionId}`);
|
|
1375
|
+
this.itemIdsCache.set(versionId, itemIds);
|
|
1376
|
+
return itemIds;
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
logger_default.error(`Failed to get item IDs for ${versionId}: ${error}`);
|
|
1379
|
+
throw error;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* アイテムIDからテクスチャパスを取得
|
|
1384
|
+
*/
|
|
1385
|
+
async getItemTexturePath(versionId, itemId) {
|
|
1386
|
+
const [namespace, id] = itemId.includes(":") ? itemId.split(":") : ["minecraft", itemId];
|
|
1387
|
+
if (namespace !== "minecraft") throw new Error(`Invalid item ID: ${itemId}`);
|
|
1388
|
+
const assetsDir = await this.versionManager.getAssets(versionId);
|
|
1389
|
+
let currentModelPath = join$1(assetsDir, "assets", "minecraft", "models", "item", `${id}.json`);
|
|
1390
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1391
|
+
while (existsSync$1(currentModelPath) && !visited.has(currentModelPath)) {
|
|
1392
|
+
visited.add(currentModelPath);
|
|
1393
|
+
const content = await readFile(currentModelPath, "utf8");
|
|
1394
|
+
const model = JSON.parse(content);
|
|
1395
|
+
if (model.textures && model.textures.layer0) {
|
|
1396
|
+
let textureId = model.textures.layer0;
|
|
1397
|
+
const [, texPath] = textureId.includes(":") ? textureId.split(":") : ["minecraft", textureId];
|
|
1398
|
+
return join$1(this.resourcePackPath, "assets", "minecraft", "textures", `${texPath}.png`);
|
|
1399
|
+
}
|
|
1400
|
+
if (model.parent) {
|
|
1401
|
+
const [, parentPath] = model.parent.split(":");
|
|
1402
|
+
currentModelPath = join$1(assetsDir, "assets", "minecraft", "models", parentPath.startsWith("block/") ? "block" : "item", `${parentPath.replace(/^(block|item)\//, "")}.json`);
|
|
1403
|
+
} else break;
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* アイテムがティントを必要とするか判定
|
|
1409
|
+
*/
|
|
1410
|
+
async needsTint(versionId, itemId) {
|
|
1411
|
+
const [namespace, id] = itemId.includes(":") ? itemId.split(":") : ["minecraft", itemId];
|
|
1412
|
+
const cleanId = id.replace(/^item\//, "").replace(/^block\//, "");
|
|
1413
|
+
if (!!TINT_COLORS[cleanId]) return true;
|
|
1414
|
+
const modelPath = join$1(await this.versionManager.getAssets(versionId), "assets", "minecraft", "models", "item", `${cleanId}.json`);
|
|
1415
|
+
if (existsSync$1(modelPath)) try {
|
|
1416
|
+
const content = await readFile(modelPath, "utf8");
|
|
1417
|
+
const model = JSON.parse(content);
|
|
1418
|
+
if (model.overrides && model.overrides.length > 0) return true;
|
|
1419
|
+
} catch {}
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* 指定言語でアイテムの表示名を取得
|
|
1424
|
+
*/
|
|
1425
|
+
async getItemLabel(versionId, itemId, lang = "en_us") {
|
|
1426
|
+
try {
|
|
1427
|
+
const langData = await this.getLangFile(versionId, lang);
|
|
1428
|
+
const labelKey = `.minecraft.${itemId.replace("minecraft:", "")}`;
|
|
1429
|
+
return langData["item" + labelKey] || langData["block" + labelKey] || langData["entity" + labelKey] || itemId;
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
logger_default.warn(`Failed to get label for ${itemId} in ${lang}: ${error}`);
|
|
1432
|
+
return itemId;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* 言語ファイルを取得(en_us はアセットから、他言語はAsset Indexから)
|
|
1437
|
+
*/
|
|
1438
|
+
async getLangFile(versionId, lang = "en_us") {
|
|
1439
|
+
try {
|
|
1440
|
+
logger_default.debug(`Loading language file: ${versionId}/${lang}`);
|
|
1441
|
+
const langFilePath = await this.versionManager.getLangFile(versionId, lang);
|
|
1442
|
+
if (!existsSync$1(langFilePath)) throw new Error(`Language file not found: ${lang}.json`);
|
|
1443
|
+
const langData = JSON.parse(readFileSync$1(langFilePath, "utf-8"));
|
|
1444
|
+
logger_default.debug(`Language file loaded: ${versionId}/${lang}`);
|
|
1445
|
+
return langData;
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
logger_default.error(`Failed to load language file ${lang} for ${versionId}: ${error}`);
|
|
1448
|
+
throw error;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* 複数の言語でアイテムラベルを取得
|
|
1453
|
+
*/
|
|
1454
|
+
async getItemLabelsByLangs(versionId, itemId, langs = ["en_us"]) {
|
|
1455
|
+
const result = {};
|
|
1456
|
+
for (const lang of langs) try {
|
|
1457
|
+
result[lang] = await this.getItemLabel(versionId, itemId, lang);
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
logger_default.warn(`Failed to get label for ${itemId} in ${lang}`);
|
|
1460
|
+
result[lang] = itemId;
|
|
1461
|
+
}
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
async isItem2DModel(modelId, assetsDir) {
|
|
1465
|
+
return await this.getModelDisplayType(modelId, assetsDir) === "2d";
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* モデルの表示タイプを判定 ('2d' または '3d')
|
|
1469
|
+
*/
|
|
1470
|
+
async getModelDisplayType(modelId, assetsDir) {
|
|
1471
|
+
const cacheKey = `${assetsDir}:${modelId}`;
|
|
1472
|
+
if (this.displayTypeCache.has(cacheKey)) return this.displayTypeCache.get(cacheKey);
|
|
1473
|
+
const BASE_PATH = join$1(assetsDir, "assets", "minecraft", "models");
|
|
1474
|
+
let [, rawPath] = modelId.includes(":") ? modelId.split(":") : ["minecraft", modelId];
|
|
1475
|
+
let modelPath = rawPath.startsWith("item/") || rawPath.startsWith("block/") ? rawPath : `item/${rawPath}`;
|
|
1476
|
+
let depth = 0;
|
|
1477
|
+
let currentPath = join$1(BASE_PATH, `${modelPath}.json`);
|
|
1478
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1479
|
+
while (existsSync$1(currentPath) && depth < 10 && !visited.has(currentPath)) {
|
|
1480
|
+
visited.add(currentPath);
|
|
1481
|
+
try {
|
|
1482
|
+
const content = await readFile(currentPath, "utf8");
|
|
1483
|
+
const parent = JSON.parse(content).parent;
|
|
1484
|
+
if (!parent) {
|
|
1485
|
+
this.displayTypeCache.set(cacheKey, "3d");
|
|
1486
|
+
return "3d";
|
|
1487
|
+
}
|
|
1488
|
+
if (parent === "minecraft:item/generated" || parent === "item/generated" || parent === "minecraft:item/handheld" || parent === "item/handheld") {
|
|
1489
|
+
this.displayTypeCache.set(cacheKey, "2d");
|
|
1490
|
+
return "2d";
|
|
1491
|
+
}
|
|
1492
|
+
if (parent.startsWith("minecraft:block/") || parent.startsWith("block/")) {
|
|
1493
|
+
this.displayTypeCache.set(cacheKey, "3d");
|
|
1494
|
+
return "3d";
|
|
1495
|
+
}
|
|
1496
|
+
[, modelPath] = parent.includes(":") ? parent.split(":") : ["minecraft", parent];
|
|
1497
|
+
currentPath = join$1(BASE_PATH, `${modelPath}.json`);
|
|
1498
|
+
depth++;
|
|
1499
|
+
} catch {
|
|
1500
|
+
this.displayTypeCache.set(cacheKey, "3d");
|
|
1501
|
+
return "3d";
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
this.displayTypeCache.set(cacheKey, "3d");
|
|
1505
|
+
return "3d";
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* minecraftId からレンダーパスを取得
|
|
1509
|
+
* ブロックモデルなら block/{id}、アイテムモデルなら item/{id}
|
|
1510
|
+
*/
|
|
1511
|
+
async getItemRenderPath(versionId, minecraftId) {
|
|
1512
|
+
const assetsDir = await this.versionManager.getAssets(versionId);
|
|
1513
|
+
const baseId = minecraftId.replace(/^minecraft:/, "");
|
|
1514
|
+
let itemModelPath = join$1(assetsDir, "assets", "minecraft", "models", "item", `${baseId}.json`);
|
|
1515
|
+
if (existsSync$1(itemModelPath)) {
|
|
1516
|
+
if (await this.getModelDisplayType(`item/${baseId}`, assetsDir) === "2d") return `item/${baseId}`;
|
|
1517
|
+
try {
|
|
1518
|
+
const itemModelContent = await readFile(itemModelPath, "utf8");
|
|
1519
|
+
const itemModelJson = JSON.parse(itemModelContent);
|
|
1520
|
+
if (itemModelJson.parent && itemModelJson.parent.startsWith("block/")) return itemModelJson.parent;
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
logger_default.warn(`Failed to parse item model ${itemModelPath}: ${e}`);
|
|
1523
|
+
}
|
|
1524
|
+
return `item/${baseId}`;
|
|
1525
|
+
}
|
|
1526
|
+
if (existsSync$1(join$1(assetsDir, "assets", "minecraft", "models", "block", `${baseId}.json`))) return `block/${baseId}`;
|
|
1527
|
+
logger_default.warn(`Could not find render path for ${minecraftId}. Defaulting to item/${baseId}.`);
|
|
1528
|
+
return `item/${baseId}`;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* 3Dアイテムのリストを取得
|
|
1532
|
+
*/
|
|
1533
|
+
async get3DItems(versionId) {
|
|
1534
|
+
if (this.items3dCache.has(versionId)) return this.items3dCache.get(versionId);
|
|
1535
|
+
try {
|
|
1536
|
+
const itemIds = await this.getItemIds(versionId);
|
|
1537
|
+
const assetsDir = await this.versionManager.getAssets(versionId);
|
|
1538
|
+
const items3d = [];
|
|
1539
|
+
const batchSize = 30;
|
|
1540
|
+
for (let i = 0; i < itemIds.length; i += batchSize) {
|
|
1541
|
+
const batchPromises = itemIds.slice(i, i + batchSize).map(async (itemId) => ({
|
|
1542
|
+
itemId,
|
|
1543
|
+
type: await this.getModelDisplayType(itemId, assetsDir)
|
|
1544
|
+
}));
|
|
1545
|
+
const batchResults = await Promise.all(batchPromises);
|
|
1546
|
+
for (const result of batchResults) if (result.type === "3d") items3d.push(result.itemId);
|
|
1547
|
+
}
|
|
1548
|
+
logger_default.debug(`Found ${items3d.length} 3D items out of ${itemIds.length} total items`);
|
|
1549
|
+
this.items3dCache.set(versionId, items3d);
|
|
1550
|
+
return items3d;
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
logger_default.error(`Failed to get 3D items for ${versionId}: ${error}`);
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* 3Dアイテムのリストを段階的に取得(dev用:キャッシュからの読み込みのみ)
|
|
1558
|
+
* ファイルシステム操作を最小限に抑える
|
|
1559
|
+
*/
|
|
1560
|
+
async get3DItemsLazy(versionId) {
|
|
1561
|
+
if (this.items3dCache.has(versionId)) return this.items3dCache.get(versionId);
|
|
1562
|
+
return [];
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
function createItemManager(resourcePackPath, versionManager) {
|
|
1566
|
+
return new ItemManager(resourcePackPath, versionManager);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
//#endregion
|
|
1570
|
+
//#region src/render/Builder.ts
|
|
1571
|
+
const clone = (value) => JSON.parse(JSON.stringify(value));
|
|
1572
|
+
/**
|
|
1573
|
+
* テクスチャ参照を解決(#texture_nameのような参照を実際のパスに変換)
|
|
1574
|
+
*/
|
|
1575
|
+
function resolveTexturePath(textures = {}, ref, visited = /* @__PURE__ */ new Set()) {
|
|
1576
|
+
if (!ref) return null;
|
|
1577
|
+
if (visited.has(ref)) return null;
|
|
1578
|
+
if (ref.startsWith("#")) {
|
|
1579
|
+
const key = ref.slice(1);
|
|
1580
|
+
visited.add(ref);
|
|
1581
|
+
return resolveTexturePath(textures, textures[key], visited);
|
|
1582
|
+
}
|
|
1583
|
+
const texturePath = new MinecraftPathResolver("").normalizeTexturePath(ref);
|
|
1584
|
+
return `${MINECRAFT_PATHS.minecraft}/${texturePath}`;
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* モデル参照を再帰的に検索
|
|
1588
|
+
*/
|
|
1589
|
+
function findModelReference(node) {
|
|
1590
|
+
if (!node || typeof node !== "object") return null;
|
|
1591
|
+
if (node.type === "minecraft:model" && typeof node.model === "string") return node.model;
|
|
1592
|
+
if (node.fallback) {
|
|
1593
|
+
const fallbackFound = findModelReference(node.fallback);
|
|
1594
|
+
if (fallbackFound) return fallbackFound;
|
|
1595
|
+
}
|
|
1596
|
+
if (Array.isArray(node.cases)) for (const entry of node.cases) {
|
|
1597
|
+
const found = findModelReference(entry?.model ?? entry);
|
|
1598
|
+
if (found) return found;
|
|
1599
|
+
}
|
|
1600
|
+
if (Array.isArray(node.entries)) for (const entry of node.entries) {
|
|
1601
|
+
const found = findModelReference(entry?.model ?? entry);
|
|
1602
|
+
if (found) return found;
|
|
1603
|
+
}
|
|
1604
|
+
if (Array.isArray(node)) {
|
|
1605
|
+
for (const entry of node) {
|
|
1606
|
+
const found = findModelReference(entry);
|
|
1607
|
+
if (found) return found;
|
|
1608
|
+
}
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
for (const value of Object.values(node)) {
|
|
1612
|
+
const found = findModelReference(value);
|
|
1613
|
+
if (found) return found;
|
|
1614
|
+
}
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
var ResourcePackBuilder = class {
|
|
1618
|
+
modelsCache = /* @__PURE__ */ new Map();
|
|
1619
|
+
resolvedModelCache = /* @__PURE__ */ new Map();
|
|
1620
|
+
pathResolver;
|
|
1621
|
+
constructor(resourcePackPath) {
|
|
1622
|
+
this.pathResolver = new MinecraftPathResolver(resourcePackPath);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* リソースパック内のすべてのブロックモデルを構築
|
|
1626
|
+
*/
|
|
1627
|
+
async buildAllModels() {
|
|
1628
|
+
const modelsDir = this.pathResolver.getBlockModelsDir();
|
|
1629
|
+
if (!existsSync$1(modelsDir)) {
|
|
1630
|
+
console.warn(`Models directory not found: ${modelsDir}`);
|
|
1631
|
+
return [];
|
|
1632
|
+
}
|
|
1633
|
+
const modelFiles = (await readdir(modelsDir)).filter((f) => f.endsWith(".json"));
|
|
1634
|
+
for (const file of modelFiles) {
|
|
1635
|
+
const name$1 = file.slice(0, -5);
|
|
1636
|
+
const filePath = join$1(modelsDir, file);
|
|
1637
|
+
try {
|
|
1638
|
+
const content = await readFile(filePath, "utf-8");
|
|
1639
|
+
this.modelsCache.set(name$1, JSON.parse(content));
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
console.warn(`Failed to load model ${name$1}:`, error);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
const resolvedModels = [];
|
|
1645
|
+
for (const name$1 of this.modelsCache.keys()) {
|
|
1646
|
+
const resolvedModel = this.resolveModel(name$1);
|
|
1647
|
+
if (!resolvedModel) continue;
|
|
1648
|
+
const elements = resolvedModel.elements || [];
|
|
1649
|
+
const usedTextures = /* @__PURE__ */ new Map();
|
|
1650
|
+
for (const element of elements) {
|
|
1651
|
+
const faces = element.faces || {};
|
|
1652
|
+
for (const [, faceDef] of Object.entries(faces)) {
|
|
1653
|
+
const texturePath = resolveTexturePath(resolvedModel.textures || {}, faceDef?.texture);
|
|
1654
|
+
if (texturePath && !usedTextures.has(texturePath)) usedTextures.set(texturePath, {
|
|
1655
|
+
path: texturePath,
|
|
1656
|
+
dataUrl: null,
|
|
1657
|
+
mcmeta: null
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
resolvedModels.push({
|
|
1662
|
+
name: name$1,
|
|
1663
|
+
model: resolvedModel,
|
|
1664
|
+
sourceModel: clone(this.modelsCache.get(name$1)),
|
|
1665
|
+
usedTextures: Array.from(usedTextures.values())
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
resolvedModels.sort((a, b) => a.name.localeCompare(b.name));
|
|
1669
|
+
return resolvedModels;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* すべてのアイテムを構築
|
|
1673
|
+
*/
|
|
1674
|
+
async buildAllItems() {
|
|
1675
|
+
const itemsDir = this.pathResolver.getItemsDir();
|
|
1676
|
+
if (!existsSync$1(itemsDir)) {
|
|
1677
|
+
console.warn(`Items directory not found: ${itemsDir}`);
|
|
1678
|
+
return [];
|
|
1679
|
+
}
|
|
1680
|
+
const itemFiles = (await readdir(itemsDir)).filter((f) => f.endsWith(".json"));
|
|
1681
|
+
const resolvedItems = [];
|
|
1682
|
+
for (const file of itemFiles) {
|
|
1683
|
+
const name$1 = file.slice(0, -5);
|
|
1684
|
+
const filePath = join$1(itemsDir, file);
|
|
1685
|
+
try {
|
|
1686
|
+
const content = await readFile(filePath, "utf-8");
|
|
1687
|
+
const definition = JSON.parse(content);
|
|
1688
|
+
const normalized = findModelReference(definition?.model ?? definition)?.replace(/^minecraft:/, "");
|
|
1689
|
+
const isBlockModel = normalized?.startsWith("block/");
|
|
1690
|
+
const texturePath = `assets/minecraft/textures/item/${normalized?.startsWith("item/") ? normalized.slice(5) : name$1}.png`;
|
|
1691
|
+
resolvedItems.push({
|
|
1692
|
+
name: name$1,
|
|
1693
|
+
definition,
|
|
1694
|
+
modelReference: normalized ?? null,
|
|
1695
|
+
textureInfo: {
|
|
1696
|
+
texturePath: isBlockModel ? null : texturePath,
|
|
1697
|
+
dataUrl: null,
|
|
1698
|
+
mcmeta: null
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
console.warn(`Failed to load item ${name$1}:`, error);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
resolvedItems.sort((a, b) => a.name.localeCompare(b.name));
|
|
1706
|
+
return resolvedItems;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* モデルとその親の継承を解決
|
|
1710
|
+
*/
|
|
1711
|
+
resolveModel(name$1, chain = /* @__PURE__ */ new Set()) {
|
|
1712
|
+
if (this.resolvedModelCache.has(name$1)) return this.resolvedModelCache.get(name$1);
|
|
1713
|
+
if (chain.has(name$1)) return null;
|
|
1714
|
+
chain.add(name$1);
|
|
1715
|
+
const baseModel = this.modelsCache.get(name$1);
|
|
1716
|
+
if (!baseModel) return null;
|
|
1717
|
+
const model = clone(baseModel);
|
|
1718
|
+
if (model.parent) {
|
|
1719
|
+
let parentName = model.parent.replace(/^minecraft:/, "");
|
|
1720
|
+
if (parentName.startsWith("block/")) parentName = parentName.slice(6);
|
|
1721
|
+
const parentModel = this.resolveModel(parentName, chain);
|
|
1722
|
+
if (parentModel) {
|
|
1723
|
+
model.textures = {
|
|
1724
|
+
...parentModel.textures || {},
|
|
1725
|
+
...model.textures || {}
|
|
1726
|
+
};
|
|
1727
|
+
if (!model.elements && parentModel.elements) model.elements = clone(parentModel.elements);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
this.resolvedModelCache.set(name$1, model);
|
|
1731
|
+
return model;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* キャッシュをクリア
|
|
1735
|
+
*/
|
|
1736
|
+
clearCache() {
|
|
1737
|
+
this.modelsCache.clear();
|
|
1738
|
+
this.resolvedModelCache.clear();
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
//#endregion
|
|
1743
|
+
//#region src/render/ResourcePack.ts
|
|
1744
|
+
/**
|
|
1745
|
+
* Minecraft リソースパック統合システム
|
|
1746
|
+
* リソースパック解析とレンダリングの統合ファサード
|
|
1747
|
+
*/
|
|
1748
|
+
var MinecraftResourcePack = class {
|
|
1749
|
+
builder;
|
|
1750
|
+
renderer;
|
|
1751
|
+
constructor(resourcePackPath, modelPath) {
|
|
1752
|
+
this.builder = new ResourcePackBuilder(resourcePackPath);
|
|
1753
|
+
this.renderer = new MinecraftBlockRenderer(resourcePackPath, modelPath);
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* レンダラーを取得(内部使用)
|
|
1757
|
+
*/
|
|
1758
|
+
getRenderer() {
|
|
1759
|
+
return this.renderer;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* すべてのブロックモデルを取得
|
|
1763
|
+
*/
|
|
1764
|
+
async getAllBlockModels() {
|
|
1765
|
+
return this.builder.buildAllModels();
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* すべてのアイテムを取得
|
|
1769
|
+
*/
|
|
1770
|
+
async getAllItems() {
|
|
1771
|
+
return this.builder.buildAllItems();
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* ブロックモデルの詳細情報を取得
|
|
1775
|
+
*/
|
|
1776
|
+
async getBlockModel(blockName) {
|
|
1777
|
+
return (await this.builder.buildAllModels()).find((m) => m.name === blockName) || null;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* 複数のブロックをレンダリング
|
|
1781
|
+
*/
|
|
1782
|
+
async renderBlocks(blockNames, options = {}) {
|
|
1783
|
+
const { outputDir = "./renders", width = CONFIG.WIDTH, height = options.width ?? CONFIG.HEIGHT, scale, rotation = CONFIG.ROTATION, dryRun = false } = options;
|
|
1784
|
+
const renderOptions = {
|
|
1785
|
+
width,
|
|
1786
|
+
height,
|
|
1787
|
+
scale,
|
|
1788
|
+
rotation
|
|
1789
|
+
};
|
|
1790
|
+
const result = {
|
|
1791
|
+
success: [],
|
|
1792
|
+
failed: []
|
|
1793
|
+
};
|
|
1794
|
+
const renderTasks = blockNames.map(async (blockName) => {
|
|
1795
|
+
const normalizedName = blockName.replace(/^minecraft:/, "");
|
|
1796
|
+
const modelPath = `block/${normalizedName}`;
|
|
1797
|
+
const outputPath = join$1(outputDir, `${normalizedName}.png`);
|
|
1798
|
+
try {
|
|
1799
|
+
if (dryRun) {
|
|
1800
|
+
console.log(`[DRY-RUN] Would render: ${normalizedName} -> ${outputPath}`);
|
|
1801
|
+
return {
|
|
1802
|
+
type: "success",
|
|
1803
|
+
name: normalizedName
|
|
1804
|
+
};
|
|
1805
|
+
} else {
|
|
1806
|
+
await this.renderer.renderBlock(modelPath, outputPath, renderOptions);
|
|
1807
|
+
return {
|
|
1808
|
+
type: "success",
|
|
1809
|
+
name: normalizedName
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
} catch (error) {
|
|
1813
|
+
console.error(`❌ Failed to render ${blockName}:`, error);
|
|
1814
|
+
return {
|
|
1815
|
+
type: "failed",
|
|
1816
|
+
name: normalizedName
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
const results = await Promise.allSettled(renderTasks);
|
|
1821
|
+
for (const settledResult of results) if (settledResult.status === "fulfilled") {
|
|
1822
|
+
const { type: type$1, name: name$1 } = settledResult.value;
|
|
1823
|
+
if (type$1 === "success") result.success.push(name$1);
|
|
1824
|
+
else result.failed.push(name$1);
|
|
1825
|
+
} else result.failed.push("unknown");
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* すべてのブロックモデルをレンダリング
|
|
1830
|
+
*/
|
|
1831
|
+
async renderAllBlocks(options = {}) {
|
|
1832
|
+
const blockNames = (await this.builder.buildAllModels()).map((m) => m.name);
|
|
1833
|
+
console.log(`🎨 Rendering ${blockNames.length} block models...`);
|
|
1834
|
+
const result = await this.renderBlocks(blockNames, options);
|
|
1835
|
+
console.log(`\n📊 Render Summary:`);
|
|
1836
|
+
console.log(` ✅ Success: ${result.success.length}`);
|
|
1837
|
+
console.log(` ❌ Failed: ${result.failed.length}`);
|
|
1838
|
+
return {
|
|
1839
|
+
success: result.success.length,
|
|
1840
|
+
failed: result.failed.length
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* モデルの使用テクスチャを取得
|
|
1845
|
+
*/
|
|
1846
|
+
async getModelTextures(blockName) {
|
|
1847
|
+
const model = await this.getBlockModel(blockName);
|
|
1848
|
+
if (!model) return [];
|
|
1849
|
+
return model.usedTextures.map((t) => t.path);
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* キャッシュをクリア
|
|
1853
|
+
*/
|
|
1854
|
+
clearCache() {
|
|
1855
|
+
this.builder.clearCache();
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
function createResourcePack(resourcePackPath, modelPath) {
|
|
1859
|
+
return new MinecraftResourcePack(resourcePackPath, modelPath);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
//#endregion
|
|
1863
|
+
//#region src/patternMatcher.ts
|
|
1864
|
+
/**
|
|
1865
|
+
* より正確なパターンマッチング
|
|
1866
|
+
*/
|
|
1867
|
+
function matchesPattern(path$1, patterns) {
|
|
1868
|
+
const pathParts = path$1.split("/");
|
|
1869
|
+
return patterns.some((pattern) => {
|
|
1870
|
+
const regex = /* @__PURE__ */ new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".+").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]") + "$");
|
|
1871
|
+
if (!pattern.includes("/")) return pathParts.some((part) => regex.test(part));
|
|
1872
|
+
return regex.test(path$1);
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
//#endregion
|
|
1877
|
+
//#region src/codeScanner.ts
|
|
1878
|
+
/**
|
|
1879
|
+
* ソースコードをスキャンして、使用されているMinecraft IDとレンダリングオプションを検出
|
|
1880
|
+
*/
|
|
1881
|
+
function scanSourceCode(root, options = {}) {
|
|
1882
|
+
const { include = [
|
|
1883
|
+
"**/*.ts",
|
|
1884
|
+
"**/*.tsx",
|
|
1885
|
+
"**/*.js",
|
|
1886
|
+
"**/*.jsx"
|
|
1887
|
+
], exclude = [], outputPath = "./mcpacks", distDir = "./dist" } = options;
|
|
1888
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1889
|
+
const renderingOptions = /* @__PURE__ */ new Map();
|
|
1890
|
+
const alwaysExclude = [
|
|
1891
|
+
"node_modules",
|
|
1892
|
+
".git",
|
|
1893
|
+
"*.d.ts"
|
|
1894
|
+
];
|
|
1895
|
+
const normalizedOutputPath = outputPath.replace(/^\.\//, "").replace(/\/$/, "");
|
|
1896
|
+
const normalizedDistDir = distDir.replace(/^\.\//, "").replace(/\/$/, "");
|
|
1897
|
+
const finalExclude = [
|
|
1898
|
+
...alwaysExclude,
|
|
1899
|
+
normalizedOutputPath,
|
|
1900
|
+
normalizedDistDir,
|
|
1901
|
+
...exclude
|
|
1902
|
+
];
|
|
1903
|
+
const scanDir = (dir, relativeBase = "") => {
|
|
1904
|
+
try {
|
|
1905
|
+
const entries = readdirSync(dir);
|
|
1906
|
+
for (const entry of entries) {
|
|
1907
|
+
const fullPath = join(dir, entry);
|
|
1908
|
+
const relativePath = relativeBase ? `${relativeBase}/${entry}` : entry;
|
|
1909
|
+
const stat = statSync(fullPath);
|
|
1910
|
+
if (matchesPattern(relativePath, finalExclude)) continue;
|
|
1911
|
+
if (stat.isDirectory()) scanDir(fullPath, relativePath);
|
|
1912
|
+
else if (stat.isFile()) {
|
|
1913
|
+
if (matchesPattern(relativePath, include)) extractResourceIds(fullPath, usedIds, renderingOptions);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
} catch {}
|
|
1917
|
+
};
|
|
1918
|
+
scanDir(root);
|
|
1919
|
+
return {
|
|
1920
|
+
usedIds,
|
|
1921
|
+
renderingOptions
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* オプションのハッシュキーを生成
|
|
1926
|
+
*/
|
|
1927
|
+
function generateOptionHash(width, height, scale) {
|
|
1928
|
+
const parts = [];
|
|
1929
|
+
if (width !== void 0) parts.push(`w${width}`);
|
|
1930
|
+
if (height !== void 0) parts.push(`h${height}`);
|
|
1931
|
+
if (scale !== void 0) parts.push(`s${scale}`);
|
|
1932
|
+
return parts.join("_");
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* ファイルからMinecraft IDを抽出
|
|
1936
|
+
*/
|
|
1937
|
+
function extractResourceIds(filePath, usedIds, renderingOptions) {
|
|
1938
|
+
try {
|
|
1939
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1940
|
+
const resourcePackRegex = /getResourcePack\s*\(\s*["']minecraft:([a-z0-9/_\-\.]+)["']\s*,\s*\{\s*([^}]*)\s*\}\s*\)/gi;
|
|
1941
|
+
let match;
|
|
1942
|
+
const processedItems = /* @__PURE__ */ new Set();
|
|
1943
|
+
while ((match = resourcePackRegex.exec(content)) !== null) {
|
|
1944
|
+
const itemId = `minecraft:${match[1]}`;
|
|
1945
|
+
usedIds.add(itemId);
|
|
1946
|
+
const optionsStr = match[2];
|
|
1947
|
+
let width;
|
|
1948
|
+
let height;
|
|
1949
|
+
let scale;
|
|
1950
|
+
const widthMatch = /width\s*:\s*(\d+)/.exec(optionsStr);
|
|
1951
|
+
const heightMatch = /height\s*:\s*(\d+)/.exec(optionsStr);
|
|
1952
|
+
const scaleMatch = /scale\s*:\s*([\d.]+)/.exec(optionsStr);
|
|
1953
|
+
if (widthMatch) width = parseInt(widthMatch[1], 10);
|
|
1954
|
+
if (heightMatch) height = parseInt(heightMatch[1], 10);
|
|
1955
|
+
if (scaleMatch) scale = parseFloat(scaleMatch[1]);
|
|
1956
|
+
if (width || height || scale) {
|
|
1957
|
+
const optionHash = generateOptionHash(width, height, scale);
|
|
1958
|
+
const uniqueKey = `${itemId}:${optionHash}`;
|
|
1959
|
+
if (!renderingOptions.has(uniqueKey)) renderingOptions.set(uniqueKey, {
|
|
1960
|
+
itemId,
|
|
1961
|
+
optionHash,
|
|
1962
|
+
width,
|
|
1963
|
+
height,
|
|
1964
|
+
scale
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
processedItems.add(itemId);
|
|
1968
|
+
}
|
|
1969
|
+
const resourcePackNoOptionRegex = /getResourcePack\s*\(\s*["']minecraft:([a-z0-9/_\-\.]+)["']\s*(?!\s*,\s*\{)(?=\s*\))/gi;
|
|
1970
|
+
while ((match = resourcePackNoOptionRegex.exec(content)) !== null) {
|
|
1971
|
+
const itemId = `minecraft:${match[1]}`;
|
|
1972
|
+
usedIds.add(itemId);
|
|
1973
|
+
const uniqueKey = `${itemId}:default`;
|
|
1974
|
+
if (!renderingOptions.has(uniqueKey)) renderingOptions.set(uniqueKey, {
|
|
1975
|
+
itemId,
|
|
1976
|
+
optionHash: "default"
|
|
1977
|
+
});
|
|
1978
|
+
processedItems.add(itemId);
|
|
1979
|
+
}
|
|
1980
|
+
} catch {}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
//#endregion
|
|
1984
|
+
//#region src/plugin/core.ts
|
|
1985
|
+
const parseConfig = (options) => {
|
|
1986
|
+
const validatedOptions = PluginOptionsSchema.parse(options);
|
|
1987
|
+
return {
|
|
1988
|
+
mcVersion: validatedOptions.mcVersion,
|
|
1989
|
+
resourcePackPath: validatedOptions.resourcePackPath,
|
|
1990
|
+
outputPath: validatedOptions.outputPath ?? CONFIG.OUTPUT_DIR,
|
|
1991
|
+
emptyOutDir: validatedOptions.emptyOutDir ?? CONFIG.EMPTY_OUT_DIR,
|
|
1992
|
+
include: validatedOptions.include ?? CONFIG.INCLUDE,
|
|
1993
|
+
exclude: validatedOptions.exclude ?? CONFIG.EXCLUDE,
|
|
1994
|
+
cacheDir: validatedOptions.cacheDir ?? CONFIG.CACHE_DIR,
|
|
1995
|
+
startUpRenderCacheRefresh: validatedOptions.startUpRenderCacheRefresh ?? CONFIG.START_UP_RENDER_CACHE_REFRESH,
|
|
1996
|
+
logLevel: validatedOptions.logLevel ?? CONFIG.LOG_LEVEL
|
|
1997
|
+
};
|
|
1998
|
+
};
|
|
1999
|
+
var McResourcesCore = class {
|
|
2000
|
+
config;
|
|
2001
|
+
renderingTasks = /* @__PURE__ */ new Map();
|
|
2002
|
+
fileGenerationStarted = false;
|
|
2003
|
+
fileGenerationPromise = null;
|
|
2004
|
+
isGenerated = false;
|
|
2005
|
+
resourcePack = null;
|
|
2006
|
+
versionManager;
|
|
2007
|
+
itemManager;
|
|
2008
|
+
constructor(config) {
|
|
2009
|
+
this.config = parseConfig(config);
|
|
2010
|
+
if (!this.config.cacheDir) throw new Error("Cache directory is not set. Please set the cache directory in the configuration.");
|
|
2011
|
+
this.versionManager = createVersionManager(this.config.cacheDir);
|
|
2012
|
+
this.itemManager = createItemManager(this.config.resourcePackPath, this.versionManager);
|
|
2013
|
+
logger_default.setLogLevel(this.config.logLevel);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* ItemManagerを取得
|
|
2017
|
+
*/
|
|
2018
|
+
getItemManager() {
|
|
2019
|
+
if (!this.itemManager) throw new Error("ItemManager is not initialized. Call initializeManagers() first.");
|
|
2020
|
+
return this.itemManager;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* VersionManagerを取得
|
|
2024
|
+
*/
|
|
2025
|
+
getVersionManager() {
|
|
2026
|
+
if (!this.versionManager) throw new Error("VersionManager is not initialized. Call initializeManagers() first.");
|
|
2027
|
+
return this.versionManager;
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Dev Modeのアセット取得
|
|
2031
|
+
*/
|
|
2032
|
+
async getAssetsInDevMode() {
|
|
2033
|
+
setTimeout(() => {
|
|
2034
|
+
this.versionManager.getAssets(this.config.mcVersion).catch((err) => {
|
|
2035
|
+
logger_default.warn(`Failed to pre-fetch assets: ${err}`);
|
|
2036
|
+
});
|
|
2037
|
+
}, 500);
|
|
2038
|
+
setTimeout(() => {
|
|
2039
|
+
this.itemManager.get3DItems(this.config.mcVersion).catch((err) => {
|
|
2040
|
+
logger_default.warn(`Failed to preload 3D items: ${err}`);
|
|
2041
|
+
});
|
|
2042
|
+
}, 1e3);
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Build Modeのアセット取得
|
|
2046
|
+
*/
|
|
2047
|
+
async getAssetsInBuildMode() {
|
|
2048
|
+
try {
|
|
2049
|
+
return await this.versionManager.getAssets(this.config.mcVersion);
|
|
2050
|
+
} catch (err) {
|
|
2051
|
+
logger_default.warn(`Failed to pre-fetch assets: ${err}`);
|
|
2052
|
+
throw err;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
/**
|
|
2056
|
+
* ファイル生成関数(遅延生成対応)
|
|
2057
|
+
*/
|
|
2058
|
+
async generateFiles(options = {}) {
|
|
2059
|
+
const { isBuild = false, usedIds = void 0, ensureItems3d = false, itemsUrlMap = void 0 } = options;
|
|
2060
|
+
if (this.isGenerated) return;
|
|
2061
|
+
if (this.fileGenerationPromise) return this.fileGenerationPromise;
|
|
2062
|
+
this.fileGenerationPromise = (async () => {
|
|
2063
|
+
let itemManager;
|
|
2064
|
+
try {
|
|
2065
|
+
itemManager = this.getItemManager();
|
|
2066
|
+
} catch {}
|
|
2067
|
+
if (ensureItems3d && !isBuild && itemManager) try {
|
|
2068
|
+
await itemManager.get3DItems(this.config.mcVersion);
|
|
2069
|
+
} catch (err) {
|
|
2070
|
+
logger_default.warn(`Failed to fetch 3D items: ${err}`);
|
|
2071
|
+
}
|
|
2072
|
+
const images = getAllImages(this.config.resourcePackPath);
|
|
2073
|
+
const jsCode = await generateGetResourcePackCode({
|
|
2074
|
+
images,
|
|
2075
|
+
usedIds,
|
|
2076
|
+
itemManager,
|
|
2077
|
+
versionId: this.config.mcVersion,
|
|
2078
|
+
itemsUrlMap
|
|
2079
|
+
});
|
|
2080
|
+
const tsCode = await generateTypeDefinitions({
|
|
2081
|
+
images,
|
|
2082
|
+
itemManager,
|
|
2083
|
+
versionId: this.config.mcVersion
|
|
2084
|
+
});
|
|
2085
|
+
writeFiles(this.config.outputPath, jsCode, tsCode);
|
|
2086
|
+
logger_default.info(chalk.bgGreen("Generated") + " TypeScript and JavaScript files");
|
|
2087
|
+
this.isGenerated = true;
|
|
2088
|
+
})();
|
|
2089
|
+
return this.fileGenerationPromise;
|
|
2090
|
+
}
|
|
2091
|
+
/**
|
|
2092
|
+
* ビルド時にアイテムをレンダリング(outputPath配下に保存)
|
|
2093
|
+
*/
|
|
2094
|
+
async renderItemsForBuildWithEmit(detectedIds, renderingOptions) {
|
|
2095
|
+
const itemUrlMap = /* @__PURE__ */ new Map();
|
|
2096
|
+
if (detectedIds.size === 0) return itemUrlMap;
|
|
2097
|
+
try {
|
|
2098
|
+
const assetsDirPath = await this.versionManager.getAssets(this.config.mcVersion);
|
|
2099
|
+
if (!this.resourcePack) this.resourcePack = createResourcePack(this.config.resourcePackPath, assetsDirPath);
|
|
2100
|
+
const renderedItemsDir = join(this.config.outputPath, "rendered-items");
|
|
2101
|
+
if (existsSync(renderedItemsDir)) {
|
|
2102
|
+
const files = readdirSync(renderedItemsDir);
|
|
2103
|
+
for (const file of files) rmSync(join(renderedItemsDir, file), {
|
|
2104
|
+
recursive: true,
|
|
2105
|
+
force: true
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
mkdirSync(renderedItemsDir, { recursive: true });
|
|
2109
|
+
const renderTargets = [];
|
|
2110
|
+
const processedItems = /* @__PURE__ */ new Set();
|
|
2111
|
+
if (renderingOptions && renderingOptions.size > 0) {
|
|
2112
|
+
for (const [, opt] of renderingOptions) if (detectedIds.has(opt.itemId)) {
|
|
2113
|
+
renderTargets.push({
|
|
2114
|
+
itemId: opt.itemId,
|
|
2115
|
+
optionHash: opt.optionHash,
|
|
2116
|
+
width: opt.width,
|
|
2117
|
+
height: opt.height,
|
|
2118
|
+
scale: opt.scale
|
|
2119
|
+
});
|
|
2120
|
+
processedItems.add(opt.itemId);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
for (const itemId of detectedIds) if (!processedItems.has(itemId)) renderTargets.push({ itemId });
|
|
2124
|
+
logger_default.info(`Rendering ${renderTargets.length} items for build...`);
|
|
2125
|
+
const renderPromises = renderTargets.map(async (target) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const cleanId = target.itemId.replace("minecraft:", "");
|
|
2128
|
+
const fileName = target.optionHash && target.optionHash !== "default" ? `${cleanId}_${target.optionHash}.png` : `${cleanId}.png`;
|
|
2129
|
+
const outputFile = join(renderedItemsDir, fileName);
|
|
2130
|
+
const isItemModel = await this.itemManager.isItem2DModel(target.itemId, assetsDirPath);
|
|
2131
|
+
const modelPath = isItemModel ? `item/${cleanId}` : `block/${cleanId}`;
|
|
2132
|
+
const renderOptions = {
|
|
2133
|
+
width: target.width ?? (isItemModel ? CONFIG.TEXTURE_SIZE : CONFIG.WIDTH),
|
|
2134
|
+
height: target.height ?? target.width ?? (isItemModel ? CONFIG.TEXTURE_SIZE : CONFIG.WIDTH),
|
|
2135
|
+
...target.scale !== void 0 && { scale: target.scale }
|
|
2136
|
+
};
|
|
2137
|
+
if (isItemModel) await this.resourcePack.getRenderer().renderItem(modelPath, outputFile, renderOptions);
|
|
2138
|
+
else await this.resourcePack.getRenderer().renderBlock(modelPath, outputFile, renderOptions);
|
|
2139
|
+
logger_default.info(`Rendered: ${target.itemId} with options: ${JSON.stringify(renderOptions)}`);
|
|
2140
|
+
return {
|
|
2141
|
+
mapKey: target.optionHash ? `${target.itemId}_${target.optionHash}` : target.itemId,
|
|
2142
|
+
relativePath: `./rendered-items/${fileName}`
|
|
2143
|
+
};
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
logger_default.warn(`Failed to render ${target.itemId} with options ${target.optionHash || "default"}: ${err}`);
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
});
|
|
2149
|
+
const results = await Promise.all(renderPromises);
|
|
2150
|
+
for (const result of results) if (result) {
|
|
2151
|
+
itemUrlMap.set(result.mapKey, result.relativePath);
|
|
2152
|
+
logger_default.info(`Rendered item saved: ${result.mapKey} -> ${result.relativePath}`);
|
|
2153
|
+
}
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
logger_default.error(`Failed to render items for build: ${error}`);
|
|
2156
|
+
}
|
|
2157
|
+
return itemUrlMap;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Build
|
|
2161
|
+
*/
|
|
2162
|
+
async build(options) {
|
|
2163
|
+
initializeOutputDirectory(this.config.outputPath, this.config.emptyOutDir);
|
|
2164
|
+
try {
|
|
2165
|
+
await this.getAssetsInBuildMode();
|
|
2166
|
+
} catch (err) {
|
|
2167
|
+
logger_default.warn(`Failed to get assets: ${err}`);
|
|
2168
|
+
}
|
|
2169
|
+
const root = process.cwd();
|
|
2170
|
+
const scanResult = scanSourceCode(root, {
|
|
2171
|
+
include: this.config.include,
|
|
2172
|
+
exclude: this.config.exclude,
|
|
2173
|
+
outputPath: this.config.outputPath,
|
|
2174
|
+
distDir: path.relative(root, options.distDir)
|
|
2175
|
+
});
|
|
2176
|
+
const detectedIds = scanResult.usedIds;
|
|
2177
|
+
const renderingOptions = scanResult.renderingOptions;
|
|
2178
|
+
logger_default.debug(`Rendering options: ${JSON.stringify(Array.from(renderingOptions?.entries() ?? []))}`);
|
|
2179
|
+
const itemsUrlMap = await this.renderItemsForBuildWithEmit(detectedIds, renderingOptions);
|
|
2180
|
+
await this.generateFiles({
|
|
2181
|
+
usedIds: detectedIds.size > 0 ? detectedIds : void 0,
|
|
2182
|
+
itemsUrlMap
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Dev Server Start
|
|
2187
|
+
*/
|
|
2188
|
+
async devServerStart() {
|
|
2189
|
+
if (this.config.startUpRenderCacheRefresh) rmSync(join(this.config.cacheDir, "renders"), {
|
|
2190
|
+
recursive: true,
|
|
2191
|
+
force: true
|
|
2192
|
+
});
|
|
2193
|
+
this.fileGenerationStarted = true;
|
|
2194
|
+
logger_default.debug("Starting file generation in background...");
|
|
2195
|
+
this.generateFiles({ ensureItems3d: true }).catch((err) => {
|
|
2196
|
+
logger_default.warn(`Failed to generate files: ${err}`);
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Dev Server Middleware
|
|
2201
|
+
*/
|
|
2202
|
+
async devServerMiddleware(options) {
|
|
2203
|
+
const { next, req, res, isBuild, isGenerated } = options;
|
|
2204
|
+
const { url, headers } = req;
|
|
2205
|
+
if (!isBuild && !isGenerated && !this.fileGenerationStarted) {
|
|
2206
|
+
this.fileGenerationStarted = true;
|
|
2207
|
+
logger_default.debug("Starting file generation in background...");
|
|
2208
|
+
this.generateFiles({ ensureItems3d: true }).catch((err) => {
|
|
2209
|
+
logger_default.warn(`Failed to generate files: ${err}`);
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
if (!url?.startsWith("/@hato810424:mc-resources-plugin/minecraft:")) {
|
|
2213
|
+
next();
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
try {
|
|
2217
|
+
const urlObj = new URL(url, `http://${headers.host}`);
|
|
2218
|
+
const minecraftId = urlObj.pathname.replace("/@hato810424:mc-resources-plugin/minecraft:", "");
|
|
2219
|
+
if (!minecraftId) {
|
|
2220
|
+
res.setStatus(400);
|
|
2221
|
+
res.send("Invalid minecraft ID");
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const assetsDirPath = await this.versionManager.getAssets(this.config.mcVersion);
|
|
2225
|
+
if (!this.resourcePack) this.resourcePack = createResourcePack(this.config.resourcePackPath, assetsDirPath);
|
|
2226
|
+
if (!this.itemManager) this.itemManager = createItemManager(this.config.resourcePackPath, this.versionManager);
|
|
2227
|
+
const isItemModel = await this.itemManager.isItem2DModel(minecraftId, assetsDirPath);
|
|
2228
|
+
const baseSize = isItemModel ? CONFIG.TEXTURE_SIZE : CONFIG.WIDTH;
|
|
2229
|
+
const width = parseInt(urlObj.searchParams.get("width") ?? String(baseSize), 10);
|
|
2230
|
+
const height = parseInt(urlObj.searchParams.get("height") ?? String(width), 10);
|
|
2231
|
+
const scaleParam = urlObj.searchParams.get("scale");
|
|
2232
|
+
const scale = scaleParam ? parseFloat(scaleParam) : void 0;
|
|
2233
|
+
if (isItemModel && width === 16 && height === 16 && scale === void 0) {
|
|
2234
|
+
if (!await this.itemManager.needsTint(this.config.mcVersion, minecraftId)) {
|
|
2235
|
+
const texturePath = await this.itemManager.getItemTexturePath(this.config.mcVersion, minecraftId);
|
|
2236
|
+
if (texturePath && existsSync(texturePath)) {
|
|
2237
|
+
const imageBuffer = readFileSync(texturePath);
|
|
2238
|
+
res.setHeader("Content-Type", "image/png");
|
|
2239
|
+
res.send(imageBuffer);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
const sendResponse = (imageBuffer) => {
|
|
2245
|
+
res.setHeader("Content-Type", "image/png");
|
|
2246
|
+
res.send(imageBuffer);
|
|
2247
|
+
};
|
|
2248
|
+
const cacheKey = `${minecraftId}_${width}x${height}${scale !== void 0 ? `_${scale}` : ""}.png`;
|
|
2249
|
+
const hash = createHash("md5").update(this.config.resourcePackPath).digest("hex").substring(0, 10);
|
|
2250
|
+
const cacheFile = join(this.config.cacheDir, "renders", hash, cacheKey);
|
|
2251
|
+
logger_default.debug(`Processing request: id=${minecraftId}, width=${width}, height=${height}, scale=${scale}, cacheKey=${cacheKey}`);
|
|
2252
|
+
if (existsSync(cacheFile)) {
|
|
2253
|
+
sendResponse(readFileSync(cacheFile));
|
|
2254
|
+
logger_default.info(`File cache hit: ${minecraftId}`);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
if (this.renderingTasks.has(cacheKey)) {
|
|
2258
|
+
sendResponse(await this.renderingTasks.get(cacheKey));
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const renderPromise = (async () => {
|
|
2262
|
+
logger_default.info(`Rendering ${minecraftId}...`);
|
|
2263
|
+
if (this.fileGenerationPromise) await this.fileGenerationPromise;
|
|
2264
|
+
const renderPath = await this.itemManager.getItemRenderPath(this.config.mcVersion, minecraftId);
|
|
2265
|
+
const renderOptions = {
|
|
2266
|
+
width,
|
|
2267
|
+
height
|
|
2268
|
+
};
|
|
2269
|
+
if (scale !== void 0) renderOptions.scale = scale;
|
|
2270
|
+
let outputPath = cacheFile;
|
|
2271
|
+
if (isItemModel) outputPath = await this.resourcePack.getRenderer().renderItem(renderPath, cacheFile, renderOptions);
|
|
2272
|
+
else outputPath = await this.resourcePack.getRenderer().renderBlock(renderPath, cacheFile, renderOptions);
|
|
2273
|
+
const imageBuffer = readFileSync(outputPath);
|
|
2274
|
+
logger_default.info(`Rendered: ${minecraftId} with options: ${JSON.stringify(renderOptions)}`);
|
|
2275
|
+
return imageBuffer;
|
|
2276
|
+
})();
|
|
2277
|
+
this.renderingTasks.set(cacheKey, renderPromise);
|
|
2278
|
+
try {
|
|
2279
|
+
sendResponse(await renderPromise);
|
|
2280
|
+
} finally {
|
|
2281
|
+
this.renderingTasks.delete(cacheKey);
|
|
2282
|
+
}
|
|
2283
|
+
} catch (error) {
|
|
2284
|
+
logger_default.error(`Failed to render minecraft item: ${error}`);
|
|
2285
|
+
const urlObj = new URL(url, `http://${headers.host}`);
|
|
2286
|
+
const extractedId = urlObj.pathname.replace("/@hato810424:mc-resources-plugin/minecraft:", "");
|
|
2287
|
+
const width = parseInt(urlObj.searchParams.get("width") ?? String(CONFIG.WIDTH), 10);
|
|
2288
|
+
const height = parseInt(urlObj.searchParams.get("height") ?? String(width), 10);
|
|
2289
|
+
const scaleParam = urlObj.searchParams.get("scale");
|
|
2290
|
+
const scale = scaleParam ? parseFloat(scaleParam) : void 0;
|
|
2291
|
+
const errorCacheKey = `${extractedId}_${width}x${height}${scale !== void 0 ? `_${scale}` : ""}.png`;
|
|
2292
|
+
this.renderingTasks.delete(errorCacheKey);
|
|
2293
|
+
res.setStatus(500);
|
|
2294
|
+
res.send("Failed to render minecraft item");
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
|
|
2299
|
+
//#endregion
|
|
2300
|
+
export { McResourcesCore as t };
|