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": {
|
||||
"promptHeading": "리소스팩 설치",
|
||||
"promptBody": "이 음악퀴즈의 최종 리소스팩을 설치하시겠습니까? 설치 후에는 마인크래프트 내에서 직접 리소스팩을 적용해야 합니다.",
|
||||
"promptBody": "이 음악퀴즈의 최종 리소스팩을 설치하시겠습니까? 설치하면 마인크래프트에서 자동으로 적용됩니다.",
|
||||
"yes": "예",
|
||||
"no": "아니요",
|
||||
"downloadHeading": "최종 리소스팩 다운로드",
|
||||
"downloading": "최종 리소스팩을 다운로드하는 중…",
|
||||
"downloadDone": "최종 리소스팩 다운로드 완료. 마인크래프트 설정 → 리소스팩에서 직접 적용해 주세요.",
|
||||
"downloadDone": "최종 리소스팩 다운로드 완료. 마인크래프트를 켜면 자동으로 적용됩니다.",
|
||||
"downloadFailed": "최종 리소스팩 다운로드 실패: {{message}}"
|
||||
},
|
||||
"step5": {
|
||||
@@ -244,6 +244,8 @@
|
||||
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
|
||||
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
|
||||
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
|
||||
"finalResourcepackApplied": "최종 리소스팩을 options.txt 에 등록해 자동 적용되게 했습니다: {{name}}",
|
||||
"finalResourcepackApplyFail": "최종 리소스팩 자동 적용 실패(마인크래프트에서 직접 켜주세요): {{message}}",
|
||||
"serverInstallPath": "서버 설치 경로: {{path}}",
|
||||
"runBatJavaPatched": "run.bat 이 설치기가 준비한 자바를 쓰도록 수정했습니다: {{java}}",
|
||||
"runBatJavaSkip": "설치기가 준비한 JDK 를 찾지 못해 run.bat 의 자바 경로는 그대로 둡니다(시스템 자바 사용).",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minecraft-music-quiz-installer",
|
||||
"version": "0.4.22",
|
||||
"version": "0.4.23",
|
||||
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
|
||||
"main": "dist/installer/main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -306,6 +306,14 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
|
||||
}
|
||||
})
|
||||
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 }
|
||||
} catch (error) {
|
||||
// 실패 시 부분/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) => {
|
||||
if (!state.packs.has(packKey)) {
|
||||
throw new Error(t('errors.packNotFound'))
|
||||
|
||||
Reference in New Issue
Block a user