기존 copyMinecraftUserSettings 는 .mc_custom 에 같은 이름의 파일이
있으면 무조건 보존했기 때문에, 사용자가 .minecraft 에서 새로 바꾼
키설정·옵션이 .mc_custom 으로 이어지지 못했다. options.txt /
optionsof.txt / optionsshaders.txt 는 사용자가 원래 쓰던 설정을
그대로 가져오기 위한 파일이므로 매번 .minecraft 쪽으로 덮어써서
동기화하고, servers.dat 같은 그 외 파일은 종전대로 보존한다.
normalizePackDefinition 이 fabric 일 때 downloadUrl 을 의도적으로
스트립하고 있어 저장 후 입력값이 사라지는 문제. vanilla 외에는
모두 보관하도록 조건을 변경하고, 에디터 UI 도 fabric 에서 URL
입력 칸을 다시 보여주도록 되돌렸다.
증상: 플랫폼 타입이 fabric 인 음악퀴즈를 편집할 때 "플랫폼 설치파일
URL" 필드가 잠깐 보이고, 그 자리에 값을 입력해 저장해도 disk 에는
저장되지 않아 다시 비어 보였다 (normalizePackDefinition 이 fabric 의
downloadUrl 을 의도적으로 제거하기 때문).
원인: editor.ejs 가 platformDownloadField 를 항상 visible 로,
platformLoaderField 를 항상 hidden 으로 렌더한 뒤 JS 가 뒤늦게 보정.
이 짧은 깜빡임 동안 사용자가 URL 필드를 보고 입력하게 됨.
수정: 서버 렌더 시점에 pack.platform.type 에 따라 hidden 속성을 미리
붙여 둔다 (fabric/vanilla → URL 숨김, fabric → loader 표시).
증상: 두 번째 설치 시도에서 fabric-installer 가
FileSystemException: ...fabric-loader-X-Y.jar: 다른 프로세스가 파일을
사용 중이기 때문에 프로세스가 액세스 할 수 없습니다
로 실패. 마인크래프트(또는 OS 인덱서)가 jar 핸들을 잡고 있을 때 발생.
원인: fabric-installer 는 매 실행마다 versions/<id>/<id>.jar 를
deleteIfExists 한 뒤 다시 쓰려고 한다. 이미 설치돼 있으면 굳이 다시
쓸 필요가 없다.
수정: installFabricLoader 에서 customRoot/versions/<versionId>/<versionId>.jar
와 .json 이 둘 다 존재하면 곧바로 return 하고 안내 로그만 남긴다.
- listEditor: keydown(Escape) 시 열린 .modalOverlay 닫기. 별칭 모달은
"돌아가기" 와 동일하게 입력값을 저장한 뒤 닫는다.
- datapack: pickModal 도 ESC 로 닫히게 추가.
(팝업 바깥 영역 클릭으로 닫기는 두 페이지 모두 기존부터 동작 중.)
운영자가 mc_datapack 의 init/songs.mcfunction 파일에 직접 복사해 붙여넣
는 워크플로로 단순화. 전체 데이터팩을 패키징할 필요가 없다.
- /op/datapack/:packName/generate 가 buildSongsMcfunction(list) 결과를
text/plain 으로 반환 (zip 스트리밍 제거).
- file/datapacks/music_quiz_template/ 정적 사본 제거.
- datapack.ejs 에 코드블록·복사 버튼 복원, 안내 문구 추가
("data/mq/function/init/songs.mcfunction 에 그대로 덮어쓰세요").
- datapack 로케일 라벨을 "코드 출력 / 복사 / 출력 완료" 로 정리.
가이드 (mc_datapack/launcher_datapack_연동_가이드.txt) 에 따라:
- file/datapacks/music_quiz_template/ 에 mc_datapack 의 music_quiz/ 정적
파일을 미리 동봉 (data/mq/function/init/songs.mcfunction 제외).
- src/server/datapack.ts: list.music → SNBT (`{title, author, alias}`)
songs.mcfunction 빌더와 archiver 기반 zip 스트리머 추가.
- /op/datapack/:packName/generate 가 텍스트 placeholder 대신
music_quiz_<key>.zip 을 Content-Disposition attachment 로 내려준다.
- datapack.ejs 의 코드블록·복사 UI 제거, 곡 수는 서버 렌더 시점에 표시.
- 더 이상 쓰이지 않는 locales 의 datapackOutput.* 키 제거, datapack
버튼 라벨/상태 문구를 zip 다운로드용으로 정리.
- MusicListEntry 에 aliases: string[] 필드 추가, 저장 시 trim·중복 제거.
- 목록 행에 "별칭" 버튼 표시(개수 있으면 강조), 클릭 시 모달 오픈.
- 모달에서 "별칭 추가" → 입력행 생성, "−" 버튼 → 해당 행 삭제,
좌상단 "← 돌아가기" 또는 오버레이 클릭으로 저장 후 닫기.
sub42 에서 클라이언트 설치가 끝나면 그 자리의 "다음" 버튼이 바로 5단계로
넘어가도록 변경. 중복적이던 sub43 의 i18n 키와 renderSubStep43 함수도 함께
삭제.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- main/preload/ytdlp/ffmpeg/music/images/pack/renderer 전반에서 로그·에러·진행
메시지 문자열을 locales/installer-rp/ko-kr.json 사전 키로 교체
- preload 에 loadLocale 추가, main 에 rp:i18n:dict IPC 핸들러 추가
- 패키징된 .exe 에서도 한국어 사전이 적용되도록 electron-builder.yml 의
extraResources 에 locales/ 폴더 추가
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- src/shared/i18n.ts: 공용 i18n 로더 (dotted-key + {{placeholder}} 보간)
- locales/server/ko-kr.json: 사이트 + 라우터 + 데이터팩 출력 사전
- EJS 뷰는 res.locals.t 미들웨어로 일괄 적용
- listEditor.js 등 클라이언트 JS 는 사전을 inline <script> 로 주입받아 tt() 헬퍼 사용
- electron-builder.yml
- dist/shared/** 추가 (env.js 등이 패키지에 들어가도록)
- extraResources 로 빌드 시점 .env 를 resources/.env 에 배포
- loadEnv: 패키징된 Electron 앱이면 process.resourcesPath/.env 를 먼저 시도하고
없으면 프로젝트 루트 .env 로 폴백
- docs/admin-site.md: .exe 빌드에도 .env 가 따라가는 동작 설명 추가
- dotenv 도입, src/shared/env.ts 추가
- loadEnv() 가 프로젝트 루트 .env 를 로드 (override=false: 쉘 env 우선)
- getSiteBaseUrl() / getManifestUrl() 헬퍼
- 서버/설치기/리소스팩설치기 진입점에서 loadEnv() 호출
- 설치기 두 종의 기본 MANIFEST_URL 을 SITE_BASE_URL 기반으로 변경
(운영 도메인을 한 곳에서만 바꾸면 됨)
- .env.example 템플릿 + .gitignore 에 .env 추가
- README / docs/admin-site.md 에 환경변수 표·사용법 추가
- installer/renderer.js: 4단계 완료 후 자동 종료 다시 활성화
- README.md 를 개발 명세에서 프로젝트 개요/구성/빠른 시작 형식으로 재작성
- docs/installer.md (음악퀴즈 간편설치기 단계별 사용 가이드) 신설
- docs/admin-site.md (관리 사이트 운영자 가이드) 신설
- docs/resourcepack-installer.md (리소스팩 간편설치기 동작 명세) 신설
- 중복된 docs/add.md 제거 (resourcepack-installer.md 로 이전)
이전 커밋이 server run.bat 에 잘못 적용됐던 것을 되돌리고, 본래 의도대로
launcher_profiles.json 의 javaArgs 에 Aikar 권장 G1 GC 플래그 6종을 병합한다.
- mergeRamArgs(-Xmx) 후 mergeJvmTuningFlags 로 누락 플래그만 추가
- 사용자가 같은 키를 이미 지정했으면 그 값을 존중(덮어쓰지 않음)
- run.bat 의 injectJvmFlags 호출 및 함수 제거
메모리(-Xms/-Xmx)는 기존 설정 그대로 두고 Aikar 권장 G1 GC 플래그 6종
(-XX:+UnlockExperimentalVMOptions, -XX:+UseG1GC, -XX:G1NewSizePercent=20,
-XX:G1ReservePercent=20, -XX:MaxGCPauseMillis=50, -XX:G1HeapRegionSize=32M)
을 -jar 앞에 끼워 넣는다. -XX:+UseG1GC 가 이미 있으면 멱등 처리.
minecraft:// URL 스킴이 핸들러가 깨졌거나 비어 있을 때 MS Store 로 폴백되어
실제 런처가 안 떠는 케이스 대응. 실행 순서를 아래로 변경:
1) Win32 설치판 직접 spawn (Program Files / Xbox / portable)
2) App Execution Alias(Minecraft.exe / MinecraftLauncher.exe, reparse point
이므로 cmd /c start 경유)
3) explorer.exe shell:AppsFolder\\Microsoft.4297127D64EC6_8wekyb3d8bbwe!Minecraft
로 MSIX(Microsoft Store) 런처 직접 호출
4) 마지막 수단: minecraft:// URL 스킴
JDK 가 없을 때 사용자가 "자동 설치" 를 눌러 Adoptium Temurin 21 LTS
(Windows x64 zip) 를 받아 %APPDATA% 의 jdk/temurin-21 으로 풀어 사용하도록
한다. 다운로드는 streaming + AbortController 로 묶어, 설치 진행 중 같은
버튼이 "설치 취소" 로 바뀌며 누르면 다운로드를 즉시 중단하고 부분 파일을
정리한다. jdk:detect 후보에 자동 설치 경로도 추가해 다음 실행 시 자동 탐색됨.
서버가 실행 중일 때만 25565(또는 server-port) 가 열려 있도록 run.bat 을
후처리해 java 호출 전 UPnP Add, 종료 후 UPnP Remove 를 PowerShell 한 줄
(HNetCfg.NATUPnP.1)로 끼워 넣는다. Add 전에 같은 포트 매핑을 Remove 하므로
재실행에도 idempotent. 포트체크 단계에서 만든 테스트용 UPnP 매핑은 테스트
직후 제거해 실제 개방은 run.bat 이 단독으로 책임지게 한다.
제한: 콘솔창 X 강제 종료 시 teardown 미실행. 라우터 TTL 만료 또는 다음
실행 시 재등록 직전 Remove 로 자연 정리.
options.txt, optionsof.txt, servers.dat, usercache.json 등 .minecraft 최상위
파일을 .mc_custom 으로 복사해 사용자가 기존에 만들어둔 키바인딩/볼륨/렌더거리/
서버목록을 음악퀴즈 인스턴스에서도 그대로 사용할 수 있도록 한다.
이미 .mc_custom 에 같은 이름의 파일이 있으면 보존(덮어쓰지 않음). 디렉터리는
복사 대상에서 제외(mods/saves/versions/assets 등은 별도 처리).
세 가지 문제를 정리한다.
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>
관리 사이트에서 모드 플랫폼으로 fabric 을 선택하면 jar 파일 업로드 대신, 선택한 마인크래프트 버전을 기준으로 Fabric Meta v2 API 에서 호환 로더 목록을 가져와 드롭다운으로 선택하도록 했다. 설치기는 platform.loaderVersion 만 보고 최신 fabric-installer.jar 를 받아 CLI 로 자동 설치(GUI 미표시)한다.
스키마:
- PackPlatform 에 loaderVersion?: string 추가. fabric 일 때만 사용.
- normalizePackDefinition: fabric 이면 downloadUrl 무시하고 loaderVersion 만 저장, 그 외에는 기존 downloadUrl 유지.
웹 UI(views/op/editor.ejs):
- platformType 이 fabric 일 때 platformLoaderVersion select 노출. mcVersion 셀렉트 값을 가지고 https://meta.fabricmc.net/v2/versions/loader/<mcVersion> 호출.
- mcVersion 또는 platformType 변경 시 자동 재조회. 동시 요청 경쟁은 sequence 비교로 무시.
- 이전 저장값을 우선 선택하되 목록에 없으면 최신 stable 자동 선택.
- 폼 제출 시 fabric 인데 로더 미선택이면 경고.
- 라우트(op.ts): platformLoaderVersion 폼 필드 수신.
설치기(installer/main.ts):
- client:install 분기 추가. fabric 이면 installFabricLoader 호출.
- installFabricLoader: Fabric Meta installer 메타 조회 → 최신 stable installer jar 캐시 다운로드 → java -jar fabric-installer.jar client -mcversion <ver> -loader <ver> -dir <.mc_custom> -noprofile 실행. launcher_profiles 갱신은 우리 코드(updateLauncherProfile)가 담당하므로 -noprofile.
- findJavaExecutable: JAVA_HOME → .minecraft\runtime 의 번들 자바(델타/감마/베타 등 우선순위) → PATH 폴백.
- runJavaProcess: stdout/stderr 를 로그 뷰어에 prefix 와 함께 스트리밍. 실패 시 stderr 끝부분을 메시지에 포함.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Minecraft Launcher 실행 핸들러가 옛 Program Files 경로 두 곳만 보고 있어서 Microsoft Store/UWP/Xbox 앱 설치 등 최근 설치 형태에서 거의 못 찾았다.
- 1순위로 shell.openExternal('minecraft://') 사용. OS에 등록된 프로토콜 핸들러가 설치 형태(UWP/Win32/Xbox)에 무관하게 처리.
- 폴백 경로 후보 확장: Program Files / Program Files (x86) 양쪽의 Minecraft, Minecraft Launcher, XboxGames 경로, LOCALAPPDATA\Programs\minecraft-launcher까지 검사.
- 못 찾았을 때 메시지에 설치처(Microsoft Store/minecraft.net) 안내 추가.
5단계 완료 버튼: 모든 단계가 끝난 뒤이므로 마무리 액션(바로가기/서버 실행/런처 실행)을 처리한 다음 app.quit으로 설치기를 자동 종료한다. 'app:quit' IPC 핸들러와 preload 노출(quitApp) 추가.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
기존 방식은 자기 PC에서 자기 외부 IP로 TCP 연결을 시도해 도달성을 판정했는데, 가정용 라우터의 헤어핀(hairpin) NAT 미지원으로 실제 외부 접근은 가능해도 내부 검증은 실패하던 문제가 있었다.
- probePortFromOutside: 임시 TCP 리스너를 대상 포트에 띄우고 ifconfig.co/port/PORT를 호출해 외부에서 해당 포트로 TCP 연결을 시도하게 한 뒤, 리스너에 연결이 도달했는지 + ifconfig.co의 JSON reachable 값으로 종합 판정.
- 포트가 이미 사용 중(서버 동작 중)이면 임시 리스너를 띄우지 않고 외부 서비스 응답만으로 판정.
- ifconfig.co 응답에서 IP도 같이 얻어 외부 IP 폴백 경로 추가.
또한 1차 점검에서 이미 외부 접근이 가능한 상태(사용자가 라우터에 수동 포워딩 규칙을 등록한 경우)에는 UPnP로 추가 매핑을 만들지 않고, 우리가 이전 실행에서 만들어 둔 UPnP 매핑이 남아 있으면 portUnmapping으로 제거하여 중복/충돌 가능성을 줄인다.
UPnP 매핑 후 재점검도 외부 서비스 기반 probePortFromOutside로 1.5초 간격 3회 재시도해 NAT 상태 전파 지연을 흡수.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3-3 서버 다운로드도 진입 즉시 자동 시작하도록 변경. "다운로드 시작" 버튼을 제거하고 4-2와 같은 자동 실행 패턴을 적용했다(EULA 모달은 그대로 유지). 실패 시 이전→다음으로 재시도.
UPnP 점검 안정화:
- openPortViaUpnp에 15초 타임아웃 추가. SSDP 응답 없을 때 영구 hang을 방지한다.
- detectExternalIp: ipify 단일 실패 시 ifconfig.me/icanhazip 폴백 후, 최종 폴백으로 UPnP 게이트웨이의 externalIp를 사용. 기존에는 IP를 못 얻으면 UPnP 시도조차 안 했음.
- portMapping 성공 후 NAT 상태 전파 지연을 고려해 testPortReachable을 1.5초 간격 3회 재시도.
- 각 단계마다 로그를 남겨 라우터 UPnP 비활성/SSDP 차단/이중 NAT 등의 원인을 구분할 수 있게 함.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3-5 포트포워딩 점검에 진입하면 즉시 점검을 시작하고, 버튼은 "재점검"으로 다시 돌릴 수 있게 한다. 4-2 클라이언트 설치도 진입과 동시에 자동 실행되어 사용자가 따로 버튼을 누를 필요가 없다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- renderer.js: 카드 선택 시 .active 가 아니라 .selected 클래스를 붙여
CSS 의 .cardChoice button.selected 스타일이 실제로 적용되게 함.
- styles.css: 로그 뷰어가 position: fixed 라 본문 하단 버튼을 덮던 문제.
body 를 3-row grid (header/main/logViewer) 로 바꿔 로그 뷰어가
자연스럽게 본문 아래에 배치되도록 수정. hidden 일 때 row 자동 collapse.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
리소스팩 간편설치기:
- 베이스 리소스팩 다운로드 URL 에 encodeURIComponent 적용. "Puzzle Resource
Pack (basic).zip" 같이 공백·괄호가 들어간 파일명 정상 처리.
- 출력 경로를 %appdata%/.minecraft/resourcepacks/ → %appdata%/.mc_custom/
resourcepacks/ 로 변경 (renderer 안내문, openFolder, 빌드 출력 일괄).
- 로드 직후 각 음악퀴즈의 베이스 등록 여부를 로그에 노출 (디버그용).
- 베이스 다운로드 시 실제 URL 도 로그에 출력.
음악퀴즈 간편설치기:
- mergeRamArgs: -Xms 가 기존에 없으면 추가하지 않도록 수정. clientMinRam
은 "유저 PC 사양 최소 요구치" 의미이지 JVM 초기 힙이 아님. -Xmx 는
계속 추천 RAM 으로 강제 갱신.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1) launcher_profiles.json 의 javaArgs 를 통째로 덮어쓰던 코드를 수정.
mergeRamArgs() 로 -Xmx/-Xms 토큰만 새 값으로 교체하고 그 외
사용자 추가 JVM 인수(-Xss, -XX:..., -Dfoo=bar 등)는 보존.
2) .mc_custom 을 gameDir 로 쓰면 마인크래프트 런처가 assets/libraries/
versions 를 못 찾아 "Unable to prepare assets for download" 로 실패.
linkMinecraftRuntimeDirs() 가 .minecraft 의 해당 세 폴더를
.mc_custom 으로 junction(Windows) / symlink(POSIX) 연결. 이미 같은
자리에 무언가 있으면 손대지 않음.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
/manifest/<key>.json 의 resourcepackPath 가 비어있지 않으면
/file/resourcepacks/<path> 의 zip 을 받아 임시 폴더에 풀고, 그 위에
음악·사진·sounds.json·pack.mcmeta 를 얹어 최종 zip 을 만든다.
- types.ts: RpFetchedPack 에 resourcepackPath 필드 추가
- main.ts: 로드 시 normalizePackDefinition 으로 resourcepackPath 캡처,
설치 시 베이스 zip 다운로드 → tempRoot/base.zip → buildResourcepackZip 에 전달
- pack.ts: baseZipPath 옵션 추가. extract-zip 으로 베이스 압축 해제 후 위에 얹기.
sounds.json 은 기존 항목과 병합, pack.mcmeta 는 mcVersion 에 맞춰 항상 덮어씀.
베이스가 없으면(빈 문자열) 기존처럼 새 리소스팩을 처음부터 생성.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
동시 N개를 모두 t=0 에 띄우면 카드들이 0% 에서 같이 멈춰있다가
한꺼번에 100% 가 되는 "정지된 듯한" 구간이 보였음.
이제 새 다운로드 시작 사이에 최소 2.5초 간격을 두어, 어떤 카드는 70%,
어떤 카드는 30% 식으로 항상 진행 흐름이 이어지게 만든다.
- musicStartChain 으로 acquire 직렬화 → race-free
- nextMusicStartAt 으로 마지막 시작 시점 추적
- 동시성(코어 수 기반) 자체는 그대로 유지, 시작 시점만 분산
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
os.cpus().length 기준 동시 다운로드 수를 자동 결정:
- 2 코어 이하 → 2 동시
- 3~4 코어 → 3 동시
- 5~8 코어 → 4 동시
- 9 코어 이상 → 5 동시 (YouTube throttle 때문에 상한)
환경변수 MUSIC_CONCURRENCY 로 강제 오버라이드 가능(상한 8).
설치 로그에 감지된 코어 수와 선택된 동시성 노출.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- yt-dlp 인자에 --concurrent-fragments 5 추가 (HLS/DASH 청크 병렬 다운로드)
- yt-dlp 인자에 --newline 추가 (진행률 라인 안정화)
- 음악 다운로드 루프를 단일 순차 → worker pool 3개 동시 처리로 전환
- state.currentChild (단일) → state.activeChildren (Set) 으로 확장,
취소 시 실행 중인 모든 자식 프로세스 kill
- UI 는 카드 그리드라 병렬 진행 상태가 그대로 표시됨
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2단계 페이지 진입 즉시 설치를 시작하고, 음악·사진을 1번부터 카드 그리드로
한눈에 볼 수 있게 만든다. 다운로드는 % 게이지로, 완료/실패는 색상으로 표시.
- main: prep/item/package phase 의 ProgressEvent 를 renderer 로 송신
- music.ts: yt-dlp stdout 의 [download] X% 라인을 파싱해 onProgress 호출
- preload: onProgress 채널 구독 함수 노출
- renderer: 다음 버튼 제거, prep chip + music/image 카드 그리드 + 빌드 상태
- styles: progressCard / prepChip / progressGrid 스타일 추가
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous hardcoded pack_format 34 + supported_formats 34..75
covered 1.21 through 1.21.11 only, so a pack generated for the
current latest (26.1.2 → format 84) was rejected as outdated.
Add src/installer-rp/packFormat.ts with a 1.21 → 26.2 lookup table
from the Minecraft wiki and resolveResourcePackFormat() that returns
{matched, format}. Unknown mcVersion falls back to the table's most
recent entry, with a log line warning the user.
Plumb mcVersion through the load → install flow:
- rp:packs:load now also fetches /manifest/<key>.json alongside
/file/list/<key>.json and runs it through the existing
normalizePackDefinition so the editor and the installer agree on
the mcVersion shape. Pack manifest load failures fall back to an
empty mcVersion (which then triggers the latest-format fallback).
- RpFetchedPack carries mcVersion; the install handler hands it to
buildResourcepackZip.
- buildResourcepackZip drops the constant pack_format / supported_
formats and uses the resolved format both as pack_format and as
the {min,max} of supported_formats. Each pack is thus pinned to
exactly the MC version it was authored for.
- The renderer's pack card now shows "마인크래프트 <version>" in
the small line so the user can confirm before installing.
Verified locally: pack.mcmeta generated for mcVersion "1.21",
"1.21.6", "26.1.2", and the bogus "99.9.9" produce pack_format
34 / 63 / 84 / 86 (last falls back to the table tail) respectively.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add archiver dep (v7 — v8 dropped the function-style default export
that the @types still describe) and a new src/installer-rp/pack.ts
that assembles the resource pack tree under tempDir/resourcepack/
and zips it to %appdata%/.minecraft/resourcepacks/<key>_musicquiz.zip.
The tree matches Minecraft 1.21+ painting variant + custom sound
conventions:
pack.mcmeta pack_format 34,
supported 34..75
assets/musicquiz/sounds.json stream:true per track
assets/musicquiz/sounds/track_NN.ogg from tempDir/music
assets/musicquiz/textures/painting/cover_NN.png from tempDir/painting
Music NN.ogg is renamed to track_NN.ogg at copy time so the sound
event ids stay readable. The painting_variant JSON definitions are
intentionally NOT generated here — those live in the data pack and
are owned by /op/datapack on the website.
Wire step 2-4 of the install IPC to call buildResourcepackZip with
the now-populated music/painting temp dirs. Step 2-5 is now just a
log line since buildResourcepackZip writes directly to the final
path.
Verified by a node smoke test: tempDir of two stub ogg files plus
two 256x256 PNGs produces a valid zip with the expected entries
and UTF-8 Korean strings in pack.mcmeta description.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add sharp dep (libvips bindings) — fastest option for the per-image
center-crop + Lanczos resize step. Pure-JS alternatives (jimp) and
spawning ffmpeg per image were both ~5-10x slower in this hot loop.
Add src/installer-rp/images.ts:
- ytIdFromUrl: extracts the video ID from watch?v=, youtu.be/, and
/shorts|embed/ URL forms
- downloadImage: for YouTube URLs tries i.ytimg.com/vi/<id>/
maxresdefault.jpg first, falls back to hqdefault.jpg; plain image
URLs go through a generic HTTP/HTTPS GET that follows 302s
- normalizeToCover: center-crop to min(w,h), Lanczos resize down to
1024x1024 when larger, never upscales, writes PNG
- coverFileName: returns cover_NN.png with zero-padded NN
Wire step 2-3 of the install handler to download + normalize each
image into <tempDir>/painting/cover_NN.png. Zip build (step 2-4)
will pick those up next.
Verified with synthetic 1200x800 and 2000x1500 buffers: small
input stays 800x800 (no upscale), large input becomes 1024x1024.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add src/installer-rp/ffmpeg.ts that downloads BtbN/FFmpeg-Builds
win64-gpl zip into %appdata%/.mc_custom/, extracts ffmpeg.exe out
of bin/, drops it at %appdata%/.mc_custom/ffmpeg.exe and verifies
with `ffmpeg -version`. Reuses existing extract-zip dep.
Add src/installer-rp/music.ts that spawns yt-dlp with
--extract-audio --audio-format vorbis --ffmpeg-location <ffmpeg.exe>
to produce <tempDir>/NN.ogg per track. Streams yt-dlp stdout to
the log channel and reports stderr on non-zero exit.
Wire both into the install IPC handler: step 2-1 now preps both
binaries, step 2-2 iterates the music list and downloads each
track. Track the currently running child process in state so the
cancel button can kill it instead of waiting for it to finish.
Image / zip / place steps remain stubbed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add src/installer-rp/ytdlp.ts that downloads
https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe
into %appdata%/.mc_custom/yt-dlp.exe (fixed path, Windows-only since
the installer is shipped as .exe). If the file is already there and
--version works it is reused; otherwise it is re-downloaded and
verified. The server's existing OS-aware ensureYtDlp stays intact.
The install IPC handler now calls ensureYtDlpExe() in step 2-1 and
logs the resolved path. Music / image / zip / place steps are still
stubbed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a second Electron entry under src/installer-rp/ + installer-rp/
launched by `npm run installer:rp`. It is structurally separate from
the music quiz installer (own tsconfig, own preload, own renderer),
shares the existing styles via a relative link, and exposes a
three-step UI: pick a 음악퀴즈, run the install, then a 완료 page that
can open the resourcepacks folder or quit.
The install IPC handler currently scaffolds the flow per docs/add.md
(yt-dlp prep → music download → image normalize → zip build → place
at %appdata%/.minecraft/resourcepacks/) but the actual work is still
TODO. Cancel/cleanup of %appdata%/.mc_custom/.temp/ is wired up so
that future iterations can plug each step in without rewiring.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a red asterisk in the top-right of the dashboard header (and
prefix the document title with *) whenever the dirty flag is set,
so the user can tell at a glance that the music/image list hasn't
been saved yet. The indicator is driven by markDirty/markClean so
it follows the existing tracking.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Track a dirty flag set by every state mutation (inline edit, drag,
delete, modal save, fetch playlist, clear, image-from-music, playlist
URL input) and cleared by a successful save. Intercept back-link
clicks with the existing ask() confirm modal. Use beforeunload for
tab close / refresh. Also refactor ask() so cancel paths properly
discard the pending callback instead of leaking handlers.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The image grid wraps onto multiple rows, so picking the insertion
target by Y-axis alone meant horizontal moves within a row didn't
register. Add a 'grid' orientation that checks Y for row, then X for
column position within the row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The clone-placeholder approach hid the source element with
display:none, which some browsers treat as drag cancellation. The drag
appeared to "not respond at all" to mouse press.
Switch to a simpler approach: keep the source element in the DOM and
move it directly during dragover. Apply a .dragGhost class after the
drag image is captured (via setTimeout 0) so the source becomes a
translucent dashed placeholder at the prospective drop position. drop
just compares the source's current DOM index to its original
data-index.
Also add cursor:grabbing on :active for visible press feedback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous version had contenteditable always on for title/artist
spans, which intercepted mousedown and prevented dragstart from firing
on the row. Now the spans render as plain text, and double-click
activates contenteditable + focus + select-all. blur or Enter/Escape
exits edit mode, saves to state, and re-enables row dragging.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>