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:
2026-07-28 22:21:29 +09:00
parent f79074930f
commit 68e2312ce7
3 changed files with 61 additions and 3 deletions

View File

@@ -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'))