diff --git a/locales/installer/ko-kr.json b/locales/installer/ko-kr.json index e8ab754..b52686e 100644 --- a/locales/installer/ko-kr.json +++ b/locales/installer/ko-kr.json @@ -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 의 자바 경로는 그대로 둡니다(시스템 자바 사용).", diff --git a/package.json b/package.json index 63b2acb..1793b10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-music-quiz-installer", - "version": "0.4.22", + "version": "0.4.23", "description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트", "main": "dist/installer/main.js", "scripts": { diff --git a/src/installer/main.ts b/src/installer/main.ts index 1b2ed8e..9be1219 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -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/` 을 추가해, + * 마인크래프트를 켜면 해당 리소스팩이 자동으로 적용되게 한다. (목록 마지막 = 최상위 우선) + * options.txt 가 없으면 해당 한 줄만 새로 만든다(나머지 옵션은 게임이 기본값으로 채움). + * incompatibleResourcePacks 목록에 있으면 제거해 실제로 켜지게 한다. + */ +async function enableResourcePackInOptions(gameDir: string, packFileName: string): Promise { + 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'))