diff --git a/src/installer/main.ts b/src/installer/main.ts index 72877a3..e428d09 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -1379,39 +1379,78 @@ ipcMain.handle('finish:startServer', async () => { }) ipcMain.handle('finish:startLauncher', async () => { - // 1순위: minecraft:// URL 스킴. UWP(Microsoft Store) / Win32 / Xbox 앱 어떤 형태로 설치돼 - // 있어도 OS의 등록된 프로토콜 핸들러가 처리하므로 가장 견고하다. - try { - sendLog('마인크래프트 런처 실행 요청(URL 스킴 minecraft://)...') - await shell.openExternal('minecraft://') - sendLog('마인크래프트 런처 실행 요청 완료.') + // 마인크래프트 런처는 두 가지 형태로 배포된다: + // 1) Win32 설치판: C:\Program Files (x86)\Minecraft Launcher\MinecraftLauncher.exe 등 + // 2) MSIX(Microsoft Store) 앱: PackageFamilyName=Microsoft.4297127D64EC6_8wekyb3d8bbwe, AppId=Minecraft + // - 일반 .exe 가 아니라 shell:AppsFolder\! 또는 App Execution Alias 로만 띄울 수 있음. + // minecraft:// URL 스킴은 런처가 핸들러로 등록되어 있어야만 동작하고, 등록이 깨지거나 비어 있으면 + // MS Store 로 폴백되므로 가장 마지막 시도로 미룬다. + if (process.platform !== 'win32') { + try { + await shell.openExternal('minecraft://') + sendLog('마인크래프트 런처 실행 요청 완료(URL 스킴, 비-Windows).') + } catch (err) { + sendLog(`런처 실행 실패: ${(err as Error).message}`) + } return - } catch (err) { - sendLog(`URL 스킴 실행 실패: ${(err as Error).message}. 직접 경로 탐색으로 폴백합니다.`) } - // 2순위: 알려진 설치 경로 탐색. 구버전 .exe 설치판 / Xbox 앱 설치 위치까지 커버. const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)' const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files' const localAppData = process.env['LOCALAPPDATA'] ?? path.join(os.homedir(), 'AppData', 'Local') - const candidates = [ - path.join(programFilesX86, 'Minecraft Launcher', 'MinecraftLauncher.exe'), - path.join(programFiles, 'Minecraft Launcher', 'MinecraftLauncher.exe'), - path.join(programFilesX86, 'Minecraft', 'MinecraftLauncher.exe'), - path.join(programFiles, 'Minecraft', 'MinecraftLauncher.exe'), - 'C:\\XboxGames\\Minecraft Launcher\\Content\\Minecraft.exe', - path.join(localAppData, 'Programs', 'minecraft-launcher', 'MinecraftLauncher.exe') + + type LauncherCandidate = { label: string; path: string; viaShell: boolean } + const candidates: LauncherCandidate[] = [ + // Win32 설치판 — 실행 파일 직접 spawn. + { label: 'Win32 설치(Program Files (x86))', path: path.join(programFilesX86, 'Minecraft Launcher', 'MinecraftLauncher.exe'), viaShell: false }, + { label: 'Win32 설치(Program Files)', path: path.join(programFiles, 'Minecraft Launcher', 'MinecraftLauncher.exe'), viaShell: false }, + { label: 'Win32 설치(legacy Minecraft 폴더)', path: path.join(programFilesX86, 'Minecraft', 'MinecraftLauncher.exe'), viaShell: false }, + { label: 'Win32 설치(legacy Minecraft 폴더)', path: path.join(programFiles, 'Minecraft', 'MinecraftLauncher.exe'), viaShell: false }, + { label: 'Xbox / Game Pass', path: 'C:\\XboxGames\\Minecraft Launcher\\Content\\Minecraft.exe', viaShell: false }, + { label: 'npm/portable', path: path.join(localAppData, 'Programs', 'minecraft-launcher', 'MinecraftLauncher.exe'), viaShell: false }, + // App Execution Alias(MS Store 설치 시 자동 생성, reparse point 라 cmd /c start 로 띄워야 안정적). + { label: 'App Execution Alias(Minecraft.exe)', path: path.join(localAppData, 'Microsoft', 'WindowsApps', 'Minecraft.exe'), viaShell: true }, + { label: 'App Execution Alias(MinecraftLauncher.exe)', path: path.join(localAppData, 'Microsoft', 'WindowsApps', 'MinecraftLauncher.exe'), viaShell: true } ] - const target = candidates.find((candidate) => { - try { return fs.existsSync(candidate) } catch { return false } - }) - if (target) { - spawn(target, [], { detached: true, stdio: 'ignore' }).unref() - sendLog(`마인크래프트 런처 실행: ${target}`) - return + + for (const cand of candidates) { + let exists = false + try { exists = fs.existsSync(cand.path) } catch { exists = false } + if (!exists) continue + try { + if (cand.viaShell) { + sendLog(`마인크래프트 런처 실행(${cand.label}, 셸 경유): ${cand.path}`) + spawn('cmd.exe', ['/c', 'start', '', cand.path], { detached: true, stdio: 'ignore' }).unref() + } else { + sendLog(`마인크래프트 런처 실행(${cand.label}): ${cand.path}`) + spawn(cand.path, [], { detached: true, stdio: 'ignore' }).unref() + } + return + } catch (err) { + sendLog(`${cand.path} 실행 실패: ${(err as Error).message}`) + } } - sendLog('Minecraft Launcher를 찾을 수 없습니다. Microsoft Store에서 "Minecraft Launcher"를 설치하거나 minecraft.net에서 받은 뒤 직접 실행해 주세요.') + // MSIX 앱 직접 실행: shell:AppsFolder\!. + // 마인크래프트 런처(Java) PFN: Microsoft.4297127D64EC6_8wekyb3d8bbwe, AppId: Minecraft. + try { + const aumid = 'shell:AppsFolder\\Microsoft.4297127D64EC6_8wekyb3d8bbwe!Minecraft' + sendLog(`AppsFolder 로 MS Store 런처 실행 시도: ${aumid}`) + spawn('explorer.exe', [aumid], { detached: true, stdio: 'ignore' }).unref() + return + } catch (err) { + sendLog(`AppsFolder 실행 실패: ${(err as Error).message}`) + } + + // 마지막 수단: minecraft:// URL 스킴. 런처가 없으면 MS Store 가 열린다. + try { + sendLog('마지막 시도: minecraft:// URL 스킴 (런처가 없으면 MS Store 가 열릴 수 있음).') + await shell.openExternal('minecraft://') + } catch (err) { + sendLog(`URL 스킴 실행 실패: ${(err as Error).message}.`) + } + + sendLog('Minecraft Launcher 실행 시도가 모두 실패했습니다. minecraft.net 또는 Microsoft Store 에서 "Minecraft Launcher" 를 설치한 뒤 다시 시도해 주세요.') }) ipcMain.handle('app:quit', () => {