diff --git a/src/installer/main.ts b/src/installer/main.ts index 025d755..45e9077 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -557,6 +557,10 @@ ipcMain.handle('client:install', async (_event, payload: ClientInstallPayload) = await downloadMapZip(pack.pack, customRoot) + // 런처가 .mc_custom 을 gameDir 로 잡아도 assets/libraries/versions 를 + // 찾을 수 있도록 .minecraft 의 해당 폴더로 junction 링크. + await linkMinecraftRuntimeDirs(customRoot) + await updateLauncherProfile(pack.pack, customRoot) }) @@ -575,6 +579,26 @@ function getAppDataDir(): string { 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 { const launcherPath = path.join(getAppDataDir(), '.minecraft', 'launcher_profiles.json') if (!fs.existsSync(launcherPath)) { @@ -585,12 +609,17 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro const json = JSON.parse(raw) as { profiles?: Record> } json.profiles = json.profiles ?? {} 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' ? pack.mcVersion : `${pack.mcVersion}-${pack.platform.type}` json.profiles[profileKey] = { - ...(json.profiles[profileKey] ?? {}), + ...existingProfile, name: profileKey, type: 'custom', gameDir, @@ -601,6 +630,41 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro 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 { + 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 () => { if (!state.installPath) return await shell.openPath(state.installPath)