Compare commits

..

4 Commits

Author SHA1 Message Date
13e4e0eb13 fix(installer): fabric 를 .minecraft 에 설치 — 바닐라 라이브러리/버전 누락 해결
근본 원인(실측): fabric-installer 를 -dir .mc_custom 으로 돌리면 .mc_custom 에
versions/ 와 libraries/ 를 실제 폴더로 만드는데 fabric 것만 들어가고 바닐라 버전
JSON·라이브러리는 빠진다. 그러면 .minecraft 로의 링크가 "폴더 있음" 으로 건너뛰어져
런처가 바닐라 데이터를 못 찾고 "Unable to prepare assets for download" 로 실패.
(웹 조사 확인: assets/libraries/versions 는 런처 폴더 .minecraft 아래 형제 폴더.)

- fabric 을 .minecraft 에 설치(-dir .minecraft)해 바닐라와 같은 위치에 둠. 최신 fabric
  버전 폴더에 .jar 이 없어 설치 완료 판정을 .json 존재로 변경.
- linkMinecraftRuntimeDirs: 예전 설치가 gameDir 에 만든 불완전한 versions/libraries
  실제 폴더를 .minecraft 링크로 교체(관리 데이터라 안전). assets 는 보존.
- 문서 갱신. 0.4.21→0.4.22.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 22:42:40 +09:00
e80933d21e fix(installer): 런타임 폴더 원본 없을 때 빈 폴더 생성 (assets 준비 실패 완화)
.minecraft/{assets,libraries,versions} 가 없으면(바닐라 미실행/다른 런처 환경)
링크를 건너뛰어 .mc_custom/<dir> 이 아예 없어졌고, 그러면 마인크래프트가
"Unable to prepare assets for download" 로 실패했다(코드 주석에도 명시된 원인).
원본이 없으면 .mc_custom/<dir> 을 빈 폴더로 만들어 런처가 그 gameDir 에 직접
받아가도록 수정. 0.4.20→0.4.21.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 22:20:39 +09:00
df5947f8bb fix(installer): 최종 리소스팩 다운로드 실패 시 부분 파일 삭제
downloadStream 실패 시 .mc_custom/resourcepacks/<파일>.zip 에 0바이트/부분 다운로드
파일이 남아 마인크래프트 리소스팩 목록에 깨진 채로 노출될 수 있던 문제 수정.
finalResourcepack:install catch 에서 대상 zip 을 삭제. 0.4.19→0.4.20.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 00:39:18 +09:00
3c404ecd8c feat(installer): 최종 리소스팩 등록 시 베이스 리소스팩 다운로드 생략
최종 리소스팩(finalResourcepackPath !== ".")이 등록돼 있으면 완성본을 설치 마지막에
따로 받으므로, 클라이언트 설치 단계에서 베이스 리소스팩 다운로드를 건너뛴다
("등록한 최종 리소스팩만 다운로드"). 최종 리소스팩이 "."(없음)이면 기존대로 베이스를
받는다. 0.4.18→0.4.19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 00:25:10 +09:00
4 changed files with 56 additions and 10 deletions

View File

@@ -78,7 +78,7 @@
2. `.minecraft` 최상위 설정 파일(`options.txt`, `optionsof.txt`, `servers.dat`, `usercache.json`, …) 을 `.mc_custom` 으로 복사. 이미 같은 이름이 있으면 보존.
3. 플랫폼 설치:
- `vanilla` → 건너뜀.
- `fabric` → Adoptium 자동 설치 → 최신 fabric-installer.jar 다운로드 → `java -jar fabric-installer.jar client -mcversion X -loader Y -dir .mc_custom -noprofile` 자동 실행.
- `fabric` → Adoptium 자동 설치 → 최신 fabric-installer.jar 다운로드 → `java -jar fabric-installer.jar client -mcversion X -loader Y -dir .minecraft -noprofile` 자동 실행. (fabric 버전/라이브러리는 런처 폴더 `.minecraft` 에 바닐라와 함께 설치해야 런처가 찾을 수 있다. `.mc_custom` 에 설치하면 바닐라 버전·라이브러리가 빠져 "Unable to prepare assets for download" 로 실패한다.)
- `forge` / `neoforge``platform.downloadUrl` 의 설치 jar 다운로드(사용자가 직접 실행하거나 마인크래프트 런처가 인식).
4. `modsFolder` 의 모든 `.jar``resourcepackPath` zip, `mapPath` zip 을 자동 다운로드.
5. `.minecraft\{assets,libraries,versions}``.mc_custom\{assets,libraries,versions}` 로 junction 링크. (없으면 "Unable to prepare assets for download" 오류로 마인크래프트가 실패하기 때문)

View File

