op: 데이터팩 출력을 zip 대신 songs.mcfunction 코드 텍스트로 변경

운영자가 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 로케일 라벨을 "코드 출력 / 복사 / 출력 완료" 로 정리.
This commit is contained in:
2026-05-13 16:41:25 +09:00
parent af884706d4
commit de08f9a810
65 changed files with 44 additions and 817 deletions

View File

@@ -1,7 +0,0 @@
scoreboard objectives add func.temp dummy
$execute store result score n1 func.temp run data get storage func:temp $(n1)
$execute store result score n2 func.temp run data get storage func:temp $(n2)
execute if score n1 func.temp = n2 func.temp run return 1
return 0

View File

@@ -1,15 +0,0 @@
scoreboard players set two func.temp 2
$data modify storage func:temp half.result set from storage func:temp $(list)
execute store result score length func.temp run data get storage func:temp half.result
scoreboard players operation half func.temp = length func.temp
scoreboard players operation half func.temp /= two func.temp
scoreboard players operation odd func.temp = length func.temp
scoreboard players operation odd func.temp %= two func.temp
scoreboard players operation half func.temp += odd func.temp
function func:half/f1

View File

@@ -1 +0,0 @@
execute if score half func.temp < length func.temp run function func:half/f2

View File

@@ -1,5 +0,0 @@
data remove storage func:temp half.result[-1]
scoreboard players remove length func.temp 1
function func:half/f1

View File

@@ -1,18 +0,0 @@
$function func:length {text:"$(text)"}
# return length
function func:num_list with storage func:temp
# return num_list
function func:shuffle {list:"num_list"}
# return shuffle.result
function func:half {list:"shuffle.result"}
# return half.result
$function func:length {text:"$(text)"}
# return length
function func:text_list with storage func:temp
# return text_list
function func:join {list:"text_list"}
# return join.text
# tellraw @a {"storage":"func:temp","nbt":"join.text"}

View File

@@ -1,2 +0,0 @@
$execute if data storage func:temp {$(l1):{$(l2):[$(index)]}} run return 1
return 0

View File

@@ -1,2 +0,0 @@
$execute if data storage func:temp {space:{text:"$(space)"}} run return 1
return 0

View File

@@ -1,4 +0,0 @@
data modify storage func:temp join.text set value ""
$data modify storage func:temp join.list set from storage func:temp $(list)
function func:join/f1

View File

@@ -1,8 +0,0 @@
execute store result score length func.temp run data get storage func:temp join.list
execute if score length func.temp matches 0 run return 1
data modify storage func:temp join.now set from storage func:temp join.text
data modify storage func:temp join.next set from storage func:temp join.list[0]
function func:join/f2 with storage func:temp join

View File

@@ -1,5 +0,0 @@
$data modify storage func:temp join.text set value "$(now)$(next)"
data remove storage func:temp join.list[0]
function func:join/f1

View File

@@ -1,5 +0,0 @@
$data modify storage func:temp text set value "$(text)"
execute store result storage func:temp length int 1 run data get storage func:temp text
return run data get storage func:temp length

View File

@@ -1,13 +0,0 @@
data modify storage func:temp space.space set value " "
data modify storage func:temp zero set value 0
$data modify storage func:temp length set value $(length)
execute store result score result func.temp run function func:comp_num {n1:"zero",n2:"length"}
execute if score result func.temp matches 1 run tellraw @s {"text":"length는 1이상 이어야 합니다.","color":"red"}
execute if score result func.temp matches 1 run return 0
data modify storage func:temp num_list set value []
data modify storage func:temp index set value 0
data modify storage func:temp index_next set value 1
function func:num_list/f1 with storage func:temp

View File

@@ -1,12 +0,0 @@
$data modify storage func:temp space.text set string storage func:temp text $(index) $(index_next)
execute store result score result func.temp run function func:is_space with storage func:temp space
$execute if score result func.temp matches 0 run data modify storage func:temp num_list append value $(index)
function func:plus {name:"index",plus:1}
function func:plus {name:"index_next",plus:1}
execute store result score result func.temp run function func:comp_num {n1:"index",n2:"length"}
execute if score result func.temp matches 1 run return 1
function func:num_list/f1 with storage func:temp

View File

@@ -1,7 +0,0 @@
scoreboard objectives add func.temp dummy
$execute store result score temp func.temp run data get storage func:temp $(name)
$scoreboard players add temp func.temp $(plus)
$execute store result storage func:temp $(name) int 1 run scoreboard players get temp func.temp

View File

@@ -1,9 +0,0 @@
scoreboard objectives add func.temp dummy
data modify storage func:temp shuffle.result set value []
$data modify storage func:temp shuffle.list set from storage func:temp $(list)
execute store result score length func.temp run data get storage func:temp shuffle.list
function func:shuffle/f1

View File

@@ -1,11 +0,0 @@
execute store result score length func.temp run data get storage func:temp shuffle.list
execute if score length func.temp matches 0 run return 1
execute store result score random func.temp run random value 0..2147483646
scoreboard players operation random func.temp %= length func.temp
execute run function func:shuffle/f2 with storage func:temp {index:0}
execute store result storage func:temp shuffle.index int 1 run scoreboard players get random func.temp
function func:shuffle/f2 with storage func:temp shuffle

View File

