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>
187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
'use strict'
|
|
|
|
const api = window.rpInstaller
|
|
|
|
const state = {
|
|
packs: [],
|
|
selectedKey: null,
|
|
installing: false,
|
|
installed: false,
|
|
resourcepackPath: ''
|
|
}
|
|
|
|
const pageHost = document.getElementById('pageHost')
|
|
const stepIndicator = document.getElementById('stepIndicator')
|
|
const logViewer = document.getElementById('logViewer')
|
|
const logBody = document.getElementById('logBody')
|
|
const logToggle = document.getElementById('logToggle')
|
|
|
|
logToggle.addEventListener('click', function () {
|
|
logViewer.classList.toggle('collapsed')
|
|
if (logViewer.classList.contains('collapsed')) {
|
|
logViewer.style.height = '36px'
|
|
logToggle.textContent = '펼치기'
|
|
} else {
|
|
logViewer.style.height = ''
|
|
logToggle.textContent = '접기'
|
|
}
|
|
})
|
|
|
|
api.onLog(function (line) {
|
|
logViewer.hidden = false
|
|
logBody.textContent += line + '\n'
|
|
logBody.scrollTop = logBody.scrollHeight
|
|
})
|
|
|
|
function setActiveStep(step) {
|
|
stepIndicator.querySelectorAll('li').forEach(function (item) {
|
|
var index = Number(item.getAttribute('data-step'))
|
|
item.classList.remove('active', 'done')
|
|
if (index < step) item.classList.add('done')
|
|
if (index === step) item.classList.add('active')
|
|
})
|
|
}
|
|
|
|
function clearPage() { pageHost.innerHTML = '' }
|
|
|
|
// ── 1단계: 음악퀴즈 선택 ────────────────────────────
|
|
function renderStep1() {
|
|
setActiveStep(1)
|
|
clearPage()
|
|
var section = document.createElement('section')
|
|
section.className = 'page'
|
|
section.innerHTML =
|
|
'<h2>1단계. 음악퀴즈 선택</h2>' +
|
|
'<div id="packList" class="cardChoice"><p class="formMessage">목록을 불러오는 중...</p></div>' +
|
|
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
|
pageHost.appendChild(section)
|
|
var listEl = section.querySelector('#packList')
|
|
var nextBtn = section.querySelector('#next')
|
|
|
|
function renderList() {
|
|
listEl.innerHTML = ''
|
|
if (state.packs.length === 0) {
|
|
listEl.innerHTML = '<p class="formMessage error">등록된 음악퀴즈가 없습니다.</p>'
|
|
return
|
|
}
|
|
state.packs.forEach(function (pack) {
|
|
var card = document.createElement('button')
|
|
card.type = 'button'
|
|
card.className = 'choiceCard'
|
|
if (state.selectedKey === pack.key) card.classList.add('active')
|
|
var verLabel = pack.mcVersion ? '마인크래프트 ' + escapeHtml(pack.mcVersion) + ' · ' : ''
|
|
card.innerHTML =
|
|
'<strong>' + escapeHtml(pack.name) + '</strong>' +
|
|
'<small>' + verLabel +
|
|
'음악 ' + pack.list.music.length + '곡 · 사진 ' + pack.list.images.length + '장</small>'
|
|
card.addEventListener('click', function () {
|
|
state.selectedKey = pack.key
|
|
nextBtn.disabled = false
|
|
renderList()
|
|
})
|
|
listEl.appendChild(card)
|
|
})
|
|
}
|
|
|
|
nextBtn.addEventListener('click', function () {
|
|
if (!state.selectedKey) return
|
|
api.selectPack(state.selectedKey).then(function () {
|
|
renderStep2()
|
|
}).catch(function (err) {
|
|
alert(err.message || '선택 실패')
|
|
})
|
|
})
|
|
|
|
api.loadPacks().then(function (packs) {
|
|
state.packs = packs || []
|
|
renderList()
|
|
}).catch(function (err) {
|
|
listEl.innerHTML = '<p class="formMessage error">목록 로드 실패: ' + escapeHtml(err.message || '') + '</p>'
|
|
})
|
|
}
|
|
|
|
// ── 2단계: 설치 진행 ────────────────────────────────
|
|
function renderStep2() {
|
|
setActiveStep(2)
|
|
clearPage()
|
|
var section = document.createElement('section')
|
|
section.className = 'page'
|
|
section.innerHTML =
|
|
'<h2>2단계. 리소스팩 설치</h2>' +
|
|
'<p class="formMessage">아래 "다음"을 누르면 음악·사진을 받아 리소스팩을 만들고 ' +
|
|
'<code>%appdata%/.minecraft/resourcepacks/</code> 에 넣습니다.</p>' +
|
|
'<div class="actionRow">' +
|
|
' <button class="secondaryBtn" id="prev">이전</button>' +
|
|
' <button class="primaryBtn" id="start">다음</button>' +
|
|
' <button class="secondaryBtn" id="cancel" hidden>취소</button>' +
|
|
'</div>'
|
|
pageHost.appendChild(section)
|
|
var prevBtn = section.querySelector('#prev')
|
|
var startBtn = section.querySelector('#start')
|
|
var cancelBtn = section.querySelector('#cancel')
|
|
|
|
prevBtn.addEventListener('click', function () {
|
|
if (state.installing) return
|
|
renderStep1()
|
|
})
|
|
|
|
startBtn.addEventListener('click', function () {
|
|
if (state.installing) return
|
|
state.installing = true
|
|
startBtn.disabled = true
|
|
prevBtn.disabled = true
|
|
cancelBtn.hidden = false
|
|
logViewer.hidden = false
|
|
|
|
api.startInstall().then(function (result) {
|
|
state.installing = false
|
|
state.installed = true
|
|
state.resourcepackPath = (result && result.resourcepackPath) || ''
|
|
renderStep3()
|
|
}).catch(function (err) {
|
|
state.installing = false
|
|
startBtn.disabled = false
|
|
prevBtn.disabled = false
|
|
cancelBtn.hidden = true
|
|
alert('설치 실패: ' + (err.message || err))
|
|
})
|
|
})
|
|
|
|
cancelBtn.addEventListener('click', function () {
|
|
api.cancelInstall()
|
|
})
|
|
}
|
|
|
|
// ── 3단계: 완료 ────────────────────────────────────
|
|
function renderStep3() {
|
|
setActiveStep(3)
|
|
clearPage()
|
|
var section = document.createElement('section')
|
|
section.className = 'page'
|
|
section.innerHTML =
|
|
'<h2>3단계. 완료</h2>' +
|
|
'<p class="formMessage">리소스팩 설치를 완료했습니다.</p>' +
|
|
(state.resourcepackPath
|
|
? '<p class="formMessage"><code>' + escapeHtml(state.resourcepackPath) + '</code></p>'
|
|
: '') +
|
|
'<div class="actionRow">' +
|
|
' <button class="secondaryBtn" id="openFolder">리소스팩 폴더 열기</button>' +
|
|
' <button class="primaryBtn" id="finish">확인</button>' +
|
|
'</div>'
|
|
pageHost.appendChild(section)
|
|
section.querySelector('#openFolder').addEventListener('click', function () {
|
|
api.openResourcepackFolder()
|
|
})
|
|
section.querySelector('#finish').addEventListener('click', function () {
|
|
api.quit()
|
|
})
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"']/g, function (c) {
|
|
return c === '&' ? '&' : c === '<' ? '<' : c === '>' ? '>' : c === '"' ? '"' : '''
|
|
})
|
|
}
|
|
|
|
renderStep1()
|