From 9efd4a696a22aca16621b11ff35f47483fe039ba Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 23 May 2026 17:18:46 +0900 Subject: [PATCH] rp-pack: never overwrite base resourcepack sounds/paintings (v0.3.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- locales/installer-rp/ko-kr.json | 6 +++++ package.json | 2 +- src/installer-rp/pack.ts | 44 ++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/locales/installer-rp/ko-kr.json b/locales/installer-rp/ko-kr.json index 84c3eab..1caaf6b 100644 --- a/locales/installer-rp/ko-kr.json +++ b/locales/installer-rp/ko-kr.json @@ -106,6 +106,12 @@ "packFormatFallback": "pack_format = {{format}} (mcVersion \"{{version}}\" 매칭 실패, 최신 폴백)", "packFormatRange": "호환 범위 선언: pack_format {{min}} ~ {{max}} (supported_formats / min_format / max_format 모두 기록)", "soundsMerged": "기존 sounds.json 병합 ({{count}}개 항목)", + "tracksAdded": "음악 트랙 추가됨: {{count}}곡", + "trackCollisionSkipped": "베이스 리소스팩에 같은 트랙이 이미 있어 건너뜀: {{trackId}}", + "tracksCollisionSummary": "베이스 자산 보존을 위해 {{count}}개 트랙을 건너뜀: {{list}}", + "paintingsAdded": "사진 텍스처 추가됨: {{count}}장", + "paintingCollisionSkipped": "베이스 리소스팩에 같은 사진이 이미 있어 건너뜀: {{name}}", + "paintingsCollisionSummary": "베이스 자산 보존을 위해 {{count}}장 사진을 건너뜀: {{list}}", "ytdlpLine": "yt-dlp> {{line}}" }, "progress": { diff --git a/package.json b/package.json index 9f158d0..ad1e91c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.3.4", + "version": "0.3.5", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/installer-rp/pack.ts b/src/installer-rp/pack.ts index 76638d0..9c3a1bd 100644 --- a/src/installer-rp/pack.ts +++ b/src/installer-rp/pack.ts @@ -131,6 +131,10 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom opts.log?.(t('log.packFormatRange', { min: minFmt, max: maxFmt })) // 2) 음악 파일 복사 + sounds.json 생성/병합 + // 핵심 정책: 베이스 리소스팩에 이미 있는 자산은 절대 덮어쓰지 않는다. + // - 베이스 sounds.json 의 엔트리는 그대로 보존하고, 우리 트랙은 그 위에 "추가" 만 한다. + // - 베이스 sounds/track_NN.ogg 가 이미 있으면 덮어쓰지 않고 건너뛴다. + // - 키나 파일명이 충돌하면 우리 트랙을 스킵하고 로그로 알린다. const musicFiles = (await fs.readdir(opts.musicDir)) .filter((n) => n.toLowerCase().endsWith('.ogg')) .sort() @@ -147,28 +151,62 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom } catch { // 없으면 새로 생성. } + let musicAdded = 0 + const musicSkipped: string[] = [] for (const fname of musicFiles) { throwIfCancelled(cancel) // NN.ogg → track_NN.ogg 로 리네임해 패키지. const stem = path.basename(fname, path.extname(fname)) // "01" 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] = { sounds: [ { name: `${NAMESPACE}:${trackId}`, stream: true } ] } + musicAdded++ } 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) - // 3) painting 텍스처 복사 (이미 cover_NN.png 형태). 같은 파일명은 덮어씀. + // 3) painting 텍스처 복사 (이미 cover_NN.png 형태). + // 음악과 동일한 정책: 베이스에 같은 파일명이 이미 있으면 덮어쓰지 않고 건너뛴다. const paintingFiles = (await fs.readdir(opts.paintingDir)) .filter((n) => n.toLowerCase().endsWith('.png')) .sort() + let paintingAdded = 0 + const paintingSkipped: string[] = [] for (const fname of paintingFiles) { 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)