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
}