사진 URL 에 data: URI 가 들어오면 http/https 만 처리하는 다운로더가
'Protocol "data:" not supported' 로 설치 전체를 중단시키던 문제 수정.
data: URL 은 이미지 바이트를 직접 품고 있으므로 base64/percent-encoding
을 디코드해 Buffer 로 바로 반환한다. 잘못된 형식은 명확한 메시지로 거절.
이미지 zip 의 cover_NN.json 이 title/author 를 {text:...} 객체로 내보내
일부 환경에서 인식되지 않던 문제. 요청 형식대로 author:"musicquiz",
title:"cover_NN" 평문 문자열로 바꿔 asset_id/width/height 뒤에 배치한다.
renamePack 가 manifest 정의와 약관 폴더는 새 키로 옮기면서 정작 음악·사진
목록(file/list/<key>.json)은 옛 키 파일에 남겨, 이름 변경 후 목록이 비어
보이던 버그 수정. 약관 폴더와 동일하게 fsp.rename 으로 옮기고 옛 파일이
없으면(ENOENT) 무시한다.
entrySnbt() now emits {title, author, alias, description} so the
mcfunction export carries the operator-entered song description
into the data modify storage command. Multiline descriptions are
flattened via newline/tab escapes inside the SNBT string literal
(escapeSnbtString extended to handle \r, \n, \t alongside the
existing backslash + quote escapes) so each `data modify` stays a
single line.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The url-edit modal's save handler was rebuilding state.music[idx]
from scratch using only meta-lookup fields, silently dropping aliases
and (newly added) description. Carry them over from prev so editing
a track's URL no longer wipes operator-entered metadata.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each music list row now shows a 설명 button immediately to the left of
the 별칭 button. Click opens a modal with a multi-line textarea; on
close the value is persisted into MusicListEntry.description and saved
to the same pack list JSON. The button gets a hasDesc visual indicator
when filled. Description is stored but intentionally not consumed by
datapack export or alias matching — purely informational metadata.
- types.ts: add description: string to MusicListEntry
- store.ts: normalize entry.description via sanitizeStr (defaults to '')
- listEditor.ejs: new #descModal alongside aliasModal
- listEditor.js: render descBtn left of aliasBtn, attach handlers,
also set description: '' on playlist-fetched entries
- styles.css: extend trackRow grid to 6 cols, reuse aliasBtn styling
for descBtn, add descTextarea sizing
- locale (ko-kr): descBtn / descModalTitle / descBack / descPlaceholder
/ descHint
Backwards-compatible: existing list JSON files without description
field normalize to ''.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ensureYtDlp() and prepareYtDlp() now check the on-disk yt-dlp_zipapp
before re-running the native-fail -> network-download path. On Linux
servers where the native binary always fails verification (glibc/musl/
arch mismatch), every previous request was re-downloading both the
native (~33MB) and the zipapp (~3MB). With the fast path, after the
first successful zipapp install all subsequent requests short-circuit
to the cached zipapp.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Accidentally tracked by the previous commit's git add -A. Untracked
artifact from shell-command mangling in an earlier smoke test, not
part of any feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
If the native yt-dlp_linux/yt-dlp_macos binary fails to execute (glibc
mismatch, musl libc, wrong arch) AND no system yt-dlp is on PATH, fall
back to downloading the universal Python zipapp ('yt-dlp', ~3MB) and
running it via shebang. Requires python3 on PATH, which is standard on
modern Linux servers. Also: rewrite stale Windows-flavored install-path
comments to reflect actual cross-platform behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
NTFS marks files downloaded over HTTP with a Zone.Identifier alternate
data stream, which SmartScreen/Attachment Manager can use to block
execution of yt-dlp.exe. Remove the ADS best-effort after each
download to reduce one likely cause of "execution verification failed"
in the user-reported failure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
probeVersion() now captures stderr/exit-code/signal/spawn-error instead of
returning a bare boolean, and ensureYtDlp() tries the bundled binary first,
falls back to `yt-dlp(.exe)` on PATH if the bundled one won't execute (AV
block, missing libc symbol, broken download), and only then re-downloads.
The final user-facing error includes the per-attempt diagnostics so we can
actually see WHY verification failed instead of the opaque
"yt-dlp 다운로드는 됐지만 실행 검증에 실패했습니다." message.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer correctly flagged that the previous skip-on-collision
behavior silently drops new quiz tracks when the base resourcepack
already has the same track_NN key. That makes the install LOOK
successful but breaks the quiz at runtime (datapack references the
missing track).
The new behavior throws a clear error explaining which key collided
and what the user must do (remove the conflicting base entry, or
use a different base). The base assets are still preserved (we
never overwrite); we just refuse to build a broken pack.
Removed the now-unused skip-summary log keys.
If the base resourcepack already has audio files under
assets/musicquiz/sounds/ or entries in assets/musicquiz/sounds.json,
the build now PRESERVES them and skips any new track that would
collide. Same policy for painting textures: existing cover_*.png
in the base are not overwritten by new ones.
Per-track collision is logged so the user can see exactly what was
preserved and what was skipped. Summary counts (added / skipped)
are also logged.
Requested by 사금향: "기존에 있는걸 삭제하거나 이상하게 엎어쓰지
말것" — preserve base assets unconditionally.
RP installer already escapes k.tab; main installer was injecting it raw.
Add escapeHtml helper and apply to tab id/label so admin-supplied
agreement labels can't break the HTML.
User asked for "약관 표시 실패시 설치 실패로 처리". The block-on-failure path
is already in place; this just sharpens the message so users see "설치를
진행할 수 없습니다" rather than a soft retry prompt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer caught that v0.3.4 was bypassing the agreement step entirely on
network/server errors, letting users install without ever seeing terms.
Now only the explicit empty-list response (terms:[]) skips the step.
Network errors, 404s, and IPC failures render an error page with Back/Retry
buttons; no next button is exposed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- _meta.json: customLabels -> terms.{label,showInInstaller,showInInstallerRp}
- Drop builtin protection; any term kind can be deleted/added/toggled
- New public route /manifest/terms/<pack>/index.json for installer term lists
- Installers fetch terms:list dynamically; skip agreement step if list empty
- Term editor: 2 visibility checkboxes (설치기 / 리소스팩 설치기), multi-select
- Migration from old schema preserves custom labels (default: visible in both)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- public route `/manifest/terms/:packKey/:fileName` 가 sendFile 전에
`ensurePackTermsDir(packKey)` 를 호출하도록 수정. 관리자가 사이트 약관
페이지를 한 번도 열지 않은 fresh 배포에서도 설치기가 정상적으로 약관을
받을 수 있다. `loadPackDefinition` 으로 실제 pack 만 허용해 임의 키로
빈 폴더가 생성되는 것을 차단.
- `renamePack`: pack JSON 이름이 바뀌면 `manifest/terms/<oldKey>/` 도
`<newKey>/` 로 함께 rename.
- `deletePackKeys`: pack 삭제 시 약관 폴더도 `fs.rm` 으로 정리 — 동일 key
재생성 시 옛 약관 부활 방지.
- `ensurePackTermsDir` export.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- store.ts: 약관을 manifest/terms/<packKey>/ 폴더별로 저장. 첫 접근 시
legacy 전역 .md 파일을 시드로 자동 복사한다.
- importTerms() 추가: 다른 음악퀴즈의 .md + _meta.json 을 현재 pack 으로
복사한다. 동일 kind 는 source 값으로 덮어쓴다.
- /op/agreement 라우트를 세 단계로 분리:
· /op/agreement → 음악퀴즈 카드 선택 페이지
· /op/agreement/:packName → 해당 pack 의 약관 목록 + 추가 + 불러오기
· /op/agreement/:packName/:kind → 에디터
- 공개 라우트도 /manifest/terms/:packKey/:fileName 으로 변경.
- 설치기 main.ts: state.selectedKey 를 약관 URL 에 포함하도록 수정 (메인 +
rp 양쪽). pack 미선택 상태에서는 에러 반환.
- termsEditor.js: PACK_KEY 를 받아 저장 URL 에 포함.
- 다른 음악퀴즈 후보 select + 확인 모달 + locale 추가.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 약관 편집기 배경/슬래시 메뉴를 사이트 다크 팔레트로 통일 (흰 배경 + 흰 글씨 가시성 문제 해결)
- 약관 목록을 가로 풀폭 1줄씩 세로로 쌓이는 레이아웃으로 변경
- 사용자 정의 약관 추가/삭제 지원
- manifest/terms/_meta.json 에 라벨 저장
- builtin 5종(map/resourcepack/mod/installer/installer-rp)은 삭제 불가, "기본" 배지 표시
- kind 식별자 규칙: 소문자/숫자/하이픈 32자 이내
- 공개 라우트 /manifest/terms/<file>.md 는 isPublicTermsFile() 로 _meta.json 차단
- 0.3.0 → 0.3.1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- 5종 약관(map/resourcepack/mod/installer/installer-rp) markdown 시드 + manifest/terms/ 노출
- 사이트 /op/agreement 목록 + Notion 스타일 markdown 에디터 (슬래시 명령어, 미리보기)
- 메인 installer: 음악퀴즈 선택 직후 약관 동의 페이지(맵·모드·설치기) 추가
- rp installer: 음악퀴즈 선택 직후 약관 동의 페이지(리소스팩·설치기) 추가
- rp installer 취소 버그 수정: buildResourcepackZip 단계간 + archive.abort() 폴링
- rp installer 취소 UX: 즉시 "취소 중…" 표시, 취소 시 installFailed 알림 생략
- 0.2.6 → 0.3.0 (큰 기능)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Minecraft launcher's "설치 설정" screen reads `profile.icon` from
launcher_profiles.json. We were leaving it unset, so the launcher fell
back to the default Furnace icon. Inline build/icon.png as a base64
data URL at build time (scripts/build-launcher-icon.cjs generates
src/installer/launcherIcon.ts) and set it on the profile we write.
The build/ directory isn't included in the electron-builder asar (it's
only used to point at the .ico for the exe), so a runtime read isn't
possible — the icon ships compiled into the bundle. To refresh after
changing icon.png, run `npm run build:launcher-icon` (it's wired into
`dist:win` so a fresh exe build always regenerates it).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The RP installer downloads a fresh copy of the base zip into its temp
dir and composes the final pack on top of it. The base zip the main
installer placed in .mc_custom/resourcepacks/ has nothing to do after
that — but it stays in the Minecraft resource-pack list as a second
entry. Delete it after the final zip is written.
Guard against the case where the user set outputPackName equal to the
base filename, which would make base path == final path; in that case
we leave it alone so we don't wipe the file we just wrote.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Korean Windows defaults the JVM's stdout to cp949 (MS949), so the
fabric-installer's Korean status lines came through as mojibake when
Node decoded them as UTF-8 (e.g. "���가져오는중 (org.ow2.asm:asm:9.9)").
Pass -Dfile.encoding/-Dstdout.encoding/-Dstderr.encoding=UTF-8 before
-jar so the JVM writes UTF-8 and our existing utf-8 decode matches.
stdout/stderr.encoding properties take effect on Java 18+;
file.encoding covers older JDKs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous version only deleted platform-cache at the very end of the
success path. If anything between platform install and launcher
profile update failed, the cache jar stuck around. Move the rm into
a finally block so the directory is always cleaned up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- yt-dlp.exe, ffmpeg.exe now live in %appdata%/.mc_custom/installer/ so
the .mc_custom root stays a clean Minecraft game folder. Existing
binaries at the old location are migrated on first run.
- After a successful install, the platform-cache (downloaded fabric /
forge / neoforge installer jars) is deleted — it's regenerable and
was just wasting disk space.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per user request: when outputPackName is empty, fall back to
`<packKey>_resourcepack` instead of `<packKey>_musicquiz`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a new "생성되는 리소스팩 이름" admin field saved to the pack
manifest and consumed by the rp installer when naming the final zip.
Empty value falls back to <packKey>_musicquiz; Windows-invalid chars
are sanitized to '_'. Bumps version 0.1.1 → 0.2.0 (new feature).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The rp installer's `index.html` references `../installer/styles.css`,
which works in dev because both source directories sit side by side.
The packaged exe's `files` list only included `installer-rp/**`, so
inside the asar the stylesheet path resolved to nothing and the UI
rendered completely unstyled (per user screenshot).
Add the single shared file `installer/styles.css` to the rp build's
file list. The cross-directory `<link>` reference now resolves inside
the asar, and we avoid duplicating the stylesheet.
Bump to 0.1.1 — small patch-level fix.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The packaged installer-rp crashed on launch with
"Could not load the 'sharp' module using the win32-x64 runtime" because
electron-builder ran on Linux and only the Linux sharp variants were
present in node_modules.
- Add `preinstall:sharp-win32` script that force-installs
`@img/sharp-win32-x64@0.34.5` into the local node_modules (npm refuses
it on Linux without --force due to its os/cpu restrictions).
- Chain that script before both `dist:win` and `dist:win:rp` so future
Windows builds always have the native prebuilt available.
- Exclude `@img/sharp-{,libvips-}linux*` from the packaged files list in
both electron-builder configs so the unused Linux variants don't bloat
the portable exe.
Verified `release/win-unpacked/resources/app.asar.unpacked/node_modules/
@img/sharp-win32-x64/lib/sharp-win32-x64.node` is present and that no
linux sharp variants ship inside the asar.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Track `.env.build` in version control so the production site domain
(`https://mc.tkrmagid.kr`) is baked into every portable exe build by
default. `.env` (server/dev secrets) stays gitignored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer noted that returning on the first found file meant a project-root
`.env.build` could shadow the dev `.env`, leaving the server without
`PORT`/`HOST`/`SESSION_SECRET`. Switch `loadEnv()` to iterate every
candidate with `override:false` so multiple files merge — first-loaded
value wins per key.
Order also reshuffled so dev's `.env` takes precedence over `.env.build`
at the project root (server settings stay alive), while in packaged
mode `resources/.env.build` still wins. Verified manually with a
temp-dir reproduction.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous setup packaged the development `.env` into the installer
resources, mixing local server settings (PORT/HOST/SESSION_SECRET) with
the build-time site domain. Introduce a dedicated `.env.build`:
- electron-builder configs now copy `.env.build` (gitignored) into
`resources/`, no longer touching the dev `.env`.
- `loadEnv()` prefers `resources/.env.build` first, falling back to
`resources/.env` (for operators who hand-edit the packaged file),
then `<root>/.env.build`, then `<root>/.env`.
- `.env.build.example` documents the build-only keys (SITE_BASE_URL,
MANIFEST_URL, MUSIC_CONCURRENCY); server-side keys stay in `.env`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both electron-builder configs now produce a single-file portable .exe
instead of an NSIS installer. Removes installer/uninstaller icons and
the install-directory wizard — users can run the .exe directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The root package.json's `main` field points at dist/installer/main.js
because that's the default `npm run installer` entry. Without an
override, `electron-builder --config electron-builder-rp.yml` would
package the resourcepack installer with the wrong main, so the exe
would start the regular installer (or fail outright).
Add `extraMetadata.main: dist/installer-rp/main.js` so the packaged
package.json is rewritten at build time to point at the right entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reviewer follow-ups:
1) Preserve mods/ for vanilla packs. `downloadModsFolder` now checks
`!pack.modsFolder` BEFORE wiping — vanilla packs (no modsFolder) no
longer clobber a user's hand-curated mods directory. Wipe still runs
for modded packs to keep different MC versions from colliding.
2) Always rename the extracted map to `saves/<퀴즈이름>/`, regardless of
the zip's top-level layout. The zip is now extracted into a temp
directory under saves/, and:
- if the temp has a single subdirectory, that subdirectory's content
becomes the world;
- otherwise the temp dir itself (e.g. level.dat + region/ at root) is
the world.
In either case, it is renamed atomically to `saves/<sanitized name>`
(or `<name>_2` etc. if a user world collides). Marker tracks the
final folder name for participant cleanup.
User request: replace both .exe icons.
- Added build/icon.ico (multi-size 16/32/48/64/128/256) and build/icon.png
generated from the new music-note artwork.
- electron-builder.yml: set win.icon, nsis installer/uninstaller icons,
buildResources=build, include build/icon.* in files for runtime use.
- New electron-builder-rp.yml + dist:win:rp script so the resourcepack
installer also packages with the same icon.
- BrowserWindow({ icon }) wired in both installer and installer-rp main
processes so the running window's titlebar/taskbar icon matches.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two follow-ups requested by the user (and the first flagged by the
reviewer for omission):
1) Different Minecraft versions or different packs leave behind mod jars
that crash Fabric on load. `downloadModsFolder` now removes the entire
`.mc_custom/mods/` directory before every install — including when the
pack is vanilla (no modsFolder) so leftovers from a previous modded
pack get cleared too.
2) `downloadMapZip` renames the single extracted top-level folder to the
pack name (sanitized for Windows: forbidden chars `<>:"/\|?*` and
control chars → `_`, trailing space/dot trimmed, reserved names like
CON/NUL prefixed, empty fallback to `map`). Collisions with user
worlds get `_2`, `_3` … suffixes so we never overwrite the user's
own worlds. The marker file tracks the post-rename folder so future
participant cleanup still removes only what the installer created.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously the multi-mode role pick appeared as a sub-section that
expanded inline under the single/multi cards. Per request, separate it
into a dedicated page reached by pressing Next on the mode tab.
- renderStep2 now only handles mode selection (single/multi). Next
routes single → step4 directly, multi → renderStep2Role.
- New renderStep2Role shows the host/participant cards on its own page
with back to renderStep2. Next routes host → step3, participant → step4.
- backToPrevStep in step4 and the back from sub31 in step3 updated so
the role tab is the correct intermediate landing for multi flows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the user installs as single (skipMap=false) and then navigates back
to choose participant (skipMap=true), the previously-extracted map files
in .mc_custom/saves/ would remain because skipMap=true only skipped the
download. The final participant install state was therefore inconsistent
with the chosen role.
Track the top-level entries that downloadMapZip extracts via a marker
file (.musicquiz-installer-map.json) inside saves/. On participant
install (skipMap=true) or before a re-download, only the entries listed
in the marker are removed, so user-created worlds are preserved.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
기존 state.client.clientInstalled boolean 은 packKey/installPlatform/skipMap
차이를 보지 않아, 참가자→싱글 로 뒤로가서 변경했을 때 skipMap=false
경로의 맵 설치가 영영 안 일어났다 (반대로 싱글→참가자 면 안 풀어도 될
맵이 남음). state.client.lastInstall 에 마지막 성공 payload 전체를
저장하고, 진입 시 새 payload 와 필드별 비교해서 다르면 재설치한다.
실패는 lastInstall 을 비워 다음 진입에서 자동 재시도.
리뷰어 지적사항(installer/renderer.js:657) 대응.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
step2:
- 멀티 선택 시 호스트 / 참가자 sub-choice 추가. 호스트 는 기존 멀티 흐름 그대로,
참가자 는 step3 (서버 설치) 를 건너뛰고 step4 client install 만 진행.
step4 client install:
- 플랫폼 설치/생략 선택 화면(sub41) 제거. 음악퀴즈 platform.type 이 vanilla 가
아니면 무조건 자동 설치, vanilla 면 자동 건너뜀. 사용자 결정 없음.
- 참가자 모드에서는 ClientInstallPayload.skipMap=true 로 보내 client 측
saves/ 에 맵을 풀지 않는다 (서버에 이미 있음).
- types.ts 에 skipMap 필드 추가. main.ts client:install 핸들러에서 분기.
step3 EULA modal:
- eula.txt 의 내용과 무관하게 항상 minecraft.net 의 공식 서버 EULA 페이지를
받아 iframe 에 표시. readEula() 분기 제거.
step3 포트포워딩 결과:
- 성공(preForwarded/upnpOk) 시 "친구는 <address> 주소로 서버에 접속할 수
있습니다" 처럼 외부 주소를 강조해 표시.
- 포트가 25565 면 :포트 를 생략하고 ip 만 보여줌 (마인크래프트 자바판
기본 포트라 클라이언트에서도 생략 가능).
step5:
- 서버 마무리 액션 (바로가기/서버 실행 토글) 은 호스트 만 노출. 참가자는
서버를 띄우지 않으므로 런처 토글만 보인다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
데이터팩 수정 페이지에 "이미지.zip 출력" 버튼과 크기 입력(기본 4, 1~16)
을 추가. 누르면 GET /op/datapack/:key/images-zip?size=N 으로 음악 개수만큼
cover_NN.json (asset_id, width=size, height=size, title, author) 을 zip 으로
스트리밍해서 내려준다. 사용자가 맵 데이터팩의 data/musicquiz/painting_variant/
에 그대로 풀어 넣을 수 있다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
베이스팩의 vanilla 셰이더는 manifest 의 mcVersion(resolved.format) 이 64 이하
라도, 우리가 supported_formats/max_format 으로 1.21.9+ 까지 호환을 선언하면
새 GLSL API 환경에서 로드돼 "리소스 새로고침 실패" 가 다시 난다. 셰이더 제거
판정 기준을 resolved.format > 64 에서 maxFmt > 64 로 옮기고, 그 계산을
mcmeta 작성보다 먼저 수행한다. 로그의 format 값도 maxFmt 를 표시해 어떤
호환 상한 때문에 제거됐는지 추적 가능하게 했다.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pack.mcmeta now spans pack_format MIN_SUPPORTED_FORMAT (=63, 1.21.6) up to
max(LATEST_KNOWN_FORMAT, resolved.format) so a single build loads on every
MC from 1.21.6 through 26.1.2+ (currently extending to 86 = 26.2). Both
schemas are written: supported_formats for clients on pack_format <= 64,
and min_format/max_format for 1.21.9+ clients. pack_format itself stays
at the build target so newer clients see the pack as current rather than
legacy.
Reviewer was right that warn-only let broken zips through. On 1.21.9+
(pack_format > 64) the vanilla shader GLSL API changed (ProjMat, FogColor
etc.) so any base pack carrying old assets/minecraft/shaders/* fails to
compile and causes the same "리소스 새로고침 실패" the pack.mcmeta fix
addressed. Strip that directory at build time when the target pack_format
exceeds 64; keep textures/models/sounds intact and log what was removed.
On <= 64 the old shaders still work, so leave the base pack untouched.