diff --git a/installer-rp/renderer.js b/installer-rp/renderer.js index fa6a790..7e1973d 100644 --- a/installer-rp/renderer.js +++ b/installer-rp/renderer.js @@ -69,9 +69,11 @@ function renderStep1() { card.type = 'button' card.className = 'choiceCard' if (state.selectedKey === pack.key) card.classList.add('active') + var verLabel = pack.mcVersion ? '마인크래프트 ' + escapeHtml(pack.mcVersion) + ' · ' : '' card.innerHTML = '' + escapeHtml(pack.name) + '' + - '음악 ' + pack.list.music.length + '곡 · 사진 ' + pack.list.images.length + '장' + '' + verLabel + + '음악 ' + pack.list.music.length + '곡 · 사진 ' + pack.list.images.length + '장' card.addEventListener('click', function () { state.selectedKey = pack.key nextBtn.disabled = false diff --git a/src/installer-rp/main.ts b/src/installer-rp/main.ts index c49c74b..65f4feb 100644 --- a/src/installer-rp/main.ts +++ b/src/installer-rp/main.ts @@ -6,7 +6,8 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import { URL } from 'node:url' import type { ChildProcess } from 'node:child_process' -import type { Manifest, PackList } from '../shared/types.js' +import type { Manifest, PackDefinition, PackList } from '../shared/types.js' +import { normalizePackDefinition } from '../shared/store.js' import { getAppDataDir, getMcCustomDir } from '../shared/paths.js' import type { RpFetchedPack } from './types.js' import { ensureYtDlpExe } from './ytdlp.js' @@ -112,15 +113,26 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi for (const entry of manifest.packs ?? []) { if (typeof entry?.file !== 'string') continue const listUrl = `${state.baseUrl}/file/list/${encodeURIComponent(entry.file)}.json` + const packUrl = `${state.baseUrl}/manifest/${encodeURIComponent(entry.file)}.json` try { - const raw = await fetchJson>(listUrl) + // 목록(필수) + 팩 정의(mcVersion 용, 실패해도 폴백) 동시 로드. + const [listRaw, packRaw] = await Promise.all([ + fetchJson>(listUrl), + fetchJson>(packUrl).catch((err) => { + sendLog(`팩 정의 로드 실패 (${entry.file}): ${(err as Error).message} — mcVersion 폴백`) + return null + }) + ]) const list: PackList = { - musicPlaylistUrl: typeof raw.musicPlaylistUrl === 'string' ? raw.musicPlaylistUrl : '', - imagePlaylistUrl: typeof raw.imagePlaylistUrl === 'string' ? raw.imagePlaylistUrl : '', - music: Array.isArray(raw.music) ? raw.music : [], - images: Array.isArray(raw.images) ? raw.images : [] + musicPlaylistUrl: typeof listRaw.musicPlaylistUrl === 'string' ? listRaw.musicPlaylistUrl : '', + imagePlaylistUrl: typeof listRaw.imagePlaylistUrl === 'string' ? listRaw.imagePlaylistUrl : '', + music: Array.isArray(listRaw.music) ? listRaw.music : [], + images: Array.isArray(listRaw.images) ? listRaw.images : [] } - results.push({ key: entry.file, name: entry.name || entry.file, list }) + const mcVersion = packRaw + ? normalizePackDefinition(packRaw as Partial).mcVersion + : '' + results.push({ key: entry.file, name: entry.name || entry.file, mcVersion, list }) } catch (error) { sendLog(`목록 로드 실패 (${entry.file}): ${(error as Error).message}`) } @@ -222,8 +234,10 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string musicDir, paintingDir, packName: pack.name, + mcVersion: pack.mcVersion, workDir: tempRoot, - outZipPath: resourcepackPath + outZipPath: resourcepackPath, + log: sendLog }) // 2-5. %appdata%/.minecraft/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장) diff --git a/src/installer-rp/pack.ts b/src/installer-rp/pack.ts index 5c8b054..47aa8b4 100644 --- a/src/installer-rp/pack.ts +++ b/src/installer-rp/pack.ts @@ -1,16 +1,10 @@ import { promises as fs, createWriteStream } from 'node:fs' import path from 'node:path' import archiver from 'archiver' +import { resolveResourcePackFormat } from './packFormat.js' const NAMESPACE = 'musicquiz' -/** - * 1.21 (pack_format 34) 를 기준으로 하되 supported_formats 로 1.21 ~ 1.21.11 - * 까지 받아들이도록 선언. 더 신 버전이 나오면 max_inclusive 만 올리면 됨. - */ -const RESOURCE_PACK_FORMAT = 34 -const SUPPORTED_FORMATS = { min_inclusive: 34, max_inclusive: 75 } - export interface BuildResourcepackOptions { /** ogg 음악 파일들이 들어 있는 폴더 (01.ogg, 02.ogg, …). */ musicDir: string @@ -18,10 +12,14 @@ export interface BuildResourcepackOptions { paintingDir: string /** pack.mcmeta 의 description 에 들어갈 표시 이름. */ packName: string + /** /manifest/.json 의 mcVersion. pack_format 결정용. */ + mcVersion: string /** 작업 폴더(임시). 이 안에 트리를 펼친 뒤 zip 생성. */ workDir: string /** 최종 zip 출력 경로. */ outZipPath: string + /** 진단용 로그 콜백 (선택). */ + log?: (line: string) => void } /** @@ -42,11 +40,19 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom await fs.mkdir(paintingOutDir, { recursive: true }) // 1) pack.mcmeta + // mcVersion 으로 resource pack format 을 결정. 알 수 없는 버전이면 가장 최근 + // 알려진 포맷으로 폴백 (resolveResourcePackFormat 가 알아서 처리). + const resolved = resolveResourcePackFormat(opts.mcVersion) + if (resolved.matched) { + opts.log?.(`pack_format = ${resolved.format} (mcVersion ${resolved.matched})`) + } else { + opts.log?.(`pack_format = ${resolved.format} (mcVersion "${opts.mcVersion}" 매칭 실패, 최신 폴백)`) + } const mcmeta = { pack: { description: `음악퀴즈 리소스팩 - ${opts.packName}`, - pack_format: RESOURCE_PACK_FORMAT, - supported_formats: SUPPORTED_FORMATS + pack_format: resolved.format, + supported_formats: { min_inclusive: resolved.format, max_inclusive: resolved.format } } } await fs.writeFile(path.join(root, 'pack.mcmeta'), JSON.stringify(mcmeta, null, 2) + '\n') diff --git a/src/installer-rp/packFormat.ts b/src/installer-rp/packFormat.ts new file mode 100644 index 0000000..537761f --- /dev/null +++ b/src/installer-rp/packFormat.ts @@ -0,0 +1,44 @@ +// Minecraft Java Edition 버전 → resource pack format 번호. +// 출처: https://minecraft.wiki/w/Pack_format (수동 동기화). +// 1.21.9 부터는 minor 버전(예: 69.0)이 도입됐지만 JSON Number 로 0 차이는 +// 표현되지 않으므로 정수만 사용한다. +const TABLE: Array = [ + ['1.21', 34], + ['1.21.1', 34], + ['1.21.2', 42], + ['1.21.3', 42], + ['1.21.4', 46], + ['1.21.5', 55], + ['1.21.6', 63], + ['1.21.7', 64], + ['1.21.8', 64], + ['1.21.9', 69], + ['1.21.10', 69], + ['1.21.11', 75], + ['26.1', 84], + ['26.1.1', 84], + ['26.1.2', 84], + ['26.2', 86] +] + +/** 테이블에서 마지막(=최신) 항목의 포맷. 알 수 없는 mcVersion 에 대한 폴백. */ +export const LATEST_KNOWN_FORMAT: number = TABLE[TABLE.length - 1][1] + +export interface ResolvedFormat { + /** 매칭된 mcVersion 키 (없으면 null). */ + matched: string | null + /** pack.mcmeta 에 들어갈 pack_format 값. */ + format: number +} + +/** + * mcVersion 문자열 ("1.21.6", "26.1.2", …) 에서 pack_format 을 찾는다. + * 정확히 일치하는 게 있으면 그 값, 없으면 가장 최근 알려진 포맷을 폴백. + */ +export function resolveResourcePackFormat(mcVersion: string): ResolvedFormat { + const key = (mcVersion || '').trim() + for (const [v, f] of TABLE) { + if (v === key) return { matched: v, format: f } + } + return { matched: null, format: LATEST_KNOWN_FORMAT } +} diff --git a/src/installer-rp/types.ts b/src/installer-rp/types.ts index ba5e659..cbf92b2 100644 --- a/src/installer-rp/types.ts +++ b/src/installer-rp/types.ts @@ -3,6 +3,8 @@ import type { PackList } from '../shared/types.js' export interface RpFetchedPack { key: string name: string + /** /manifest/.json 의 mcVersion (예: "1.21.6", "26.1.2"). */ + mcVersion: string /** /file/list/.json 의 음악·사진 목록. */ list: PackList }