@@ -1,4 +0,0 @@
$data modify storage func:temp shuffle.result append from storage func:temp shuffle.list[$(index)]
$data remove storage func:temp shuffle.list[$(index)]
function func:shuffle/f1

View File

@@ -1,13 +0,0 @@
data modify storage func:temp space.space set value " "
data modify storage func:temp zero set value 0
$data modify storage func:temp length set value $(length)
execute store result score result func.temp run function func:comp_num {n1:"zero",n2:"length"}
execute if score result func.temp matches 1 run tellraw @s {"text":"length는 1이상 이어야 합니다.","color":"red"}
execute if score result func.temp matches 1 run return 0
data modify storage func:temp text_list set value []
data modify storage func:temp index set value 0
data modify storage func:temp index_next set value 1
function func:text_list/f1 with storage func:temp

View File

@@ -1,16 +0,0 @@
$data modify storage func:temp space.text set string storage func:temp text $(index) $(index_next)
execute store result score result func.temp run function func:is_space with storage func:temp space
$execute store result score result2 func.temp run function func:is_index {l1:"half",l2:"result",index:$(index)}
execute if score result2 func.temp matches 0 if score result func.temp matches 0 run data modify storage func:temp text_list append value ""
execute if score result2 func.temp matches 0 if score result func.temp matches 1 run data modify storage func:temp text_list append from storage func:temp space.text
execute if score result2 func.temp matches 1 run data modify storage func:temp text_list append from storage func:temp space.text
function func:plus {name:"index",plus:1}
function func:plus {name:"index_next",plus:1}
execute store result score result func.temp run function func:comp_num {n1:"index",n2:"length"}
execute if score result func.temp matches 1 run return 1
function func:text_list/f1 with storage func:temp

View File

@@ -1,5 +0,0 @@
{
"values": [
"mq:load"
]
}

View File

@@ -1,5 +0,0 @@
{
"values": [
"mq:tick"
]
}

View File

@@ -1,23 +0,0 @@
{
"criteria": {
"placed_something": {
"trigger": "minecraft:impossible",
"conditions": {}
}
},
"rewards": {
"function": "mq:players/login"
},
"parent": "minecraft:story/root",
"display": {
"icon": {
"id": "minecraft:stone"
},
"title": "로그인 감지",
"description": "서버 접속을 감지",
"hidden": false,
"announce_to_chat": true,
"show_toast": true,
"frame": "task"
}
}

View File

@@ -1,46 +0,0 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n1. 정답입력시에 채팅으로 입력해주시면 됩니다.\n[ 띄어쓰기, 영어 대소문자, 특수문자 ]\n상관없이 입력하셔도 인식 됩니다.\n\n2. 모든 소리는 날씨 소리로 조절할수 있습니다.\n\n3. 게임시작후 버튼들은 과반수(절반이상)가 눌러야 작동합니다.\n\n4. 힌트는 특수문자 제외 정답의 절반이 가려져서 나옵니다.\n힌트는 여러번 받을수 있고,\n받을때마다 가려지는 부분이 달라집니다."
},
"width": 300
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "취소",
"type": "text"
},
"action": {
"type": "minecraft:run_command",
"command": "trigger cancel"
}
},
"no": {
"label": {
"text": "다음 페이지 ->"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page2"
}
}
}

View File

@@ -1,45 +0,0 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n5. 다시듣기는 노래가 끝까지 다 재생되었거나,\n다시 처음부분부터 들어보고싶을때\n누르면 좋습니다.\n\n6. 뒤에있는 \"소리 테스트\" 버튼으로\n미리 소리크기를 들어보고 조절할수있습니다."
}
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "<- 이전 페이지",
"type": "text"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page1"
}
},
"no": {
"label": {
"text": "다음 페이지 ->"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page3"
}
}
}

View File

@@ -1,46 +0,0 @@
{
"type": "minecraft:confirmation",
"title": {
"text": "음악퀴즈",
"bold": true
},
"body": [
{
"type": "minecraft:plain_message",
"contents": {
"text": "음악퀴즈 설명",
"bold": true
}
},
{
"type": "minecraft:plain_message",
"contents": {
"text": "\n재미있게 즐겨주세요."
}
}
],
"inputs": [],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"yes": {
"label": {
"text": "<- 이전 페이지",
"type": "text"
},
"action": {
"type": "minecraft:show_dialog",
"dialog": "mq:page2"
}
},
"no": {
"label": {
"text": "준비완료",
"bold": true
},
"action": {
"type": "minecraft:run_command",
"command": "trigger ready"
}
}
}

View File

@@ -1,14 +0,0 @@
scoreboard players reset @a hint
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 힌트를 받을 수 없습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run data modify storage mq:main hint.text set from storage mq:main answer.title
execute if score init main matches 5 run data modify storage mq:main hint.hint set value ""
execute if score init main matches 5 run function func:hint with storage mq:main hint
execute if score init main matches 5 run data modify storage mq:main hint.hint set from storage func:temp join.text
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:'""'}
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"힌트: ","color":"aqua","bold":true},{"storage":"mq:main","nbt":"hint.hint","color": "yellow","bold": true}]}
execute if score init main matches 5 run function mq:tellraw {"text":"","color":"black",msg:'""'}

View File

@@ -1,9 +0,0 @@
scoreboard players reset @a replay
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run function mq:quiz/stop_sound
execute if score init main matches 5 run function mq:quiz/play_sound

View File

