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:
@@ -629,8 +629,8 @@ function renderStep5() {
|
||||
// 마무리 액션 실패는 무시하고 종료 진행
|
||||
}
|
||||
finishBtn.textContent = '완료됨'
|
||||
// 모든 단계가 끝났으므로 설치기 종료
|
||||
if (installerApi.quitApp) installerApi.quitApp()
|
||||
// 자동 종료는 임시 비활성화 (런처 실행 오류 메시지 확인용). 필요 시 X 버튼으로 직접 닫는다.
|
||||
// if (installerApi.quitApp) installerApi.quitApp()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user