installer: fabric 실행 실패 / 자동 종료 / UPnP 우선순위 정리

세 가지 문제를 정리한다.

1) fabric 으로 마인크래프트 실행 시 "Unable to prepare assets for download" 로 실패하던 문제.
   - launcher_profiles.lastVersionId 를 "<mc>-fabric" 으로 만들고 있었는데 fabric-installer 가 실제로 만드는 폴더 이름은 `fabric-loader-<loaderVer>-<mcVer>` 라서 런처가 존재하지 않는 버전을 받으려다 실패.
   - resolveLastVersionId 헬퍼 추가: fabric 일 때 platform.loaderVersion 으로 정확한 이름을 만든다. loaderVersion 미지정이면 .minecraft/versions 에서 `fabric-loader-*-<mcVer>` 패턴 자동 탐색. 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 01:37:47 +09:00
parent 7d0f1719f3
commit b407a2ca6a
2 changed files with 60 additions and 9 deletions

View File

@@ -478,6 +478,12 @@ ipcMain.handle('server:portForward', async (_event, port: number): Promise<PortF
const targetPort = Number.isFinite(port) && port > 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<PortF
sendLog(`1차 점검 결과: ${probe.reachable === true ? '성공' : probe.reachable === false ? '실패' : '확인 불가'} (${probe.detail})`)
if (probe.reachable === true) {
sendLog(`외부에서 ${externalIp || '(IP 미상)'}:${targetPort} 접근 확인됨. 포트포워딩 됨.`)
// 이미 라우터 사용자 규칙으로 포워딩 중이라면 우리가 이전에 만든 UPnP 매핑은 불필요.
// 남아 있으면 중복/충돌 소지가 있어 제거 시도.
await removeUpnpMapping(targetPort)
sendLog(`외부에서 ${externalIp || '(IP 미상)'}:${targetPort} 접근 확인됨. 사용자 규칙으로 포워딩 됨.`)
return { status: 'preForwarded', externalIp, port: targetPort }
}
@@ -966,6 +969,50 @@ function mergeRamArgs(existing: string, recommendedMb: number): string {
return merged.join(' ').trim()
}
/**
* launcher_profiles 의 lastVersionId 를 마인크래프트 런처가 실제로 가지고 있는 폴더 이름과 맞춘다.
* - vanilla: mcVersion 그대로 (예: "1.21.4")
* - fabric: fabric-installer 가 만드는 폴더 명명 규칙은 `fabric-loader-<loaderVer>-<mcVer>`.
* 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<void> {
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,