@@ -241,6 +241,7 @@
"modDownload": "모드 다운로드: {{file}}",
"skipResourcepack": "resourcepackPath가 비어 있어 리소스팩 다운로드를 건너뜁니다.",
"resourcepackDownload": "리소스팩 다운로드: {{url}}",
"skipBaseForFinal": "최종 리소스팩이 등록돼 있어 베이스 리소스팩은 건너뜁니다(설치 마지막에 최종 리소스팩만 받음).",
"finalResourcepackDownload": "최종 리소스팩 다운로드: {{url}}",
"finalResourcepackSaved": "최종 리소스팩 저장: {{path}}",
"serverInstallPath": "서버 설치 경로: {{path}}",
@@ -289,9 +290,11 @@
"settingCopyFail": "설정 복사 실패 ({{name}}): {{message}}",
"settingCopySummary": "기존 마인크래프트 설정 복사: 새로 복사 {{copied}}개 / 동기화(options 류 덮어쓰기) {{synced}}개 / 보존(이미 존재) {{skipped}}개.",
"settingCopyError": "기존 설정 복사 중 오류: {{message}}",
"runtimeDirMissing": ".minecraft/{{dir}} 가 없습니다. 마인크래프트 런처를 한 번 실행한 뒤 다시 시도해주세요.",
"runtimeDirMissing": ".minecraft/{{dir}} 가 없어 공용 링크를 만들 수 없습니다(런처가 이 폴더에 직접 받게 됩니다).",
"runtimeDirCreated": ".mc_custom/{{dir}} 폴더를 새로 만들었습니다(런처가 여기에 직접 다운로드).",
"runtimeDirExists": ".mc_custom/{{dir}} 가 실제 폴더로 이미 존재 — 건너뜀.",
"runtimeLinkCreated": "링크 생성: .mc_custom/{{dir}} → .minecraft/{{dir}}",
"runtimeLinkReplaced": "불완전한 .mc_custom/{{dir}} 폴더를 .minecraft/{{dir}} 링크로 교체했습니다(바닐라 데이터 포함).",
"runtimeLinkFail": "링크 생성 실패 ({{dir}}): {{message}}",
"shortcutCreated": "바로가기 생성: {{path}}",
"shortcutFailed": "바로가기 생성 실패",

View File

