리소스팩 간편설치기:
- 베이스 리소스팩 다운로드 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>
VS Code surfaced the TS deprecation:
'moduleResolution=node10' is deprecated and won't work in TS 7.0.
Fix: switch the root tsconfig.json from
module: CommonJS, moduleResolution: node
to
module: Node16, moduleResolution: Node16
TypeScript 7 only supports node16/nodenext/bundler. node16 matches the
runtime semantics we already use (Node ≥ 16, CommonJS output via the
absence of "type": "module" in package.json), so the emit is unchanged.
Side effect of Node16 resolution: relative imports must carry the .js
extension. Added .js to every relative import across src/* (17 sites,
8 files). Bare module specifiers (express, electron, node:fs, ...) are
unaffected.
Verified:
- tsc -p tsconfig.server.json — 0 errors
- tsc -p tsconfig.installer.json — 0 errors
- node dist/server/app.js boots; /op login → 302, /op/list → 200
Music list tab:
- Title + artist are now contenteditable in-place. Typing updates state on
every input event; Enter blurs (no embedded newlines), and on Save the
current DOM text is re-synced into state.music[i].title/.artist before
the JSON POST so a quick-save right after typing doesn't drop the last
keystroke. While focused, the field is highlighted with the accent
outline and unclamped so long titles wrap. Empty cells show a muted
placeholder via :empty::before { content: attr(data-placeholder) }.
- Edit modal "save" now POSTs the new URL to /op/list/<pack>/video-meta
(new endpoint backed by yt-dlp --dump-json --no-playlist) and patches
state.music[i] with the returned title/channel/durationSec. If yt-dlp
is unavailable or the lookup fails, the modal asks the user whether to
apply just the URL change without metadata.
- Drag-and-drop UX: replaced the simple "highlight target" feedback with a
"gap opens above the target" animation. The hovered row gets a
.dropAbove class that animates margin-top to 64px via CSS transition,
visually carving out the slot the dragged item will land in. Insertion
math is now strictly "drop before the hover target" (with the
srcIdx<dstIdx → dstIdx-1 adjustment after the splice removal), matching
what the gap implies. Contenteditable spans no longer hijack drag start
— on focus the parent <li>.draggable is flipped off so the user can
freely select text, and back on at blur.
Image list tab:
- New "음악목록에서 가져오기" button. Copies state.music[*].url into
state.images, which (via the existing thumbUrl() helper) renders each
song's YouTube thumbnail as the image. Behind a confirm prompt because
it replaces the entire image list.
- Same drag gap animation (.dropAbove → margin-left 80px) applied to the
grid cards for consistency.
Server:
- youtube.ts: add fetchVideoMeta(url) using the same ensureYtDlp() path
(auto-installed binary under %appdata%/.mc_custom). Returns one
YtPlaylistEntry or null.
- routes/op.ts: POST /op/list/:packName/video-meta. 400 on missing URL,
503 NO_YTDLP if the auto-install failed, 500 on other yt-dlp errors,
200 { ok: true, entry } otherwise.
Smoke test (Rick Astley URL) returns
title=Rick Astley - Never Gonna..., channel=Rick Astley, durationSec=213.
The previous flow required the operator to manually install yt-dlp on the
server. Now the server downloads the correct binary for the current OS/arch
from GitHub Releases (latest) the first time a playlist fetch is requested,
caches it under %appdata%/.mc_custom (resolved per-OS), chmod +x's it on
POSIX, and skips the download on every subsequent call.
- shared/paths.ts: add getAppDataDir() + getMcCustomDir() that map %appdata%
to ~/.config (Linux), ~/Library/Application Support (macOS), or
process.env.APPDATA (Windows) so the path is OS-agnostic.
- server/youtube.ts: replace the old "probe PATH and bail" probeYtDlp with
ensureYtDlp(): picks the right GitHub asset name per platform.arch
(yt-dlp.exe / yt-dlp_macos / yt-dlp_linux / yt-dlp_linux_aarch64 /
yt-dlp_linux_armv7l), downloads it with redirect-following https.get to
the install path, chmods +x, then verifies with `--version` before
reporting success. Uses an installPromise singleton so concurrent
requests don't race the download. On any failure it cleans up the
partial file and throws YtDlpUnavailableError with a real reason.
- docs/yt-dlp-setup.md: note that auto-install is now the default; manual
install is only for environments where the auto-download fails.
Smoke-tested on this Linux x64 box: first call downloads to
~/.config/.mc_custom/yt-dlp_linux and reports `2026.03.17`; second call
takes ~700ms (just the version probe) and reuses the cached file.
docs/add.md
- 사진 PNG 규격을 1024×1024 (4×4 블록 슬롯 × ×16 배율) 로 못박음
- 짧은 변 기준 가운데 정사각 크롭 + 1024 초과 시만 축소, 미만은 native 유지
신규 라우트 (모두 requireAuth):
- GET /op/list → manifest 카드 목록
- GET /op/list/:pack → 음악목록·사진목록 탭 편집기
- POST /op/list/:pack → file/list/<pack>.json 저장 (JSON)
- POST /op/list/:pack/playlist → yt-dlp 로 플레이리스트 펼치기
- GET /op/datapack → 음악퀴즈 선택 + 출력
- GET /op/datapack/:pack/generate → 임시 포맷 mcfunction 텍스트
shared/types.ts: PackList / MusicListEntry / ImageListEntry
shared/store.ts: loadPackList, savePackList, normalizePackList
shared/paths.ts: fileListDirPath = file/list/
server/youtube.ts: yt-dlp 플레이리스트 펼치기 (--flat-playlist --dump-json),
설치 안 됐을 때 NO_YTDLP 코드로 503.
UI:
- views/op/list.ejs: 가로 카드 목록 + 돌아가기 버튼
- views/op/listEditor.ejs + public/listEditor.js: 탭 전환, 드래그 정렬,
우클릭 컨텍스트 메뉴(수정/삭제), 사진 수정 모달의 [유튜브 / 이미지] 토글,
목록 저장·초기화·플레이리스트 불러오기 확인 팝업
- views/op/datapack.ejs: 음악퀴즈 카드 선택 팝업 → 데이터팩 출력 + 복사
- views/op/dashboard.ejs: 상단에 [음악목록 수정] [데이터팩 수정] 버튼
- public/styles.css: 탭, 트랙 로우, 이미지 그리드, 컨텍스트 메뉴, 모달, 코드블록
.gitignore: conversations/ 추가.
스모크: login → /op/list 렌더, POST 저장 라운드트립 OK,
/op/datapack/.../generate 텍스트 출력 OK, 플레이리스트 fetch는 yt-dlp 미설치
환경에서 503 NO_YTDLP 메시지 정상.
Section 1 (리소스팩 설치기 EXE: yt-dlp 음악 다운로드, painting variant
텍스처 패키징, 리소스팩 zip 배치) 은 후속 커밋에서 작업.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Each 3-x and 4-x sub-step now has its own [이전][다음] action row at the bottom; 이전 returns to the previous sub-step (or previous main step on the first one) so users can move freely both directions
- 3-3 EULA: replace the inline checkbox with a modal popup. After server zip downloads, the renderer reads eula.txt via server:readEula; if absent, falls back to the live minecraft.net/en-us/eula HTML via server:fetchMinecraftEula and shows it in a sandboxed iframe
- Popup buttons: 동의 → server:acceptEula and proceed to RAM check; 비동의 / X / overlay click → "EULA 동의 실패. 다운로드를 취소합니다." and re-enable 다운로드 시작 for retry
- main.ts: stop auto-deleting eula.txt after server zip extraction so the popup can read whatever the zip provided
- 4-2 install completion now keeps a state.client.clientInstalled flag so backing into 4-2 doesn't force a re-install
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PackDefinition: replace mods[]/resourcepacks[] with modsFolder (string) + resourcepackPath (string); drop PackAsset
- Editor: replace dynamic add/remove lists with two single inputs; remove the now-dead JS for adding/removing rows
- Server: expose GET /file/mods/<folder>/index.json that returns the list of .jar names; folder name restricted to [a-zA-Z0-9_-]+
- Installer: fetch the listing JSON and download each jar from /file/mods/<folder>/<file>.jar; download the single resourcepack from /file/resourcepacks/<file>.zip directly into resourcepacks/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 2 (싱글/멀티): replace auto-advance with a card selection plus a
"다음" button so the user can review their choice before moving on.
Step 4-1 (모드 플랫폼): replace the "설치 / 건너뛰기" buttons with two
cards — "권장 플랫폼 설치" and "기본 마인크래프트로 설치". Selection
only records intent; the actual platform install fires in 4-2 along
with mods/resourcepacks (already handled by installer:client install).
Server: default HOST to 127.0.0.1 instead of 0.0.0.0 so the startup
log prints a Ctrl+클릭으로 바로 열 수 있는 URL. Set HOST=0.0.0.0
externally when public exposure is needed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The installer pulls each pack JSON from /manifest/<file>.json, but the
server was blocking every /manifest/ path. Add a strict whitelist
route that only serves /manifest/<a-zA-Z0-9_-name>.json files (no
directory listing, no path traversal, no other extensions); keep the
catch-all 404 for anything else under /manifest/.
Also strip the manifest URL input and "목록 새로고침" button from
installer step 1 — packs auto-load on page render, only the selectable
list remains.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Path is set by the loader upload form below, so the manual
text input was redundant. Drop the field from the editor UI
and stop overwriting the saved value on form submit so an
uploaded loader keeps its path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>