@@ -1,9 +0,0 @@
scoreboard players reset @a skip
execute if score init main matches 0 run return run function mq:tellraw {"text":"아직 퀴즈가 시작되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 1..4 run return run function mq:tellraw {"text":"아직 스킵 할수없습니다.","color":"red",msg:'""'}
execute if score init main matches 6.. run return run function mq:tellraw {"text":"아직 다음노래가 재생되지 않았습니다.","color":"red",msg:'""'}
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"red",msg:'""'}
execute if score init main matches 5 run scoreboard players set skip buttons -2
execute if score init main matches 5 run function mq:quiz/correct

View File

@@ -1,10 +0,0 @@
execute if score init main matches 10 run return run function mq:tellraw {"text":"퀴즈가 완전히 종료된후 시작해주세요.","color":"red","msg":""}
setblock ~ ~ ~ minecraft:air
function mq:quiz/stop_sound
$scoreboard players set max_index main $(max_index)
scoreboard players set init main 1
dialog show @a mq:page1

View File

@@ -1,59 +0,0 @@
scoreboard players set index main 0
$scoreboard players set max_index main $(max_index)
scoreboard players set score main 0
scoreboard players set init main 0
scoreboard players set timer main 0
scoreboard players set start buttons -1
scoreboard players set stop buttons -1
scoreboard players set skip buttons -1
scoreboard players set hint buttons -1
scoreboard players set replay buttons -1
scoreboard players set test buttons -1
scoreboard players reset @a answer
# 트리거 시작
scoreboard objectives remove ready
scoreboard objectives add ready trigger
scoreboard objectives remove cancel
scoreboard objectives add cancel trigger
scoreboard objectives remove stop
scoreboard objectives add stop trigger
scoreboard objectives remove skip
scoreboard objectives add skip trigger
scoreboard objectives remove hint
scoreboard objectives add hint trigger
scoreboard objectives remove replay
scoreboard objectives add replay trigger
# 트리거 끝
scoreboard objectives setdisplay sidebar
scoreboard objectives remove score
scoreboard objectives add score dummy {"text":"점수","bold":true}
scoreboard objectives setdisplay sidebar score
dialog clear @a
bossbar set mq:process name [{"text":"진행도: ","color": "yellow","bold": true},{"score":{"name":"index","objective": "main"},"color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"score":{"name":"max_index","objective": "main"},"color": "yellow","bold": true}]
$bossbar set mq:process max $(max_index)
bossbar set mq:process value 0
bossbar set mq:process color pink
bossbar set mq:process visible false
bossbar set mq:process style notched_10
bossbar set mq:process players @a
# 대기 상태 marker 1개만 소환 (answer.title="음악퀴즈" 가 sentinel)
data modify storage mq:main answer set value {title:"음악퀴즈", alias:[]}
data modify storage mq:tmp marker_call set from storage mq:main marker
data modify storage mq:tmp marker_call.name set value "음악퀴즈"
data modify storage mq:tmp marker_call.alias set value []
function mq:quiz/macro/summon with storage mq:tmp marker_call
function mq:quiz/stop_sound
function mq:images/clear

View File

@@ -1,5 +0,0 @@
stopsound @a block minecraft:block.stone_button.click_on
function mq:tellraw {"text":"띵!!!","color":"white","msg":'""'}
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9
execute as @a at @s run playsound minecraft:block.note_block.bell weather @s ~ ~ ~ 1 0.9

View File

@@ -1 +0,0 @@
kill @e[type=minecraft:painting,tag=mq_cover]

View File

@@ -1 +0,0 @@
$summon minecraft:painting $(x) $(y) $(z) {variant:"$(namespace):$(cover)",facing:$(facing)b,Tags:["mq","mq_cover"]}

View File

@@ -1,3 +0,0 @@
data modify storage mq:tmp painting set from storage mq:main image
data modify storage mq:tmp painting.cover set from storage mq:main answer.cover
function mq:images/macro/show with storage mq:tmp painting

View File

@@ -1,7 +0,0 @@
data modify storage mq:main button_defs set value []
data modify storage mq:main button_defs append value {n:"start", x:140, y:62, z:-225, f:"south", c:"function mq:commands/start with storage mq:main"}
data modify storage mq:main button_defs append value {n:"stop", x:142, y:62, z:-225, f:"south", c:"function mq:commands/stop with storage mq:main"}
data modify storage mq:main button_defs append value {n:"skip", x:144, y:62, z:-225, f:"south", c:"function mq:commands/skip"}
data modify storage mq:main button_defs append value {n:"hint", x:146, y:62, z:-225, f:"south", c:"function mq:commands/hint"}
data modify storage mq:main button_defs append value {n:"replay", x:148, y:62, z:-225, f:"south", c:"function mq:commands/replay"}
data modify storage mq:main button_defs append value {n:"test", x:144, y:62, z:-213, f:"north", c:"function mq:commands/test"}

View File

@@ -1,23 +0,0 @@
# 음악퀴즈 주제 — tellraw 접두사([ 이름 ])와 사이드바 표시에 사용
data modify storage mq:main title set value "음악퀴즈"
# 플레이어 접속 시 텔레포트 위치 (x y z, r=yaw, f=pitch)
data modify storage mq:main spawn set value {x: 144, y: 61, z: -219, r: 180, f: 0}
# 음원 재생 — minecraft_launcher 리소스팩의 musicquiz:track_NN 사운드 이벤트
# namespace — 리소스팩 네임스페이스 (기본 "musicquiz")
# source — /playsound 채널. stopsound 와 동일해야 함 (기본 "weather")
# volume — 기본 음량. 곡별 override 는 init/songs.mcfunction 의 volume 필드 사용
# pitch — 1.0 = 원본 속도
data modify storage mq:main audio set value {namespace: "musicquiz", source: "weather", volume: 1.0, pitch: 1.0}
# 정답 페인팅 — minecraft_launcher 리소스팩의 musicquiz:cover_NN painting_variant
# namespace — painting_variant 네임스페이스 (기본 "musicquiz")
# x,y,z — 페인팅 entity 좌표 (벽면 앞쪽 블록 위치)
# facing — 페인팅이 바라보는 방향: south=0 / west=1 / north=2 / east=3
data modify storage mq:main image set value {namespace: "musicquiz", x: 144, y: 84, z: -261, facing: 0b}
# 정답 입력용 marker entity 소환 좌표
data modify storage mq:main marker set value {x: 144, y: 59, z: -219}
# 곡 개수 max_index 는 init/songs.mcfunction 의 길이로 자동 계산됨

View File

@@ -1,5 +0,0 @@
data modify storage mq:main trigger_defs set value []
data modify storage mq:main trigger_defs append value {n:"stop", n2:"중지", c:"function mq:commands/stop with storage mq:main"}
data modify storage mq:main trigger_defs append value {n:"skip", n2:"스킵", c:"function mq:commands/skip"}
data modify storage mq:main trigger_defs append value {n:"hint", n2:"힌트", c:"function mq:commands/hint"}
data modify storage mq:main trigger_defs append value {n:"replay", n2:"다시재생", c:"function mq:commands/replay"}

View File

@@ -1,29 +0,0 @@
data modify storage mq:main answer set value {title:"", author:"", alias:[]}
data merge storage func:temp {}
data merge storage mq:tmp {}
function mq:init/config
function mq:init/songs
function mq:init/buttons
function mq:init/triggers
function mq:tellraw {"text":"서버 리로드 성공!","color":"white","msg":'""'}
scoreboard objectives remove func.temp
scoreboard objectives remove main
scoreboard objectives remove buttons
scoreboard objectives remove answer
scoreboard objectives remove leave_game
scoreboard objectives add func.temp dummy
scoreboard objectives add main dummy
scoreboard objectives add buttons dummy
scoreboard objectives add answer dummy
scoreboard objectives add leave_game custom:leave_game
scoreboard players set two func.temp 2
bossbar add mq:process [{"text":"진행도: ","color": "yellow","bold": true},{"text":"0","color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"text":"0","color": "yellow","bold": true}]
function mq:commands/stop with storage mq:main
function mq:players/login with storage mq:main spawn

View File

@@ -1,10 +0,0 @@
tag @s add player
scoreboard players reset @s leave_game
title @s times 10t 80t 10t
title @s subtitle ""
title @s title ""
$setworldspawn $(x) $(y) $(z) $(r) $(f)
$tp @s $(x) $(y) $(z) $(r) $(f)
gamemode adventure @s

View File

@@ -1,32 +0,0 @@
scoreboard players set init main 6
scoreboard players set @s answer 2
function mq:tellraw {"text":"","color":"black","msg":""}
function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답: ","color": "aqua"},{"storage":"mq:main","nbt":"answer.title","color": "yellow","bold": true}]}
function mq:tellraw {"text":"","color":"black",msg:[{"text":"가수: ","color":"aqua"},{"storage":"mq:main","nbt":"answer.author","color": "yellow","bold": true}]}
execute if score skip buttons matches -2 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답자: ","color": "aqua"},{"text":"스킵","color": "yellow","bold": true}]}
execute unless score skip buttons matches -2 run function mq:tellraw {"text":"","color":"black",msg:[{"text":"정답자: ","color": "aqua"},{"selector":"@s","color": "yellow","bold": true}]}
function mq:tellraw {"text":"","color":"black",msg:[{"text": "( 15초뒤 다음문제로 넘어갑니다. )","color": "gray"}]}
function mq:tellraw {"text":"","color":"black","msg":""}
title @a subtitle [{"text":"정답: ","color": "aqua"},{"storage":"mq:main","nbt":"answer.title","color": "yellow","bold": true}]
title @a title {"text":""}
scoreboard players set @a ready 0
scoreboard players set @a stop 0
scoreboard players set @a skip 0
scoreboard players set @a hint 0
scoreboard players set @a replay 0
execute if score skip buttons matches -2 run scoreboard players add score 1
execute unless score skip buttons matches -2 run scoreboard players add @s score 1
scoreboard players set stop buttons -3
scoreboard players set skip buttons -3
scoreboard players set hint buttons -3
scoreboard players set replay buttons -3
scoreboard players set timer main 1
function mq:images/show

View File

@@ -1,3 +0,0 @@
scoreboard players set init main 10
scoreboard players set timer main 1

View File

@@ -1 +0,0 @@
$execute as @a at @s run playsound $(namespace):$(track) $(source) @s ~ ~ ~ $(volume) $(pitch)

View File

@@ -1,3 +0,0 @@
$data modify storage mq:main answer set from storage mq:main songs[$(idx)]
$data modify storage mq:main answer.track set value "track_$(pad)$(num)"
$data modify storage mq:main answer.cover set value "cover_$(pad)$(num)"

View File

@@ -1,9 +0,0 @@
$execute unless data storage mq:main {answer:{title:"음악퀴즈"}} run summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"정답입력시작"}
$summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"$(name)"}
execute store result score length func.temp run data get storage mq:tmp marker_call.alias
execute if score length func.temp matches 1.. run data modify storage mq:tmp marker_call.name set from storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run data remove storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run function mq:quiz/macro/summon2 with storage mq:tmp marker_call
$execute unless data storage mq:main {answer:{title:"음악퀴즈"}} run summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"정답입력종료"}

View File

@@ -1,6 +0,0 @@
$summon minecraft:marker $(x) $(y) $(z) {Tags:["mq","default"],CustomName:"$(name)"}
execute store result score length func.temp run data get storage mq:tmp marker_call.alias
execute if score length func.temp matches 1.. run data modify storage mq:tmp marker_call.name set from storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run data remove storage mq:tmp marker_call.alias[0]
execute if score length func.temp matches 1.. run function mq:quiz/macro/summon2 with storage mq:tmp marker_call

View File

@@ -1,5 +0,0 @@
data modify storage mq:tmp playsound set from storage mq:main audio
data modify storage mq:tmp playsound.track set from storage mq:main answer.track
# 곡 단위 volume override — songs[i].volume 가 없으면 audio.volume 그대로 유지 (no-op)
data modify storage mq:tmp playsound.volume set from storage mq:main answer.volume
function mq:quiz/macro/play_sound with storage mq:tmp playsound

View File

@@ -1,20 +0,0 @@
scoreboard players set timer main 0
execute if score index main >= max_index main run return run function mq:quiz/end with storage mq:main
scoreboard players add index main 1
bossbar set mq:process name [{"text":"진행도: ","color": "yellow","bold": true},{"score":{"name":"index","objective": "main"},"color": "yellow","bold": true},{"text":"/","color": "yellow","bold": true},{"score":{"name":"max_index","objective": "main"},"color": "yellow","bold": true}]
bossbar set mq:process players @a
execute store result bossbar mq:process value run scoreboard players get index main
# tmp.{idx (0-based, songs[] 인덱스), num (1-based, track_NN), pad ("0"|"")} 구성
execute store result storage mq:tmp num int 1 run scoreboard players get index main
scoreboard players operation song_idx func.temp = index main
scoreboard players remove song_idx func.temp 1
execute store result storage mq:tmp idx int 1 run scoreboard players get song_idx func.temp
execute if score index main matches 1..9 run data modify storage mq:tmp pad set value "0"
execute unless score index main matches 1..9 run data modify storage mq:tmp pad set value ""
function mq:quiz/setanswer

View File

@@ -1,17 +0,0 @@
# songs[$(idx)] → answer 로 복사하고, 트랙/커버 id 부여
function mq:quiz/macro/setanswer with storage mq:tmp
# 정답 marker entity 소환 (좌표 + name/alias 합쳐서 macro 호출)
data modify storage mq:tmp marker_call set from storage mq:main marker
data modify storage mq:tmp marker_call.name set from storage mq:main answer.title
data modify storage mq:tmp marker_call.alias set from storage mq:main answer.alias
function mq:quiz/macro/summon with storage mq:tmp marker_call
scoreboard players set stop buttons -1
scoreboard players set skip buttons -1
scoreboard players set hint buttons -1
scoreboard players set replay buttons -1
scoreboard players set init main 5
function mq:quiz/play_sound

View File

@@ -1,6 +0,0 @@
scoreboard players set init main 2
scoreboard players set index main 0
bossbar set mq:process visible true
scoreboard players set timer main 1

View File

@@ -1 +0,0 @@
function mq:quiz/macro/stop_sound with storage mq:main audio

View File

@@ -1,28 +0,0 @@
$execute if score $(n) buttons matches ..-2 run setblock $(x) $(y) $(z) minecraft:air
$execute if score $(n) buttons matches ..-2 run data modify entity @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] response set value 0b
$execute if score $(n) buttons matches ..-2 run return 0
$execute unless score $(n) buttons matches -1.. run scoreboard players set $(n) buttons -1
$execute if score $(n) buttons matches -1 run setblock $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=false]
$execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) run setblock ~ ~-3 ~ minecraft:redstone_block
$execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) run setblock ~ ~-3 ~ minecraft:red_wool
$execute if score $(n) buttons matches -1 run scoreboard players set $(n) buttons 0
$execute if block $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=true] \
if score $(n) buttons matches 0 \
run scoreboard players set $(n) buttons 1
$execute if score $(n) buttons matches 1 unless entity @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] positioned $(x) $(y) $(z) run $(c)
$execute if score $(n) buttons matches 1 \
run scoreboard players set $(n) buttons 2
$execute if block $(x) $(y) $(z) minecraft:stone_button[face=wall,facing=$(f),powered=false] \
if score $(n) buttons matches 1.. \
run scoreboard players set $(n) buttons 0
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) run playsound minecraft:block.stone_button.click_on block @s ~ ~ ~ 1 1
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) if score init main matches 0 run $(c)
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n),limit=1] on target as @s positioned $(x) $(y) $(z) unless score init main matches 0 run trigger $(n)
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s attack
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s interaction

