Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c13648f63 | |||
| 9efd4a696a |
@@ -106,6 +106,8 @@
|
|||||||
"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}}곡",
|
||||||
|
"paintingsAdded": "사진 텍스처 추가됨: {{count}}장",
|
||||||
"ytdlpLine": "yt-dlp> {{line}}"
|
"ytdlpLine": "yt-dlp> {{line}}"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
@@ -139,6 +141,8 @@
|
|||||||
"ytdlpInstallFailed": "yt-dlp.exe 자동 설치 실패: {{message}}",
|
"ytdlpInstallFailed": "yt-dlp.exe 자동 설치 실패: {{message}}",
|
||||||
"ffmpegNotInZip": "zip 내부에서 ffmpeg.exe 를 찾을 수 없습니다.",
|
"ffmpegNotInZip": "zip 내부에서 ffmpeg.exe 를 찾을 수 없습니다.",
|
||||||
"ffmpegVerifyFailed": "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 텍스처를 보존하면서 같은 파일명을 추가할 수 없습니다. 베이스에서 충돌하는 파일을 제거하거나 다른 베이스를 사용하세요."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -152,7 +156,19 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
|
|||||||
// 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 의 엔트리/파일이 있으면 두 선택지 다 깨진다:
|
||||||
|
// (a) 덮어쓰면 베이스의 기존 곡이 사라지고,
|
||||||
|
// (b) 새 곡을 스킵하면 데이터팩이 가리키는 곡이 빠진 채로 설치된다.
|
||||||
|
// 안전하게 설치를 즉시 실패시키고 어떤 키가 충돌했는지 알린다.
|
||||||
|
let collides = soundsJson[trackId] !== undefined
|
||||||
|
if (!collides) {
|
||||||
|
try { await fs.access(destFile); collides = true } catch { /* 없음 → OK */ }
|
||||||
|
}
|
||||||
|
if (collides) {
|
||||||
|
throw new Error(t('errors.baseTrackCollision', { trackId }))
|
||||||
|
}
|
||||||
|
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 }
|
||||||
@@ -160,16 +176,25 @@ export async function buildResourcepackZip(opts: BuildResourcepackOptions): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
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: musicFiles.length }))
|
||||||
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()
|
||||||
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) {
|
||||||
|
throw new Error(t('errors.basePaintingCollision', { name: fname }))
|
||||||
}
|
}
|
||||||
|
await fs.copyFile(path.join(opts.paintingDir, fname), destFile)
|
||||||
|
}
|
||||||
|
opts.log?.(t('log.paintingsAdded', { count: paintingFiles.length }))
|
||||||
throwIfCancelled(cancel)
|
throwIfCancelled(cancel)
|
||||||
|
|
||||||
// 4) zip 으로 묶기. 이 단계가 가장 길어서 별도로 cancel 폴링이 들어간다.
|
// 4) zip 으로 묶기. 이 단계가 가장 길어서 별도로 cancel 폴링이 들어간다.
|
||||||
|
|||||||
Reference in New Issue
Block a user