From b407a2ca6a59adf3b83bfd3b011cfb53a9013d1f Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 13 May 2026 01:37:47 +0900 Subject: [PATCH] =?UTF-8?q?installer:=20fabric=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20/=20=EC=9E=90=EB=8F=99=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20/=20UPnP=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=84=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세 가지 문제를 정리한다. 1) fabric 으로 마인크래프트 실행 시 "Unable to prepare assets for download" 로 실패하던 문제. - launcher_profiles.lastVersionId 를 "-fabric" 으로 만들고 있었는데 fabric-installer 가 실제로 만드는 폴더 이름은 `fabric-loader--` 라서 런처가 존재하지 않는 버전을 받으려다 실패. - resolveLastVersionId 헬퍼 추가: fabric 일 때 platform.loaderVersion 으로 정확한 이름을 만든다. loaderVersion 미지정이면 .minecraft/versions 에서 `fabric-loader-*-` 패턴 자동 탐색. forge/neoforge 도 동일 패턴으로 후보 탐색. - 결정된 lastVersionId 의 versions 폴더 존재 여부도 점검해 경고 로그를 남긴다. 2) 5단계 완료 후 자동 종료를 임시 비활성화. 마인크래프트 런처 실행 실패 메시지를 사용자가 확인할 수 있도록 quitApp 호출만 주석 처리. X 버튼으로 직접 닫는다. 3) 포트포워딩 점검에서 사용자 라우터 규칙의 활성/비활성을 우리 UPnP 매핑과 구분. - 점검 시작 즉시 removeUpnpMapping 으로 이전 실행에서 남았을 수 있는 우리 UPnP 매핑을 제거. - 그 뒤 1차 점검을 돌리면 "사용자 규칙이 활성화돼 외부 접근 가능" 인 경우만 reachable=true 가 되어 preForwarded. - 사용자 규칙이 비활성/없으면 reachable=false → UPnP 등록 단계로 자연스럽게 진행. - 기존 preForwarded 분기의 사후 unmap 은 제거(시작 단계에서 이미 했으므로 중복). Co-Authored-By: Claude Opus 4.7 --- installer/renderer.js | 4 +-- src/installer/main.ts | 65 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) 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,