View File

@@ -1,6 +0,0 @@
function mq:repeat/buttons/btn with storage mq:main button_defs[0]
function mq:repeat/buttons/btn with storage mq:main button_defs[1]
function mq:repeat/buttons/btn with storage mq:main button_defs[2]
function mq:repeat/buttons/btn with storage mq:main button_defs[3]
function mq:repeat/buttons/btn with storage mq:main button_defs[4]
function mq:repeat/buttons/btn with storage mq:main button_defs[5]

View File

@@ -1,2 +0,0 @@
execute as @a[scores={answer=1}] run function mq:quiz/correct with storage mq:main answer
execute as @a[scores={answer=2}] run scoreboard players reset @a answer

View File

@@ -1,2 +0,0 @@
execute as @a[tag=!player] run function mq:players/login with storage mq:main spawn
execute as @a if score @s leave_game matches 1.. run function mq:players/login with storage mq:main spawn

View File

@@ -1,49 +0,0 @@
execute if score timer main matches 1.. run scoreboard players add timer main 1
execute unless score init main matches 2 \
unless score init main matches 6 \
unless score init main matches 10 \
run scoreboard players set timer main 0
# start title timer
execute if score init main matches 2 if score timer main matches 20 run title @a title {"text":"3"}
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 20 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 run title @a title {"text":"2"}
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 40 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 run title @a title {"text":"1"}
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 60 as @a at @s run playsound minecraft:block.note_block.iron_xylophone weather @s ~ ~ ~ 1 1
execute if score init main matches 2 if score timer main matches 100 run title @a title {"text":""}
execute if score init main matches 2 if score timer main matches 100.. run function mq:quiz/select with storage mq:main
# next song timer
execute if score init main matches 6 if score timer main matches 300 run title @a title {"text":""}
execute if score init main matches 6 if score timer main matches 290 run function mq:images/clear
execute if score init main matches 6 if score timer main matches 300.. run function mq:quiz/select with storage mq:main
# endding timer
execute if score init main matches 10 if score timer main matches 60 run function mq:tellraw {"text":"퀴즈가 종료되었습니다.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 60 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 run function mq:tellraw {"text":"퀴즈를 다시 시작하시려면 종료를 눌러주세요.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 120 as @a at @s run scoreboard players set stop buttons -1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 120 run function mq:tellraw {"text":"플레이 해주셔서 감사합니다.","color":"white","msg":""}
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 180 as @a at @s run playsound minecraft:ui.button.click weather @s ~ ~ ~ 1 1
execute if score init main matches 10 if score timer main matches 200.. run scoreboard players set init main 11

View File

@@ -1,24 +0,0 @@
execute if score init main matches 0..1 run scoreboard players enable @a ready
execute if score init main matches 0..1 as @a if score @s ready matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"준비완료","color":"white"}]}
execute if score init main matches 0..1 as @a if score @s ready matches 1 run scoreboard players set @s ready 2
execute if score init main matches 0..1 as @a if score @s ready matches 3 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"이미 준비완료 상태입니다.","color": "red"}]}
execute if score init main matches 0..1 as @a if score @s ready matches 3 run scoreboard players set @s ready 2
execute if score init main matches 0..1 run scoreboard players enable @a cancel
execute if score init main matches 0..1 as @a if score @s cancel matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true},{"text":" : ","color":"gray"},{"text":"취소를 선택하셨습니다.","color": "red"}]}
execute if score init main matches 0..1 as @a if score @s cancel matches 1 run function mq:commands/stop with storage mq:main
execute if score init main matches 0..1 store result score max_player ready if entity @a
execute if score init main matches 0..1 store result score ready_player ready if entity @a[scores={ready=2..}]
execute if score init main matches 0..1 \
unless score max_player ready matches 0 \
if score max_player ready = ready_player ready \
run function mq:quiz/start with storage mq:main
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[0]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[1]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[2]
function mq:repeat/triggers/trigger with storage mq:main trigger_defs[3]

