@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.
@@ -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 };