@@ -1,6 +1,6 @@
{
"name": "minecraft-music-quiz-installer",
"version": "0.4.18",
"version": "0.4.22",
"description": "마인크래프트 음악퀴즈 간편설치기 + 관리 사이트",
"main": "dist/installer/main.js",
"scripts": {

View File

@@ -285,11 +285,12 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
if (!pack) return { ok: false, message: t('errors.packNotFound2') }
const finalName = pack.pack.finalResourcepackPath
if (!finalName || finalName === '.') return { ok: false, message: 'no final resourcepack' }
let dest = ''
try {
const cleaned = path.basename(finalName.replace(/^\/+/, ''))
const url = `${state.baseUrl}/file/resourcepacks/outputs/${encodeURIComponent(cleaned)}`
const destDir = path.join(getAppDataDir(), getMcCustomDirName(), 'resourcepacks')
const dest = path.join(destDir, cleaned)
dest = path.join(destDir, cleaned)
await fsp.mkdir(destDir, { recursive: true })
sendLog(t('log.finalResourcepackDownload', { url }))
// 진행률(퍼센트)을 렌더러로 흘려보내며 스트리밍 다운로드.
@@ -307,6 +308,8 @@ ipcMain.handle('finalResourcepack:install', async (): Promise<{ ok: boolean; pat
sendLog(t('log.finalResourcepackSaved', { path: dest }))
return { ok: true, path: dest }
} catch (error) {
// 실패 시 부분/0바이트 zip 이 마인크래프트 리소스팩 목록에 깨진 채로 남지 않도록 삭제.
if (dest) await fsp.rm(dest, { force: true }).catch(() => {})
return { ok: false, message: (error as Error).message }
}
})
@@ -1352,7 +1355,13 @@ ipcMain.handle('client:install', async (_event, payload: ClientInstallPayload) =
}
await downloadModsFolder(pack.pack, customRoot)
await downloadResourcepackZip(pack.pack, customRoot)
// 최종 리소스팩이 등록돼 있으면(finalResourcepackPath !== "."), 완성본을 설치 마지막에
// 따로 받으므로 베이스 리소스팩은 받지 않는다("최종 리소스팩만 다운로드").
if (pack.pack.finalResourcepackPath && pack.pack.finalResourcepackPath !== '.') {
sendLog(t('log.skipBaseForFinal'))
} else {
await downloadResourcepackZip(pack.pack, customRoot)
}
if (payload.skipMap) {
// 참가자 모드: 이전 설치 흐름에서 설치러가 풀어둔 맵이 있다면 제거한다.
@@ -1393,11 +1402,19 @@ async function installFabricLoader(pack: PackDefinition, customRoot: string): Pr
// 다시 쓰려고 시도해서, 마인크래프트나 다른 프로세스가 그 파일을 잡고
// 있으면 FileSystemException 으로 실패한다. 결과 파일이 그대로 있으면
// 재실행할 필요가 없으므로 그냥 통과.
// fabric 은 .minecraft(런처 폴더)에 설치한다. gameDir(.mc_custom)에 설치하면 fabric 이
// versions/ 와 libraries/ 를 거기에 실제 폴더로 만드는데, 이 폴더에는 fabric 자체 것만
// 들어가고 바닐라 마인크래프트의 버전 JSON·라이브러리는 빠져 있다. 그러면 .minecraft 로의
// 링크가 "이미 폴더 있음" 으로 건너뛰어져, 런처가 바닐라 데이터를 찾지 못하고
// "Unable to prepare assets for download" 로 실패한다. 그래서 .minecraft 에 설치해
// 바닐라와 같은 위치에 두고, 링크는 뒤(linkMinecraftRuntimeDirs)에서 성공하게 한다.
const mcRoot = path.join(getAppDataDir(), '.minecraft')
await fsp.mkdir(mcRoot, { recursive: true })
const versionId = `fabric-loader-${loaderVersion}-${pack.mcVersion}`
const versionDir = path.join(customRoot, 'versions', versionId)
const versionJar = path.join(versionDir, `${versionId}.jar`)
const versionDir = path.join(mcRoot, 'versions', versionId)
const versionJson = path.join(versionDir, `${versionId}.json`)
if (fs.existsSync(versionJar) && fs.existsSync(versionJson)) {
// 최신 fabric 은 버전 폴더에 .json 만 만든다(.jar 없음). json 존재를 설치 완료 기준으로 본다.
if (fs.existsSync(versionJson)) {
sendLog(t('log.fabricAlreadyInstalled', { id: versionId, dir: versionDir }))
return
}
@@ -1437,10 +1454,10 @@ async function installFabricLoader(pack: PackDefinition, customRoot: string): Pr
'client',
'-mcversion', pack.mcVersion,
'-loader', loaderVersion,
'-dir', customRoot,
'-dir', mcRoot,
'-noprofile'
]
sendLog(t('log.fabricInstallStart', { mc: pack.mcVersion, loader: loaderVersion, dir: customRoot }))
sendLog(t('log.fabricInstallStart', { mc: pack.mcVersion, loader: loaderVersion, dir: mcRoot }))
await runJavaProcess(javaCmd, args)
sendLog(t('log.fabricInstallDone'))
}
@@ -1746,13 +1763,39 @@ async function linkMinecraftRuntimeDirs(customRoot: string): Promise<void> {
const src = path.join(mcRoot, dir)
const dst = path.join(customRoot, dir)
if (!fs.existsSync(src)) {
// .minecraft 에 원본이 없으면(바닐라를 한 번도 안 받았거나 런처가 다른 위치를 쓰는
// 환경) 링크를 만들 수 없다. 이때 그냥 건너뛰면 .mc_custom/<dir> 이 아예 없어
// 런처가 "Unable to prepare assets for download" 로 실패한다. 빈 폴더라도 만들어
// 런처가 그 gameDir 에 직접 받아갈 수 있게 한다(없을 때만).
sendLog(t('log.runtimeDirMissing', { dir }))
try {
if (!fs.existsSync(dst)) {
await fsp.mkdir(dst, { recursive: true })
sendLog(t('log.runtimeDirCreated', { dir }))
}
} catch (err) {
sendLog(t('log.runtimeLinkFail', { dir, message: (err as Error).message }))
}
continue
}
let existing: import('node:fs').Stats | null = null
try { existing = await fsp.lstat(dst) } catch { existing = null }
if (existing) {
if (existing.isSymbolicLink()) continue // 이미 링크됨
// versions/libraries 가 실제 폴더면, 예전 버전이 gameDir 에 fabric 을 설치하며 만든
// 불완전한(바닐라 누락) 폴더일 수 있다. .minecraft 의 완전한 데이터로 링크되도록
// 교체한다. 이 두 폴더는 런처가 관리하는 데이터라 삭제해도 세이브/리소스팩 같은
// 사용자 데이터 손실은 없다. (assets 는 받아둔 에셋 보존을 위해 그대로 둔다.)
if (dir === 'versions' || dir === 'libraries') {
try {
await fsp.rm(dst, { recursive: true, force: true })
await fsp.symlink(src, dst, 'junction')
sendLog(t('log.runtimeLinkReplaced', { dir }))
} catch (err) {
sendLog(t('log.runtimeLinkFail', { dir, message: (err as Error).message }))
}
continue
}
sendLog(t('log.runtimeDirExists', { dir }))
continue
}