diff --git a/installer/renderer.js b/installer/renderer.js index 4492478..aa243e9 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -629,8 +629,8 @@ function renderStep5() { // 마무리 액션 실패는 무시하고 종료 진행 } finishBtn.textContent = '완료됨' - // 모든 단계가 끝났으므로 설치기 종료 - if (installerApi.quitApp) installerApi.quitApp() + // 자동 종료는 임시 비활성화 (런처 실행 오류 메시지 확인용). 필요 시 X 버튼으로 직접 닫는다. + // if (installerApi.quitApp) installerApi.quitApp() }) } diff --git a/src/installer/main.ts b/src/installer/main.ts index 22517e2..2fda631 100644 --- a/src/installer/main.ts +++ b/src/installer/main.ts @@ -478,6 +478,12 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise 0 ? port : 25565 sendLog(`포트포워딩 점검 시작: 포트 ${targetPort}`) + // 1차 점검 전에 우리가 이전 실행에서 만든 UPnP 매핑이 남아 있으면 먼저 제거한다. + // 이렇게 해야 "사용자 라우터 규칙이 활성화돼서 외부 접근이 가능한 상태" 와 "UPnP 매핑 덕분에 접근 가능한 상태" 가 구별된다. + // 사용자 규칙이 비활성/없으면 1차 점검은 false 가 되어 UPnP 시도 단계로 자연스럽게 넘어간다. + sendLog('이전 실행의 UPnP 매핑이 남아 있으면 제거합니다(중복 방지)...') + await removeUpnpMapping(targetPort) + // 외부 IP 확보: 공용 API → 실패 시 UPnP 게이트웨이의 외부 IP로 폴백. let externalIp = await detectExternalIpHttp() if (externalIp) { @@ -496,10 +502,7 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise-`. + * platform.loaderVersion 이 비어 있으면 .minecraft/versions 에서 같은 mcVersion 의 폴더를 탐색. + * - forge / neoforge: 사용자 환경마다 폴더 명명이 다를 수 있어 일단 mcVersion 으로 폴백. + * 추후 정밀하게 잡으려면 mods loader installer 가 만든 실제 폴더명을 탐색해야 한다. + */ +function resolveLastVersionId(pack: PackDefinition): string { + if (pack.platform.type === 'vanilla') return pack.mcVersion + if (pack.platform.type === 'fabric') { + const loader = pack.platform.loaderVersion + if (loader) return `fabric-loader-${loader}-${pack.mcVersion}` + // loaderVersion 미지정: 실제 설치된 폴더 탐색. + try { + const versionsRoot = path.join(getAppDataDir(), '.minecraft', 'versions') + if (fs.existsSync(versionsRoot)) { + const entries = fs.readdirSync(versionsRoot) + const match = entries.find((entry) => + entry.startsWith('fabric-loader-') && entry.endsWith(`-${pack.mcVersion}`) + ) + if (match) return match + } + } catch { + // fall through + } + return pack.mcVersion // 폴백: vanilla 로 실행 시도 + } + // forge / neoforge: 가능한 후보 탐색. + try { + const versionsRoot = path.join(getAppDataDir(), '.minecraft', 'versions') + if (fs.existsSync(versionsRoot)) { + const entries = fs.readdirSync(versionsRoot) + const match = entries.find((entry) => + entry.toLowerCase().includes(pack.platform.type) && entry.includes(pack.mcVersion) + ) + if (match) return match + } + } catch { + // fall through + } + return pack.mcVersion +} + async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Promise { const launcherPath = path.join(getAppDataDir(), '.minecraft', 'launcher_profiles.json') if (!fs.existsSync(launcherPath)) { @@ -982,9 +1029,13 @@ async function updateLauncherProfile(pack: PackDefinition, gameDir: string): Pro if (existingJavaArgs && existingJavaArgs !== javaArgs) { sendLog(`기존 JVM 인수 유지, -Xmx 만 갱신: "${existingJavaArgs}" → "${javaArgs}"`) } - const lastVersionId = pack.platform.type === 'vanilla' - ? pack.mcVersion - : `${pack.mcVersion}-${pack.platform.type}` + const lastVersionId = resolveLastVersionId(pack) + sendLog(`launcher_profiles 의 lastVersionId = ${lastVersionId}`) + // 해당 version 폴더 존재 확인. 없으면 런처가 "Unable to prepare assets for download" 로 실패한다. + const versionDir = path.join(getAppDataDir(), '.minecraft', 'versions', lastVersionId) + if (!fs.existsSync(versionDir)) { + sendLog(`경고: .minecraft/versions/${lastVersionId} 가 없습니다. 마인크래프트 런처에서 해당 버전을 한 번 받아주거나, 플랫폼 설치를 먼저 마쳐주세요.`) + } json.profiles[profileKey] = { ...existingProfile, name: profileKey,