rp-pack: never overwrite base resourcepack sounds/paintings (v0.3.5)

If the base resourcepack already has audio files under
assets/musicquiz/sounds/ or entries in assets/musicquiz/sounds.json,
the build now PRESERVES them and skips any new track that would
collide. Same policy for painting textures: existing cover_*.png
in the base are not overwritten by new ones.

Per-track collision is logged so the user can see exactly what was
preserved and what was skipped. Summary counts (added / skipped)
are also logged.

Requested by 사금향: "기존에 있는걸 삭제하거나 이상하게 엎어쓰지
말것" — preserve base assets unconditionally.
This commit is contained in:
2026-05-23 17:18:46 +09:00
parent c580a50fd4
commit 9efd4a696a
3 changed files with 48 additions and 4 deletions

View File

@@ -106,6 +106,12 @@
"packFormatFallback": "pack_format = {{format}} (mcVersion \"{{version}}\" 매칭 실패, 최신 폴백)", "packFormatFallback": "pack_format = {{format}} (mcVersion \"{{version}}\" 매칭 실패, 최신 폴백)",
"packFormatRange": "호환 범위 선언: pack_format {{min}} ~ {{max}} (supported_formats / min_format / max_format 모두 기록)", "packFormatRange": "호환 범위 선언: pack_format {{min}} ~ {{max}} (supported_formats / min_format / max_format 모두 기록)",
"soundsMerged": "기존 sounds.json 병합 ({{count}}개 항목)", "soundsMerged": "기존 sounds.json 병합 ({{count}}개 항목)",
"tracksAdded": "음악 트랙 추가됨: {{count}}곡",
"trackCollisionSkipped": "베이스 리소스팩에 같은 트랙이 이미 있어 건너뜀: {{trackId}}",
"tracksCollisionSummary": "베이스 자산 보존을 위해 {{count}}개 트랙을 건너뜀: {{list}}",
"paintingsAdded": "사진 텍스처 추가됨: {{count}}장",
"paintingCollisionSkipped": "베이스 리소스팩에 같은 사진이 이미 있어 건너뜀: {{name}}",
"paintingsCollisionSummary": "베이스 자산 보존을 위해 {{count}}장 사진을 건너뜀: {{list}}",
"ytdlpLine": "yt-dlp> {{line}}" "ytdlpLine": "yt-dlp> {{line}}"
}, },
"progress": { "progress": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "minecraft-music-quiz-installer", "name": "minecraft-music-quiz-installer",
"version": "0.3.4", "version": "0.3.5",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js", "main": "dist/installer/main.js",
"scripts": { "scripts": {

View File

@@ -131,6 +131,10 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
opts.log?.(t('log.packFormatRange', { min: minFmt, max: maxFmt })) opts.log?.(t('log.packFormatRange', { min: minFmt, max: maxFmt }))
// 2) 음악 파일 복사 + sounds.json 생성/병합 // 2) 음악 파일 복사 + sounds.json 생성/병합
// 핵심 정책: 베이스 리소스팩에 이미 있는 자산은 절대 덮어쓰지 않는다.
// - 베이스 sounds.json 의 엔트리는 그대로 보존하고, 우리 트랙은 그 위에 "추가" 만 한다.
// - 베이스 sounds/track_NN.ogg 가 이미 있으면 덮어쓰지 않고 건너뛴다.
// - 키나 파일명이 충돌하면 우리 트랙을 스킵하고 로그로 알린다.
const musicFiles = (await fs.readdir(opts.musicDir)) const musicFiles = (await fs.readdir(opts.musicDir))
.filter((n) => n.toLowerCase().endsWith('.ogg')) .filter((n) => n.toLowerCase().endsWith('.ogg'))
.sort() .sort()
@@ -147,28 +151,62 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
} catch { } catch {
// 없으면 새로 생성. // 없으면 새로 생성.
} }
let musicAdded = 0
const musicSkipped: string[] = []
for (const fname of musicFiles) { for (const fname of musicFiles) {
throwIfCancelled(cancel) throwIfCancelled(cancel)
// NN.ogg → track_NN.ogg 로 리네임해 패키지. // NN.ogg → track_NN.ogg 로 리네임해 패키지.
const stem = path.basename(fname, path.extname(fname)) // "01" const stem = path.basename(fname, path.extname(fname)) // "01"
const trackId = `track_${stem}` const trackId = `track_${stem}`
await fs.copyFile(path.join(opts.musicDir, fname), path.join(soundsDir, `${trackId}.ogg`)) const destFile = path.join(soundsDir, `${trackId}.ogg`)
// 베이스에 같은 trackId 의 엔트리/파일이 있으면 보존 (덮어쓰지 않음).
let collides = soundsJson[trackId] !== undefined
if (!collides) {
try { await fs.access(destFile); collides = true } catch { /* 없음 → OK */ }
}
if (collides) {
musicSkipped.push(trackId)
opts.log?.(t('log.trackCollisionSkipped', { trackId }))
continue
}
await fs.copyFile(path.join(opts.musicDir, fname), destFile)
soundsJson[trackId] = { soundsJson[trackId] = {
sounds: [ sounds: [
{ name: `${NAMESPACE}:${trackId}`, stream: true } { name: `${NAMESPACE}:${trackId}`, stream: true }
] ]
} }
musicAdded++
} }
await fs.writeFile(soundsJsonPath, JSON.stringify(soundsJson, null, 2) + '\n') await fs.writeFile(soundsJsonPath, JSON.stringify(soundsJson, null, 2) + '\n')
opts.log?.(t('log.tracksAdded', { count: musicAdded }))
if (musicSkipped.length > 0) {
opts.log?.(t('log.tracksCollisionSummary', { count: musicSkipped.length, list: musicSkipped.join(', ') }))
}
throwIfCancelled(cancel) throwIfCancelled(cancel)
// 3) painting 텍스처 복사 (이미 cover_NN.png 형태). 같은 파일명은 덮어씀. // 3) painting 텍스처 복사 (이미 cover_NN.png 형태).
// 음악과 동일한 정책: 베이스에 같은 파일명이 이미 있으면 덮어쓰지 않고 건너뛴다.
const paintingFiles = (await fs.readdir(opts.paintingDir)) const paintingFiles = (await fs.readdir(opts.paintingDir))
.filter((n) => n.toLowerCase().endsWith('.png')) .filter((n) => n.toLowerCase().endsWith('.png'))
.sort() .sort()
let paintingAdded = 0
const paintingSkipped: string[] = []
for (const fname of paintingFiles) { for (const fname of paintingFiles) {
throwIfCancelled(cancel) throwIfCancelled(cancel)
await fs.copyFile(path.join(opts.paintingDir, fname), path.join(paintingOutDir, fname)) const destFile = path.join(paintingOutDir, fname)
let collides = false
try { await fs.access(destFile); collides = true } catch { /* 없음 → OK */ }
if (collides) {
paintingSkipped.push(fname)
opts.log?.(t('log.paintingCollisionSkipped', { name: fname }))
continue
}
await fs.copyFile(path.join(opts.paintingDir, fname), destFile)
paintingAdded++
}
opts.log?.(t('log.paintingsAdded', { count: paintingAdded }))
if (paintingSkipped.length > 0) {
opts.log?.(t('log.paintingsCollisionSummary', { count: paintingSkipped.length, list: paintingSkipped.join(', ') }))
} }
throwIfCancelled(cancel) throwIfCancelled(cancel)