View File

@@ -1,27 +0,0 @@
$scoreboard players enable @a $(n)
$execute unless score init main matches 5 as @a if score @s $(n) matches 1.. run scoreboard players reset @s $(n)
execute unless score init main matches 5 run return 0
$execute store result score real_max_player $(n) if entity @a
$execute store result score rest_player $(n) if entity @a
$execute unless score rest_player $(n) matches 0 run scoreboard players operation rest_player $(n) %= two func.temp
$execute store result score max_player $(n) if entity @a
$execute unless score real_max_player $(n) matches 0 run scoreboard players operation max_player $(n) /= two func.temp
$execute unless score real_max_player $(n) matches 0 run scoreboard players operation max_player $(n) += rest_player $(n)
$execute store result score $(n)_player $(n) if entity @a[scores={$(n)=2..}]
$execute store result score $(n)_player_add $(n) if entity @a[scores={$(n)=2..}]
$execute run scoreboard players add $(n)_player_add $(n) 1
$execute as @a if score @s $(n) matches 1 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true}," : ",{"text":"$(n2) 투표 완료","color": "white"}, \
{"text":" (","color":"gray"},{"score":{"name":"$(n)_player_add","objective": "$(n)"},"color":"gray"},{"text":"/","color":"gray"},{"score":{"name":"max_player","objective": "$(n)"},"color":"gray"},{"text":")","color":"gray"}]}
$execute as @a if score @s $(n) matches 1 run scoreboard players set @s $(n) 2
$execute as @a if score @s $(n) matches 3 run function mq:tellraw {"text":"","color":"black",msg:[{"selector":"@s","color": "yellow","bold": true}," : ",{"text":"이미 $(n2)투표를 하셨습니다.","color": "red"}]}
$execute as @a if score @s $(n) matches 3 run scoreboard players set @s $(n) 2
$execute store result score $(n)_player $(n) if entity @a[scores={$(n)=2..}]
$execute unless score real_max_player $(n) matches 0 \
if score max_player $(n) = $(n)_player $(n) \
run $(c)

