fix(installer): preserve JVM args + link runtime dirs from .minecraft

1) launcher_profiles.json 의 javaArgs 를 통째로 덮어쓰던 코드를 수정.
   mergeRamArgs() 로 -Xmx/-Xms 토큰만 새 값으로 교체하고 그 외
   사용자 추가 JVM 인수(-Xss, -XX:..., -Dfoo=bar 등)는 보존.

2) .mc_custom 을 gameDir 로 쓰면 마인크래프트 런처가 assets/libraries/
   versions 를 못 찾아 "Unable to prepare assets for download" 로 실패.
   linkMinecraftRuntimeDirs() 가 .minecraft 의 해당 세 폴더를
   .mc_custom 으로 junction(Windows) / symlink(POSIX) 연결. 이미 같은
   자리에 무언가 있으면 손대지 않음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 00:41:45 +09:00
parent 45540f3db7
commit 82307d9d16

View File

@@ -557,6 +557,10 @@ ipcMain.handle('client:install', async (_event, payload: ClientInstallPayload) =
await downloadMapZip(pack.pack, customRoot) await downloadMapZip(pack.pack, customRoot)
// 런처가 .mc_custom 을 gameDir 로 잡아도 assets/libraries/versions 를
// 찾을 수 있도록 .minecraft 의 해당 폴더로 junction 링크.
await linkMinecraftRuntimeDirs(customRoot)
await updateLauncherProfile(pack.pack, customRoot) await updateLauncherProfile(pack.pack, customRoot)
}) })
@@ -575,6 +579,26 @@ function getAppDataDir(): string {
return app.getPath('appData') return app.getPath('appData')
} }
/**
* 기존 javaArgs 에서 -Xmx/-Xms 토큰만 새 값으로 교체하고 나머지 args 는 보존한다.
* 기존에 없으면 새 RAM 인자를 앞에 붙인다.
*/
function mergeRamArgs(existing: string, maxMb: number, minMb: number): string {
const newXmx = `-Xmx${maxMb}M`
const newXms = `-Xms${minMb}M`
const tokens = (existing || '').split(/\s+/).filter(Boolean)
let foundXmx = false
let foundXms = false
const merged = tokens.map((t) => {
if (t.startsWith('-Xmx')) { foundXmx = true; return newXmx }
if (t.startsWith('-Xms')) { foundXms = true; return newXms }
return t
})
if (!foundXmx) merged.unshift(newXmx)
if (!foundXms) merged.splice(foundXmx ? 0 : 1, 0, newXms)
return merged.join(' ').trim()
}
async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Promise<void> { async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Promise<void> {
const launcherPath = path.join(getAppDataDir(), '.minecraft', 'launcher_profiles.json') const launcherPath = path.join(getAppDataDir(), '.minecraft', 'launcher_profiles.json')
if (!fs.existsSync(launcherPath)) { if (!fs.existsSync(launcherPath)) {
@@ -585,12 +609,17 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro
const json = JSON.parse(raw) as { profiles?: Record<string, Record<string, unknown>> } const json = JSON.parse(raw) as { profiles?: Record<string, Record<string, unknown>> }
json.profiles = json.profiles ?? {} json.profiles = json.profiles ?? {}
const profileKey = pack.name const profileKey = pack.name
const javaArgs = `-Xmx${pack.serverMaxRam}M -Xms${pack.serverMinRam}M` const existingProfile = json.profiles[profileKey] ?? {}
const existingJavaArgs = typeof existingProfile.javaArgs === 'string' ? (existingProfile.javaArgs as string) : ''
const javaArgs = mergeRamArgs(existingJavaArgs, pack.serverMaxRam, pack.serverMinRam)
if (existingJavaArgs && existingJavaArgs !== javaArgs) {
sendLog(`기존 JVM 인수 유지, RAM 만 갱신: "${existingJavaArgs}" → "${javaArgs}"`)
}
const lastVersionId = pack.platform.type === 'vanilla' const lastVersionId = pack.platform.type === 'vanilla'
? pack.mcVersion ? pack.mcVersion
: `${pack.mcVersion}-${pack.platform.type}` : `${pack.mcVersion}-${pack.platform.type}`
json.profiles[profileKey] = { json.profiles[profileKey] = {
...(json.profiles[profileKey] ?? {}), ...existingProfile,
name: profileKey, name: profileKey,
type: 'custom', type: 'custom',
gameDir, gameDir,
@@ -601,6 +630,41 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro
sendLog(`launcher_profiles.json 갱신: 프로필 "${profileKey}", gameDir=${gameDir}`) sendLog(`launcher_profiles.json 갱신: 프로필 "${profileKey}", gameDir=${gameDir}`)
} }
/**
* .mc_custom 에서 마인크래프트 런처가 찾는 assets/libraries/versions 를
* .minecraft 의 같은 폴더로 junction(Windows) / symlink(POSIX) 한다.
* 이미 같은 자리에 무언가 있으면 손대지 않는다.
*
* 이걸 안 하면 런처가 .mc_custom/assets 가 없다며 "Unable to prepare assets
* for download" 에러로 실행에 실패한다.
*/
async function linkMinecraftRuntimeDirs(customRoot: string): Promise<void> {
const mcRoot = path.join(getAppDataDir(), '.minecraft')
for (const dir of ['assets', 'libraries', 'versions']) {
const src = path.join(mcRoot, dir)
const dst = path.join(customRoot, dir)
if (!fs.existsSync(src)) {
sendLog(`.minecraft/${dir} 가 없습니다. 마인크래프트 런처를 한 번 실행한 뒤 다시 시도해주세요.`)
continue
}
let existing: import('node:fs').Stats | null = null
try { existing = await fsp.lstat(dst) } catch { existing = null }
if (existing) {
if (existing.isSymbolicLink()) continue // 이미 링크됨
sendLog(`.mc_custom/${dir} 가 실제 폴더로 이미 존재 — 건너뜀.`)
continue
}
try {
// 'junction' 은 Windows 에서 권한 없이 만들 수 있는 디렉터리 링크.
// 다른 OS 에서는 Node 가 알아서 일반 symlink 로 처리.
await fsp.symlink(src, dst, 'junction')
sendLog(`링크 생성: .mc_custom/${dir} → .minecraft/${dir}`)
} catch (err) {
sendLog(`링크 생성 실패 (${dir}): ${(err as Error).message}`)
}
}
}
ipcMain.handle('finish:openServerFolder', async () => { ipcMain.handle('finish:openServerFolder', async () => {
if (!state.installPath) return if (!state.installPath) return
await shell.openPath(state.installPath) await shell.openPath(state.installPath)