feat(installer-rp): use registered base resourcepack as overlay base
/manifest/<key>.json 의 resourcepackPath 가 비어있지 않으면 /file/resourcepacks/<path> 의 zip 을 받아 임시 폴더에 풀고, 그 위에 음악·사진·sounds.json·pack.mcmeta 를 얹어 최종 zip 을 만든다. - types.ts: RpFetchedPack 에 resourcepackPath 필드 추가 - main.ts: 로드 시 normalizePackDefinition 으로 resourcepackPath 캡처, 설치 시 베이스 zip 다운로드 → tempRoot/base.zip → buildResourcepackZip 에 전달 - pack.ts: baseZipPath 옵션 추가. extract-zip 으로 베이스 압축 해제 후 위에 얹기. sounds.json 은 기존 항목과 병합, pack.mcmeta 는 mcVersion 에 맞춰 항상 덮어씀. 베이스가 없으면(빈 문자열) 기존처럼 새 리소스팩을 처음부터 생성. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -188,10 +188,16 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi
|
||||
music: Array.isArray(listRaw.music) ? listRaw.music : [],
|
||||
images: Array.isArray(listRaw.images) ? listRaw.images : []
|
||||
}
|
||||
const mcVersion = packRaw
|
||||
? normalizePackDefinition(packRaw as Partial<PackDefinition>).mcVersion
|
||||
: ''
|
||||
results.push({ key: entry.file, name: entry.name || entry.file, mcVersion, list })
|
||||
const normalized = packRaw ? normalizePackDefinition(packRaw as Partial<PackDefinition>) : null
|
||||
const mcVersion = normalized?.mcVersion ?? ''
|
||||
const resourcepackPath = normalized?.resourcepackPath ?? ''
|
||||
results.push({
|
||||
key: entry.file,
|
||||
name: entry.name || entry.file,
|
||||
mcVersion,
|
||||
resourcepackPath,
|
||||
list
|
||||
})
|
||||
} catch (error) {
|
||||
sendLog(`목록 로드 실패 (${entry.file}): ${(error as Error).message}`)
|
||||
}
|
||||
@@ -334,13 +340,32 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
sendProgress({ phase: 'item', kind: 'image', index: idx, total: imageTotal, percent: 100, status: 'done' })
|
||||
}
|
||||
|
||||
// 2-4. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지)
|
||||
// 2-4. 베이스 리소스팩 다운로드 (있을 때만)
|
||||
throwIfCancelled()
|
||||
let baseZipPath: string | undefined
|
||||
if (pack.resourcepackPath) {
|
||||
const baseUrl = `${state.baseUrl}/file/resourcepacks/${pack.resourcepackPath.replace(/^\/+/, '')}`
|
||||
baseZipPath = path.join(tempRoot, 'base.zip')
|
||||
sendLog(`베이스 리소스팩 다운로드: ${pack.resourcepackPath}`)
|
||||
sendProgress({ phase: 'package', message: '베이스 리소스팩 다운로드 중' })
|
||||
try {
|
||||
const buf = await fetchBuffer(baseUrl)
|
||||
await fsp.writeFile(baseZipPath, buf)
|
||||
sendLog(`베이스 리소스팩 받음 (${(buf.length / 1024).toFixed(1)} KB)`)
|
||||
} catch (err) {
|
||||
throw new Error(`베이스 리소스팩 다운로드 실패: ${(err as Error).message}`)
|
||||
}
|
||||
} else {
|
||||
sendLog('베이스 리소스팩 없음 — 새 리소스팩으로 생성')
|
||||
}
|
||||
|
||||
// 2-5. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지, 베이스 위에 얹기)
|
||||
throwIfCancelled()
|
||||
const resourcepackName = `${state.selectedKey}_musicquiz.zip`
|
||||
const resourcepackDir = path.join(getAppDataDir(), '.minecraft', 'resourcepacks')
|
||||
const resourcepackPath = path.join(resourcepackDir, resourcepackName)
|
||||
sendLog(`리소스팩 zip 빌드 중… (${resourcepackName})`)
|
||||
sendProgress({ phase: 'package', message: 'zip 빌드 중' })
|
||||
sendProgress({ phase: 'package', message: baseZipPath ? '베이스에 음악·사진 추가 중' : 'zip 빌드 중' })
|
||||
await buildResourcepackZip({
|
||||
musicDir,
|
||||
paintingDir,
|
||||
@@ -348,10 +373,11 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
|
||||
mcVersion: pack.mcVersion,
|
||||
workDir: tempRoot,
|
||||
outZipPath: resourcepackPath,
|
||||
baseZipPath,
|
||||
log: sendLog
|
||||
})
|
||||
|
||||
// 2-5. %appdata%/.minecraft/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장)
|
||||
// 2-6. %appdata%/.minecraft/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장)
|
||||
sendLog(`설치 완료: ${resourcepackPath}`)
|
||||
sendProgress({ phase: 'package', message: '설치 완료', done: true })
|
||||
return { resourcepackPath }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { promises as fs, createWriteStream } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import archiver from 'archiver'
|
||||
import extract from 'extract-zip'
|
||||
import { resolveResourcePackFormat } from './packFormat.js'
|
||||
|
||||
const NAMESPACE = 'musicquiz'
|
||||
@@ -18,6 +19,11 @@ export interface BuildResourcepackOptions {
|
||||
workDir: string
|
||||
/** 최종 zip 출력 경로. */
|
||||
outZipPath: string
|
||||
/**
|
||||
* 베이스 리소스팩 zip 경로 (선택). 지정하면 이 zip 의 내용을 먼저 풀고
|
||||
* 그 위에 음악·사진·sounds.json·pack.mcmeta 를 덮어/병합한다.
|
||||
*/
|
||||
baseZipPath?: string
|
||||
/** 진단용 로그 콜백 (선택). */
|
||||
log?: (line: string) => void
|
||||
}
|
||||
@@ -33,15 +39,22 @@ export interface BuildResourcepackOptions {
|
||||
*/
|
||||
export async function buildResourcepackZip(opts: BuildResourcepackOptions): Promise<void> {
|
||||
const root = path.join(opts.workDir, 'resourcepack')
|
||||
// 베이스가 있건 없건 작업 트리는 항상 처음부터 다시 만든다.
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
await fs.mkdir(root, { recursive: true })
|
||||
|
||||
// 0) 베이스 리소스팩이 지정되면 먼저 풀어둔다. 그 위에 우리 파일을 얹는다.
|
||||
if (opts.baseZipPath) {
|
||||
opts.log?.(`베이스 리소스팩 압축 해제: ${path.basename(opts.baseZipPath)}`)
|
||||
await extract(opts.baseZipPath, { dir: root })
|
||||
}
|
||||
|
||||
const soundsDir = path.join(root, 'assets', NAMESPACE, 'sounds')
|
||||
const paintingOutDir = path.join(root, 'assets', NAMESPACE, 'textures', 'painting')
|
||||
await fs.mkdir(soundsDir, { recursive: true })
|
||||
await fs.mkdir(paintingOutDir, { recursive: true })
|
||||
|
||||
// 1) pack.mcmeta
|
||||
// mcVersion 으로 resource pack format 을 결정. 알 수 없는 버전이면 가장 최근
|
||||
// 알려진 포맷으로 폴백 (resolveResourcePackFormat 가 알아서 처리).
|
||||
// 1) pack.mcmeta 는 mcVersion 에 맞춰 항상 덮어쓴다 (베이스가 다른 버전일 수 있으니).
|
||||
const resolved = resolveResourcePackFormat(opts.mcVersion)
|
||||
if (resolved.matched) {
|
||||
opts.log?.(`pack_format = ${resolved.format} (mcVersion ${resolved.matched})`)
|
||||
@@ -57,11 +70,23 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
|
||||
}
|
||||
await fs.writeFile(path.join(root, 'pack.mcmeta'), JSON.stringify(mcmeta, null, 2) + '\n')
|
||||
|
||||
// 2) 음악 파일 복사 + sounds.json 생성
|
||||
// 2) 음악 파일 복사 + sounds.json 생성/병합
|
||||
const musicFiles = (await fs.readdir(opts.musicDir))
|
||||
.filter((n) => n.toLowerCase().endsWith('.ogg'))
|
||||
.sort()
|
||||
const soundsJson: Record<string, unknown> = {}
|
||||
// 베이스의 sounds.json 이 있으면 읽어서 우리 트랙을 덧붙인다.
|
||||
const soundsJsonPath = path.join(root, 'assets', NAMESPACE, 'sounds.json')
|
||||
let soundsJson: Record<string, unknown> = {}
|
||||
try {
|
||||
const existing = await fs.readFile(soundsJsonPath, 'utf8')
|
||||
const parsed = JSON.parse(existing)
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
soundsJson = parsed as Record<string, unknown>
|
||||
opts.log?.(`기존 sounds.json 병합 (${Object.keys(soundsJson).length}개 항목)`)
|
||||
}
|
||||
} catch {
|
||||
// 없으면 새로 생성.
|
||||
}
|
||||
for (const fname of musicFiles) {
|
||||
// NN.ogg → track_NN.ogg 로 리네임해 패키지.
|
||||
const stem = path.basename(fname, path.extname(fname)) // "01"
|
||||
@@ -73,12 +98,9 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
|
||||
]
|
||||
}
|
||||
}
|
||||
await fs.writeFile(
|
||||
path.join(root, 'assets', NAMESPACE, 'sounds.json'),
|
||||
JSON.stringify(soundsJson, null, 2) + '\n'
|
||||
)
|
||||
await fs.writeFile(soundsJsonPath, JSON.stringify(soundsJson, null, 2) + '\n')
|
||||
|
||||
// 3) painting 텍스처 복사 (이미 cover_NN.png 형태)
|
||||
// 3) painting 텍스처 복사 (이미 cover_NN.png 형태). 같은 파일명은 덮어씀.
|
||||
const paintingFiles = (await fs.readdir(opts.paintingDir))
|
||||
.filter((n) => n.toLowerCase().endsWith('.png'))
|
||||
.sort()
|
||||
|
||||
@@ -5,6 +5,11 @@ export interface RpFetchedPack {
|
||||
name: string
|
||||
/** /manifest/<key>.json 의 mcVersion (예: "1.21.6", "26.1.2"). */
|
||||
mcVersion: string
|
||||
/**
|
||||
* /manifest/<key>.json 의 resourcepackPath. 비어있지 않으면 베이스 zip 으로 사용.
|
||||
* 빈 문자열이면 새 리소스팩을 처음부터 생성.
|
||||
*/
|
||||
resourcepackPath: string
|
||||
/** /file/list/<key>.json 의 음악·사진 목록. */
|
||||
list: PackList
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user