From 5c13648f636cc6d664912836dfe7899bcb8627f8 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 23 May 2026 17:26:41 +0900 Subject: [PATCH] rp-pack: fail-fast on base track/painting collision (was: silent skip) Reviewer correctly flagged that the previous skip-on-collision behavior silently drops new quiz tracks when the base resourcepack already has the same track_NN key. That makes the install LOOK successful but breaks the quiz at runtime (datapack references the missing track). The new behavior throws a clear error explaining which key collided and what the user must do (remove the conflicting base entry, or use a different base). The base assets are still preserved (we never overwrite); we just refuse to build a broken pack. Removed the now-unused skip-summary log keys. --- locales/installer-rp/ko-kr.json | 8 +++----- src/installer-rp/pack.ts | 31 +++++++++---------------------- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/locales/installer-rp/ko-kr.json b/locales/installer-rp/ko-kr.json index 1caaf6b..bff65a8 100644 --- a/locales/installer-rp/ko-kr.json +++ b/locales/installer-rp/ko-kr.json @@ -107,11 +107,7 @@ "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": { @@ -145,6 +141,8 @@ "ytdlpInstallFailed": "yt-dlp.exe 자동 설치 실패: {{message}}", "ffmpegNotInZip": "zip 내부에서 ffmpeg.exe 를 찾을 수 없습니다.", "ffmpegVerifyFailed": "ffmpeg.exe 다운로드는 됐지만 실행 검증에 실패했습니다.", - "ffmpegInstallFailed": "ffmpeg.exe 자동 설치 실패: {{message}}" + "ffmpegInstallFailed": "ffmpeg.exe 자동 설치 실패: {{message}}", + "baseTrackCollision": "베이스 리소스팩에 같은 트랙 ID 가 이미 있어 설치를 중단합니다: {{trackId}}\n베이스 자산을 보존하면서 새 트랙을 같은 ID 로 추가할 수 없습니다. 베이스의 sounds.json 엔트리/sounds 폴더에서 충돌하는 항목을 제거하거나 다른 베이스를 사용하세요.", + "basePaintingCollision": "베이스 리소스팩에 같은 사진 파일이 이미 있어 설치를 중단합니다: {{name}}\n베이스의 painting 텍스처를 보존하면서 같은 파일명을 추가할 수 없습니다. 베이스에서 충돌하는 파일을 제거하거나 다른 베이스를 사용하세요." } } diff --git a/src/installer-rp/pack.ts b/src/installer-rp/pack.ts index 9c3a1bd..41a6cc0 100644 --- a/src/installer-rp/pack.ts +++ b/src/installer-rp/pack.ts @@ -151,23 +151,22 @@ 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}` const destFile = path.join(soundsDir, `${trackId}.ogg`) - // 베이스에 같은 trackId 의 엔트리/파일이 있으면 보존 (덮어쓰지 않음). + // 베이스에 같은 trackId 의 엔트리/파일이 있으면 두 선택지 다 깨진다: + // (a) 덮어쓰면 베이스의 기존 곡이 사라지고, + // (b) 새 곡을 스킵하면 데이터팩이 가리키는 곡이 빠진 채로 설치된다. + // 안전하게 설치를 즉시 실패시키고 어떤 키가 충돌했는지 알린다. 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 + throw new Error(t('errors.baseTrackCollision', { trackId })) } await fs.copyFile(path.join(opts.musicDir, fname), destFile) soundsJson[trackId] = { @@ -175,39 +174,27 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom { 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(', ') })) - } + opts.log?.(t('log.tracksAdded', { count: musicFiles.length })) throwIfCancelled(cancel) // 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) 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 + throw new Error(t('errors.basePaintingCollision', { name: fname })) } 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(', ') })) } + opts.log?.(t('log.paintingsAdded', { count: paintingFiles.length })) throwIfCancelled(cancel) // 4) zip 으로 묶기. 이 단계가 가장 길어서 별도로 cancel 폴링이 들어간다.