feat(installer): 최종 리소스팩 다운로드 후 자동 적용(options.txt)
최종 리소스팩을 받은 뒤 gameDir 의 options.txt resourcePacks 목록에 file/<이름> 을 추가해, 마인크래프트를 켜면 수동 선택 없이 자동 적용되게 한다(목록 마지막=최상위). options.txt 없으면 해당 한 줄만 생성, incompatibleResourcePacks 에 있으면 제거. 안내 문구도 "자동 적용" 으로 갱신. 0.4.22→0.4.23. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -162,12 +162,12 @@
|
|||||||
},
|
},
|
||||||
"resourcepack": {
|
"resourcepack": {
|
||||||
"promptHeading": "리소스팩 설치",
|
"promptHeading": "리소스팩 설치",
|
||||||
"promptBody": "이 음악퀴즈의 최종 리소스팩을 설치하시겠습니까? 설치 후에는 마인크래프트 내에서 직접 리소스팩을 적용해야 합니다.",
|
"promptBody": "이 음악퀴즈의 최종 리소스팩을 설치하시겠습니까? 설치하면 마인크래프트에서 자동으로 적용됩니다.",
|
||||||
"yes": "예",
|
"yes": "예",
|
||||||
"no": "아니요",
|
"no": "아니요",
|
||||||
"downloadHeading": "최종 리소스팩 다운로드",
|
"downloadHeading": "최종 리소스팩 다운로드",
|
||||||
"downloading": "최종 리소스팩을 다운로드하는 중…",
|
"downloading": "최종 리소스팩을 다운로드하는 중…",
|
||||||
"downloadDone": "최종 리소스팩 다운로드 완료. 마인크래프트 설정 → 리소스팩에서 직접 적용해 주세요.",
|
"downloadDone": "최종 리소스팩 다운로드 완료. 마인크래프트를 켜면 자동으로 적용됩니다.",
|
||||||
"downloadFailed": "최종 리소스팩 다운로드 실패: {{message}}"
|
"downloadFailed": "최종 리소스팩 다운로드 실패: {{message}}"
|
||||||
},
|
},
|
||||||
"step5": {
|
"step5": {
|
||||||
@@ -244,6 +244,8 @@
|
|||||||
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
|
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
|
||||||
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
|
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
|
||||||
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
|
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
|
||||||
|
"finalResourcepackApplied": "최종 리소스팩을 options.txt 에 등록해 자동 적용되게 했습니다: {{name}}",
|
||||||
|
"finalResourcepackApplyFail": "최종 리소스팩 자동 적용 실패(마인크래프트에서 직접 켜주세요): {{message}}",
|
||||||
"serverInstallPath": "서버 설치 경로: {{path}}",
|
"serverInstallPath": "서버 설치 경로: {{path}}",
|
||||||
"runBatJavaPatched": "run.bat 이 설치기가 준비한 자바를 쓰도록 수정했습니다: {{java}}",
|
"runBatJavaPatched": "run.bat 이 설치기가 준비한 자바를 쓰도록 수정했습니다: {{java}}",
|
||||||
"runBatJavaSkip": "설치기가 준비한 JDK 를 찾지 못해 run.bat 의 자바 경로는 그대로 둡니다(시스템 자바 사용).",
|
"runBatJavaSkip": "설치기가 준비한 JDK 를 찾지 못해 run.bat 의 자바 경로는 그대로 둡니다(시스템 자바 사용).",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "minecraft-music-quiz-installer",
|
"name": "minecraft-music-quiz-installer",
|
||||||
"version": "0.4.22",
|
"version": "0.4.23",
|
||||||
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
||||||
"main": "dist/installer/main.js",
|
"main": "dist/installer/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -306,6 +306,14 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
sendLog(t('log.finalResourcepackSaved', { path: dest }))
|
sendLog(t('log.finalResourcepackSaved', { path: dest }))
|
||||||
|
// 마인크래프트에서 직접 고르지 않아도 켜지도록 options.txt 의 resourcePacks 에 등록.
|
||||||
|
const gameDir = path.join(getAppDataDir(), getMcCustomDirName())
|
||||||
|
try {
|
||||||
|
await enableResourcePackInOptions(gameDir, cleaned)
|
||||||
|
sendLog(t('log.finalResourcepackApplied', { name: cleaned }))
|
||||||
|
} catch (err) {
|
||||||
|
sendLog(t('log.finalResourcepackApplyFail', { message: (err as Error).message }))
|
||||||
|
}
|
||||||
return { ok: true, path: dest }
|
return { ok: true, path: dest }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 실패 시 부분/0바이트 zip 이 마인크래프트 리소스팩 목록에 깨진 채로 남지 않도록 삭제.
|
// 실패 시 부분/0바이트 zip 이 마인크래프트 리소스팩 목록에 깨진 채로 남지 않도록 삭제.
|
||||||
@@ -314,6 +322,54 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* gameDir 의 options.txt 의 resourcePacks 목록에 `file/<packFileName>` 을 추가해,
|
||||||
|
* 마인크래프트를 켜면 해당 리소스팩이 자동으로 적용되게 한다. (목록 마지막 = 최상위 우선)
|
||||||
|
* options.txt 가 없으면 해당 한 줄만 새로 만든다(나머지 옵션은 게임이 기본값으로 채움).
|
||||||
|
* incompatibleResourcePacks 목록에 있으면 제거해 실제로 켜지게 한다.
|
||||||
|
*/
|
||||||
|
async function enableResourcePackInOptions(gameDir: string, packFileName: string): Promise<void> {
|
||||||
|
const optionsPath = path.join(gameDir, 'options.txt')
|
||||||
|
const entry = `file/${packFileName}`
|
||||||
|
let content = ''
|
||||||
|
try { content = await fsp.readFile(optionsPath, 'utf8') } catch { content = '' }
|
||||||
|
const lines = content.length ? content.split(/\r?\n/) : []
|
||||||
|
// 파일이 \n 으로 끝났으면 split 결과 마지막에 빈 문자열이 생기므로 제거.
|
||||||
|
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
||||||
|
|
||||||
|
const parseList = (line: string, key: string): string[] => {
|
||||||
|
const m = line.match(new RegExp(`^${key}:\\s*\\[(.*)\\]\\s*$`))
|
||||||
|
if (!m) return []
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(`[${m[1]}]`) as unknown[]
|
||||||
|
return arr.filter((x): x is string => typeof x === 'string')
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpIdx = lines.findIndex((l) => l.startsWith('resourcePacks:'))
|
||||||
|
if (rpIdx >= 0) {
|
||||||
|
const items = parseList(lines[rpIdx], 'resourcePacks')
|
||||||
|
if (!items.includes(entry)) items.push(entry)
|
||||||
|
lines[rpIdx] = `resourcePacks:${JSON.stringify(items)}`
|
||||||
|
} else {
|
||||||
|
lines.push(`resourcePacks:${JSON.stringify([entry])}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 호환불가 목록에 들어가 있으면 빼서 실제로 적용되게 한다.
|
||||||
|
const incIdx = lines.findIndex((l) => l.startsWith('incompatibleResourcePacks:'))
|
||||||
|
if (incIdx >= 0) {
|
||||||
|
const items = parseList(lines[incIdx], 'incompatibleResourcePacks')
|
||||||
|
const filtered = items.filter((x) => x !== entry)
|
||||||
|
if (filtered.length !== items.length) {
|
||||||
|
lines[incIdx] = `incompatibleResourcePacks:${JSON.stringify(filtered)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsp.writeFile(optionsPath, `${lines.join('\n')}\n`, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.handle('packs:select', async (_event, packKey: string) => {
|
ipcMain.handle('packs:select', async (_event, packKey: string) => {
|
||||||
if (!state.packs.has(packKey)) {
|
if (!state.packs.has(packKey)) {
|
||||||
throw new Error(t('errors.packNotFound'))
|
throw new Error(t('errors.packNotFound'))
|
||||||
|
|||||||
Reference in New Issue
Block a user