View File

@@ -1 +0,0 @@
$tellraw @a ["",{"text":"[ ","bold":true,"color":"gray"},{"storage":"mq:main","nbt":"title","bold":true,"color":"dark_green"},{"text":" ]","bold":true,"color":"gray"},{"text":" "},{"text":"$(text)","color":"$(color)"},$(msg)]

View File

@@ -1,6 +0,0 @@
function mq:repeat/players
function mq:repeat/buttons/handler
function mq:repeat/triggers/handler
execute if score init main matches 2.. run function mq:repeat/timer
execute if score init main matches 5..6 run function mq:repeat/check_answer

View File

@@ -1,6 +0,0 @@
{
"pack": {
"pack_format": 75,
"description": "음악퀴즈용 데이터팩입니다."
}
}

View File

@@ -136,9 +136,12 @@
"pickedNone": "선택된 음악퀴즈 없음", "pickedNone": "선택된 음악퀴즈 없음",
"pickedLabel": "선택: {{name}}", "pickedLabel": "선택: {{name}}",
"totalCount": "총 {{count}}개의 음악을 찾았습니다.", "totalCount": "총 {{count}}개의 음악을 찾았습니다.",
"export": "데이터팩 zip 다운로드", "hint": "music_quiz 데이터팩의 data/mq/function/init/songs.mcfunction 파일에 아래 코드를 그대로 덮어쓰세요.",
"exporting": "다운로드 준비 중…", "export": "코드 출력",
"exported": "다운로드를 시작했습니다.", "copy": "복사",
"copied": "복사됨",
"exporting": "출력 중…",
"exported": "출력 완료",
"failed": "실패: {{message}}", "failed": "실패: {{message}}",
"modalPickTitle": "음악퀴즈 선택" "modalPickTitle": "음악퀴즈 선택"
}, },

