fix(installer-rp): URL-encode base pack path, output to .mc_custom + installer: no auto -Xms

리소스팩 간편설치기:
- 베이스 리소스팩 다운로드 URL 에 encodeURIComponent 적용. "Puzzle Resource
  Pack (basic).zip" 같이 공백·괄호가 들어간 파일명 정상 처리.
- 출력 경로를 %appdata%/.minecraft/resourcepacks/ → %appdata%/.mc_custom/
  resourcepacks/ 로 변경 (renderer 안내문, openFolder, 빌드 출력 일괄).
- 로드 직후 각 음악퀴즈의 베이스 등록 여부를 로그에 노출 (디버그용).
- 베이스 다운로드 시 실제 URL 도 로그에 출력.

음악퀴즈 간편설치기:
- mergeRamArgs: -Xms 가 기존에 없으면 추가하지 않도록 수정. clientMinRam
  은 "유저 PC 사양 최소 요구치" 의미이지 JVM 초기 힙이 아님. -Xmx 는
  계속 추천 RAM 으로 강제 갱신.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 00:48:46 +09:00
parent 82307d9d16
commit 5a018bcb8d
4 changed files with 22 additions and 18 deletions

View File

@@ -117,7 +117,7 @@ function renderStep2() {
section.innerHTML = section.innerHTML =
'<h2>2단계. 리소스팩 설치</h2>' + '<h2>2단계. 리소스팩 설치</h2>' +
'<p class="formMessage">음악·사진을 받아 리소스팩을 만들고 ' + '<p class="formMessage">음악·사진을 받아 리소스팩을 만들고 ' +
'<code>%appdata%/.minecraft/resourcepacks/</code> 에 자동 설치합니다.</p>' + '<code>%appdata%/.mc_custom/resourcepacks/</code> 에 자동 설치합니다.</p>' +
'<div class="prepRow">' + '<div class="prepRow">' +
' <span class="prepChip" id="chip-ytdlp">yt-dlp 준비</span>' + ' <span class="prepChip" id="chip-ytdlp">yt-dlp 준비</span>' +
' <span class="prepChip" id="chip-ffmpeg">ffmpeg 준비</span>' + ' <span class="prepChip" id="chip-ffmpeg">ffmpeg 준비</span>' +

View File

@@ -205,6 +205,9 @@ ipcMain.handle('rp:packs:load', async (_event, manifestUrlInput?: string): Promi
state.packs.clear() state.packs.clear()
for (const item of results) state.packs.set(item.key, item) for (const item of results) state.packs.set(item.key, item)
sendLog(`로드된 음악퀴즈: ${results.length}`) sendLog(`로드된 음악퀴즈: ${results.length}`)
for (const item of results) {
sendLog(` - ${item.key}: mc=${item.mcVersion || '?'} 베이스=${item.resourcepackPath || '(없음)'}`)
}
return results return results
}) })
@@ -344,9 +347,12 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
throwIfCancelled() throwIfCancelled()
let baseZipPath: string | undefined let baseZipPath: string | undefined
if (pack.resourcepackPath) { if (pack.resourcepackPath) {
const baseUrl = `${state.baseUrl}/file/resourcepacks/${pack.resourcepackPath.replace(/^\/+/, '')}` // 파일명에 공백·괄호가 있을 수 있어 encodeURIComponent 로 인코딩.
const cleaned = pack.resourcepackPath.replace(/^\/+/, '')
const baseUrl = `${state.baseUrl}/file/resourcepacks/${encodeURIComponent(cleaned)}`
baseZipPath = path.join(tempRoot, 'base.zip') baseZipPath = path.join(tempRoot, 'base.zip')
sendLog(`베이스 리소스팩 다운로드: ${pack.resourcepackPath}`) sendLog(`베이스 리소스팩 다운로드: ${cleaned}`)
sendLog(` URL: ${baseUrl}`)
sendProgress({ phase: 'package', message: '베이스 리소스팩 다운로드 중' }) sendProgress({ phase: 'package', message: '베이스 리소스팩 다운로드 중' })
try { try {
const buf = await fetchBuffer(baseUrl) const buf = await fetchBuffer(baseUrl)
@@ -356,13 +362,13 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
throw new Error(`베이스 리소스팩 다운로드 실패: ${(err as Error).message}`) throw new Error(`베이스 리소스팩 다운로드 실패: ${(err as Error).message}`)
} }
} else { } else {
sendLog('베이스 리소스팩 없음 — 새 리소스팩으로 생성') sendLog('베이스 리소스팩 없음(resourcepackPath 빈 값) — 새 리소스팩으로 생성')
} }
// 2-5. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지, 베이스 위에 얹기) // 2-5. 리소스팩 zip 빌드 (pack.mcmeta + sounds.json + 음악·이미지, 베이스 위에 얹기)
throwIfCancelled() throwIfCancelled()
const resourcepackName = `${state.selectedKey}_musicquiz.zip` const resourcepackName = `${state.selectedKey}_musicquiz.zip`
const resourcepackDir = path.join(getAppDataDir(), '.minecraft', 'resourcepacks') const resourcepackDir = path.join(getMcCustomDir(), 'resourcepacks')
const resourcepackPath = path.join(resourcepackDir, resourcepackName) const resourcepackPath = path.join(resourcepackDir, resourcepackName)
sendLog(`리소스팩 zip 빌드 중… (${resourcepackName})`) sendLog(`리소스팩 zip 빌드 중… (${resourcepackName})`)
sendProgress({ phase: 'package', message: baseZipPath ? '베이스에 음악·사진 추가 중' : 'zip 빌드 중' }) sendProgress({ phase: 'package', message: baseZipPath ? '베이스에 음악·사진 추가 중' : 'zip 빌드 중' })
@@ -377,7 +383,7 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
log: sendLog log: sendLog
}) })
// 2-6. %appdata%/.minecraft/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장) // 2-6. %appdata%/.mc_custom/resourcepacks/ 에 배치 (위 빌드가 직접 outZipPath 에 저장)
sendLog(`설치 완료: ${resourcepackPath}`) sendLog(`설치 완료: ${resourcepackPath}`)
sendProgress({ phase: 'package', message: '설치 완료', done: true }) sendProgress({ phase: 'package', message: '설치 완료', done: true })
return { resourcepackPath } return { resourcepackPath }
@@ -403,7 +409,7 @@ function throwIfCancelled(): void {
// ── IPC: 3단계 완료 ────────────────────────────────── // ── IPC: 3단계 완료 ──────────────────────────────────
ipcMain.handle('rp:finish:openFolder', async () => { ipcMain.handle('rp:finish:openFolder', async () => {
const dir = path.join(getAppDataDir(), '.minecraft', 'resourcepacks') const dir = path.join(getMcCustomDir(), 'resourcepacks')
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
await fsp.mkdir(dir, { recursive: true }) await fsp.mkdir(dir, { recursive: true })
} }

View File

@@ -16,7 +16,7 @@ const api = {
cancelInstall: (): Promise<void> => cancelInstall: (): Promise<void> =>
ipcRenderer.invoke('rp:install:cancel'), ipcRenderer.invoke('rp:install:cancel'),
/** %appdata%/.minecraft/resourcepacks/ 폴더를 OS 파일 탐색기로 연다. */ /** %appdata%/.mc_custom/resourcepacks/ 폴더를 OS 파일 탐색기로 연다. */
openResourcepackFolder: (): Promise<void> => openResourcepackFolder: (): Promise<void> =>
ipcRenderer.invoke('rp:finish:openFolder'), ipcRenderer.invoke('rp:finish:openFolder'),
/** 프로그램 종료. */ /** 프로그램 종료. */

View File

@@ -580,22 +580,20 @@ function getAppDataDir(): string {
} }
/** /**
* 기존 javaArgs 에서 -Xmx/-Xms 토큰만 새 값으로 교체하고 나머지 args 는 보존한다. * 기존 javaArgs 에서 RAM 토큰만 새 값으로 교체하고 나머지 args 는 보존한다.
* 기존에 없으면 새 RAM 인자를 앞에 붙인다. * - -Xmx: 항상 추천 RAM 으로 설정 (없으면 추가).
* - -Xms: 기존에 있을 때만 교체. 없으면 추가하지 않음.
* (clientMinRam 은 "유저 PC 사양 최소 요구치" 의미이지 JVM 초기 힙이 아님)
*/ */
function mergeRamArgs(existing: string, maxMb: number, minMb: number): string { function mergeRamArgs(existing: string, recommendedMb: number): string {
const newXmx = `-Xmx${maxMb}M` const newXmx = `-Xmx${recommendedMb}M`
const newXms = `-Xms${minMb}M`
const tokens = (existing || '').split(/\s+/).filter(Boolean) const tokens = (existing || '').split(/\s+/).filter(Boolean)
let foundXmx = false let foundXmx = false
let foundXms = false
const merged = tokens.map((t) => { const merged = tokens.map((t) => {
if (t.startsWith('-Xmx')) { foundXmx = true; return newXmx } if (t.startsWith('-Xmx')) { foundXmx = true; return newXmx }
if (t.startsWith('-Xms')) { foundXms = true; return newXms }
return t return t
}) })
if (!foundXmx) merged.unshift(newXmx) if (!foundXmx) merged.unshift(newXmx)
if (!foundXms) merged.splice(foundXmx ? 0 : 1, 0, newXms)
return merged.join(' ').trim() return merged.join(' ').trim()
} }
@@ -611,9 +609,9 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro
const profileKey = pack.name const profileKey = pack.name
const existingProfile = json.profiles[profileKey] ?? {} const existingProfile = json.profiles[profileKey] ?? {}
const existingJavaArgs = typeof existingProfile.javaArgs === 'string' ? (existingProfile.javaArgs as string) : '' const existingJavaArgs = typeof existingProfile.javaArgs === 'string' ? (existingProfile.javaArgs as string) : ''
const javaArgs = mergeRamArgs(existingJavaArgs, pack.serverMaxRam, pack.serverMinRam) const javaArgs = mergeRamArgs(existingJavaArgs, pack.serverMaxRam)
if (existingJavaArgs && existingJavaArgs !== javaArgs) { if (existingJavaArgs && existingJavaArgs !== javaArgs) {
sendLog(`기존 JVM 인수 유지, RAM 만 갱신: "${existingJavaArgs}" → "${javaArgs}"`) sendLog(`기존 JVM 인수 유지, -Xmx 만 갱신: "${existingJavaArgs}" → "${javaArgs}"`)
} }
const lastVersionId = pack.platform.type === 'vanilla' const lastVersionId = pack.platform.type === 'vanilla'
? pack.mcVersion ? pack.mcVersion