View File

@@ -1,14 +1,5 @@
import path from 'node:path'
import { Readable } from 'node:stream'
import archiver from 'archiver'
import type { Response } from 'express'
import { fileDatapacksDirPath } from '../shared/paths.js'
import type { MusicListEntry, PackList } from '../shared/types.js' import type { MusicListEntry, PackList } from '../shared/types.js'
/** music_quiz/ 정적 템플릿 디렉터리. (songs.mcfunction 만 동적으로 생성) */
const TEMPLATE_DIR = path.join(fileDatapacksDirPath, 'music_quiz_template')
const SONGS_PATH_IN_ZIP = 'music_quiz/data/mq/function/init/songs.mcfunction'
/** SNBT 문자열 리터럴 안에 들어갈 문자열을 escape. */ /** SNBT 문자열 리터럴 안에 들어갈 문자열을 escape. */
function escapeSnbtString(input: string): string { function escapeSnbtString(input: string): string {
return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"') return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
@@ -30,7 +21,11 @@ function entrySnbt(entry: MusicListEntry): string {
return `{title:"${title}", author:"${author}", alias:${alias}}` return `{title:"${title}", author:"${author}", alias:${alias}}`
} }
/** list.music 으로부터 `data/mq/function/init/songs.mcfunction` 본문을 생성. */ /**
* list.music 으로부터 `data/mq/function/init/songs.mcfunction` 본문을 생성.
* 운영자는 mc_datapack 의 music_quiz 데이터팩에서 이 파일만 이 내용으로
* 덮어쓰면 된다 — 나머지 파일은 launcher 가 관여하지 않는다.
*/
export function buildSongsMcfunction(list: PackList): string { export function buildSongsMcfunction(list: PackList): string {
const lines: string[] = [] const lines: string[] = []
lines.push('# 곡 한 개 = 한 줄.') lines.push('# 곡 한 개 = 한 줄.')
@@ -48,32 +43,3 @@ export function buildSongsMcfunction(list: PackList): string {
lines.push('execute store result storage mq:main max_index int 1 run data get storage mq:main songs') lines.push('execute store result storage mq:main max_index int 1 run data get storage mq:main songs')
return lines.join('\n') + '\n' return lines.join('\n') + '\n'
} }
/** music_quiz 데이터팩 zip 을 Response 로 스트리밍. */
export function streamMusicQuizZip(res: Response, packKey: string, list: PackList): void {
const fileName = `music_quiz_${packKey}.zip`
res.setHeader('Content-Type', 'application/zip')
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`)
const archive = archiver('zip', { zlib: { level: 9 } })
archive.on('warning', (err) => {
if (err.code !== 'ENOENT') res.destroy(err)
})
archive.on('error', (err) => {
res.destroy(err)
})
archive.pipe(res)
// 정적 템플릿 전체를 music_quiz/ 아래로 묶되 songs.mcfunction 만 제외.
archive.glob('**/*', {
cwd: TEMPLATE_DIR,
dot: false,
ignore: ['data/mq/function/init/songs.mcfunction']
}, { prefix: 'music_quiz/' })
// 동적으로 만든 songs.mcfunction 을 추가.
const songsText = buildSongsMcfunction(list)
archive.append(Readable.from([songsText]), { name: SONGS_PATH_IN_ZIP })
void archive.finalize()
}

View File

@@ -17,7 +17,7 @@ import { fetchPlaylistEntries, fetchVideoMeta, YtDlpUnavailableError } from '../
import { requireAuth } from '../middleware/auth.js' import { requireAuth } from '../middleware/auth.js'
import type { PackDefinition, PackList } from '../../shared/types.js' import type { PackDefinition, PackList } from '../../shared/types.js'
import { t } from '../i18n.js' import { t } from '../i18n.js'
import { streamMusicQuizZip } from '../datapack.js' import { buildSongsMcfunction } from '../datapack.js'
export const opRouter = Router() export const opRouter = Router()
@@ -235,8 +235,8 @@ opRouter.get('/op/datapack', requireAuth, async (req, res, next) => {
} }
}) })
// 데이터팩 출력: mc_datapack 의 music_quiz/ 템플릿을 zip 으로 묶고, // 데이터팩 출력: list.music 으로부터 init/songs.mcfunction 본문만 만들어
// data/mq/function/init/songs.mcfunction 만 list.music 으로 새로 만들어 덮어쓴다. // text/plain 으로 반환한다. 운영자가 mc_datapack 의 해당 파일에 붙여넣는다.
opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, next) => { opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, next) => {
try { try {
const packKey = sanitizePackKey(pickFirstValue(req.params.packName)) const packKey = sanitizePackKey(pickFirstValue(req.params.packName))
@@ -246,7 +246,7 @@ opRouter.get('/op/datapack/:packName/generate', requireAuth, async (req, res, ne
return return
} }
const list = await loadPackList(packKey) const list = await loadPackList(packKey)
streamMusicQuizZip(res, packKey, list) res.type('text/plain; charset=utf-8').send(buildSongsMcfunction(list))
} catch (error) { } catch (error) {
next(error) next(error)
} }

View File

@@ -17,6 +17,8 @@
</div> </div>
</section> </section>
<p class="muted"><%= t('datapack.hint') %></p>
<section class="dpControls"> <section class="dpControls">
<button type="button" class="primaryButton" id="pickPackBtn"><%= t('datapack.pickPack') %></button> <button type="button" class="primaryButton" id="pickPackBtn"><%= t('datapack.pickPack') %></button>
<span class="muted" id="pickedLabel"><%= t('datapack.pickedNone') %></span> <span class="muted" id="pickedLabel"><%= t('datapack.pickedNone') %></span>
@@ -26,8 +28,11 @@
<section class="dpActions" hidden id="dpActions"> <section class="dpActions" hidden id="dpActions">
<button type="button" class="secondaryButton" id="exportBtn"><%= t('datapack.export') %></button> <button type="button" class="secondaryButton" id="exportBtn"><%= t('datapack.export') %></button>
<button type="button" class="secondaryButton" id="copyBtn"><%= t('datapack.copy') %></button>
<span class="statusText" id="dp-status"></span> <span class="statusText" id="dp-status"></span>
</section> </section>
<pre class="codeBlock" id="codeOut" hidden></pre>
</main> </main>
<!-- 음악퀴즈 선택 팝업 --> <!-- 음악퀴즈 선택 팝업 -->
@@ -85,16 +90,36 @@
document.getElementById('dpActions').hidden = false document.getElementById('dpActions').hidden = false
document.getElementById('dp-status').textContent = '' document.getElementById('dp-status').textContent = ''
document.getElementById('dp-status').classList.remove('error') document.getElementById('dp-status').classList.remove('error')
document.getElementById('codeOut').hidden = true
document.getElementById('codeOut').textContent = ''
}) })
}) })
document.getElementById('exportBtn').addEventListener('click', function () { document.getElementById('exportBtn').addEventListener('click', function () {
if (!pickedKey) return if (!pickedKey) return
var s = document.getElementById('dp-status') var s = document.getElementById('dp-status')
s.textContent = I18N.exporting; s.classList.remove('error') s.textContent = I18N.exporting; s.classList.remove('error')
// zip 다운로드를 트리거하기 위해 location 으로 이동시킨다. fetch('/op/datapack/' + encodeURIComponent(pickedKey) + '/generate')
window.location.href = '/op/datapack/' + encodeURIComponent(pickedKey) + '/generate' .then(function (r) { return r.text().then(function (t) { return { ok: r.ok, text: t } }) })
// 다운로드는 비동기 / 별도 응답이므로 약간의 지연 후 "완료" 표시. .then(function (res) {
setTimeout(function () { s.textContent = I18N.exported }, 500) if (!res.ok) {
s.textContent = I18N.failed.replace('{{message}}', res.text); s.classList.add('error')
return
}
var out = document.getElementById('codeOut')
out.textContent = res.text
out.hidden = false
s.textContent = I18N.exported
})
.catch(function (err) { s.textContent = I18N.failed.replace('{{message}}', err.message); s.classList.add('error') })
})
document.getElementById('copyBtn').addEventListener('click', function () {
var out = document.getElementById('codeOut')
if (out.hidden) return
navigator.clipboard.writeText(out.textContent).then(function () {
var s = document.getElementById('dp-status')
s.textContent = I18N.copied
s.classList.remove('error')
})
}) })
})() })()
</script> </script>