16 Commits

Author SHA1 Message Date
claude
f0a2e4fb6b music_quiz: [정답 입력] 채팅 메시지 제거 + 외부 모드 설치 검증 가드 추가
- quiz/setanswer.mcfunction: 클릭형 tellraw 제거. dialog/`/trigger input` 인프라는 유지하여 모드 없는 환경 fallback.
- 외부 모드 검증 (load/login/start):
  - mq_chat_mod: 서버 전용 모드 (mc_chat_answer_mod) — `#server mq_chat_mod` fake holder 로 서버 presence 검증.
  - mq_video_mod: 클라이언트 렌더링 모드 (mc_video_player_mod) — 같은 objective 안에 `#server` (서버 컴포넌트 매 tick 갱신) + `<player>` (클라 payload 수신 시 갱신) 두 holder 로 server/client 부재 안내 분리.
  - start.mcfunction: server presence 우선 검사 → per-player client presence 검사. unset 매치 안 되는 selector 이슈는 `add @a ... 0` 으로 materialize.
  - login.mcfunction: 플레이어 join 시 `mq_video_mod=0` 초기화 (stale 1 방지).
- docs/mc_video_player_mod_integration.md: video mod 측 구현 사양 (서버 컴포넌트 매 tick presence pulse + client payload handshake, 주기 재전송 필수 명시).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:29:13 +09:00
Claude (owner)
2f6dc17092 music_quiz: interaction hitbox 를 stone_button 면 크기에 정합
interaction entity 의 위치/크기를 facing 별 오프셋 + 버튼 hitbox
치수 (6/16 × 4/16) 에 맞춰 분리.

- button_defs 각 항목에 ox/oy/oz/w/h 추가. facing 별 보정값으로
  interaction 의 "튀어나온 쪽 면 = 버튼 visible face" 가 되게 함.
  반대편은 벽 블록 속으로 들어가 invisible.
- width=0.375f, height=0.25f → 가로/세로 버튼 face 와 정합.
  horizontal hitbox 가 square 강제라 두께는 0.375 까지 가지만,
  벽 쪽 0.25 가 wall 블록 안에 묻혀 시각적으로는 버튼 두께 0.125 만 튀어나옴.
- btn.mcfunction 의 summon 라인이 매크로 변수 (~$(ox)/$(oy)/$(oz)) 와
  $(w)f / $(h)f 사용으로 변경됨.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:23:30 +09:00
Claude (owner)
c39a0516bc music_quiz: interaction 엔티티 소환을 데이터팩 내부로 흡수
월드 cmd block 의존 (redstone_block/red_wool 펄스) 을 제거하고
btn.mcfunction 이 직접 summon 하도록 변경.

- buttons=-1 초기화 단계에서 기존 mq/<버튼명> interaction 을 모두
  kill 후 정확히 1개를 (x+0.5, y, z+0.5) 에 1f×1f 로 재소환.
  /reload 마다 dup 누적 없이 "버튼당 1개, 올바른 좌표" 로 수렴.
- /reload → load → commands/stop 이 buttons 점수를 -1 로 재설정 →
  다음 tick 에 ensure 로직 실행. /kill @e 후에도 /reload 한 번으로 복구.
- stone_button 직접 감지 fallback 및 잉여 state machine (1→2→0)
  제거. 클릭 경로는 interaction 단일화 → trigger 투표 흐름 보존.
- README 의 버튼 본체 설명을 새 구조로 갱신.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 21:07:31 +09:00
Claude (owner)
cce5469dc2 music_quiz: answer 정규화 도입 (대소문자/공백 무시)
- mq:answer/normalize: storage 의 norm.in 을 한 글자씩 떼어내
  소문자화 + 공백 제거 후 norm.acc 에 누적. char 단위 반복은
  'data modify ... set string from ... <start> <end>' (1.20+) 로,
  결합은 매크로 ($(acc)$(c)) 로 수행하는 pure-datapack 구현.

- process: 큐의 text 를 정규화한 결과를 judge.input 으로 사용
- judge: answer.title 정규화 후 judge.answer 로 사용
- iter_aliases: alias 각 항목 정규화 후 비교

원본 songs.mcfunction 의 title/alias 표기는 그대로 유지 (display
및 정규화 입력으로 모두 사용됨). 입력이 'Lose My Mind' / 'lose my mind'
/ 'LOSEMYMIND' / 'losemymind' 어떤 형태든 동일한 정규형으로 떨어져 매치.
2026-05-15 00:23:42 +09:00
Claude (owner)
8c30f4de5e temp: 애니메이션 painting 텍스처 검증용 sample 파일 추가
리소스팩 1번 방식 (.png 세로 스트립 + .png.mcmeta) 이
painting_variant 에서 동작하는지 검증하기 위한 샘플.

사용 위치: musicquiz 리소스팩의
  assets/musicquiz/textures/painting/gif.png
  assets/musicquiz/textures/painting/gif.png.mcmeta

데이터팩 v1.0.6 의 mq:gif painting_variant 와 짝.
2026-05-14 23:58:07 +09:00
Claude (owner)
ae434c3a07 music_quiz: answer/macro/match — NBT path compound matcher 공백 제거
execute if data <source> <path> 의 path 토큰은 공백을 허용하지 않음.
'judge {input:...}' → 'judge{input:...}' 로 붙여써야 compound predicate 가
정상 적용됨.
2026-05-14 23:50:28 +09:00
Claude (owner)
663891c966 music_quiz: 애니메이션 텍스처 테스트용 painting_variant gif 추가
리소스팩 musicquiz:gif 텍스처가 .png.mcmeta 애니메이션을 따르는지
검증하기 위한 1×1 painting_variant. 텍스처 자체는 리소스팩에 별도 배치.
2026-05-14 23:39:20 +09:00
Claude (owner)
c2dcf0c44f music_quiz: painting_variant 를 mq 네임스페이스로 통합
- data/musicquiz/painting_variant/* → data/mq/painting_variant/* 로 이동
  변종 ID = mq:cover_NN, 텍스처 asset_id = musicquiz:cover_NN (리소스팩)
- title/author 필드 제거 (기본값 사용)
- init/config.mcfunction 의 image.namespace 기본값을 "mq" 로 변경
2026-05-14 23:23:29 +09:00
Claude (owner)
f71bd95de5 music_quiz: pack.mcmeta 에 min_format / max_format 명시 (26.1.2 = format 75)
25w31a 이후 pack metadata 에서 min_format / max_format 가 권장 필드로
추가됨. 없으면 게임 시작 시 PackRepository.reload 단계에서
"missing mandatory fields min_format and max_format" WARN 로그가
fallback 으로 처리되며, 향후 버전에서 hard fail 로 바뀔 가능성이
있어 명시. 단일 버전(26.1.2) 만 지원하므로 75/75 로 고정.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 23:04:43 +09:00
Claude (owner)
b19f37969a music_quiz: add painting_variant definitions (cover_01 ~ cover_50)
리소스팩의 assets/musicquiz/textures/painting/cover_NN.png 를 게임에서
painting entity 로 띄우려면 데이터팩 쪽에 painting_variant 정의가
있어야 한다. 곡 수(50)에 맞춰 data/musicquiz/painting_variant/cover_NN.json
50개를 추가. asset_id 의 musicquiz:cover_NN 이 자동으로
assets/musicquiz/textures/painting/cover_NN.png 를 가져다 쓴다.
width/height = 1×1 (정사각 한 블록).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 22:57:49 +09:00
Claude (owner)
416eaee14a music_quiz: chat_answer 모드 활성 알림 — race-free 함수 호출 방식으로 전환
기존: storage chat_answer:status active 1b/0b 플래그를 모드가 set,
데이터팩이 mq:load 에서 0b 로 reset 하는 구조였는데,
통합 서버에서 mq:load 가 player join 이후에 도는 케이스가 발견되어
모드가 써놓은 1b 를 데이터팩이 직후에 0b 로 덮어쓰는 race 발생.

수정: storage 플래그를 완전히 제거. 모드가 직접 PlayerLoggedInEvent
핸들러에서 mq:players/mod_active_notice 함수를 해당 플레이어로 호출.
데이터팩이 없으면 함수가 없어 silent fail → race 없음.

- mq:load: chat_answer:status 0b 초기화 라인 삭제
- mq:players/login: 조건부 tellraw 제거 (모드가 직접 호출하므로)
- mq:players/mod_active_notice: 새 함수, 그냥 tellraw 만 수행
2026-05-14 03:14:55 +09:00
Claude (owner)
de6e040623 music_quiz: add pack.png icon 2026-05-14 02:28:52 +09:00
Claude (owner)
141bc5eaa5 music_quiz: fix invalid dialog type — simple_input_form → notice
mq/dialog/answer.json 이 'minecraft:simple_input_form' 타입을 쓰는데
1.21.6 dialog registry 에 그 타입이 없어서 데이터팩 로드 자체가 실패
"세계를 불러올 수 없습니다" 오류. 유효한 타입 중 단일 버튼 + inputs 를
지원하는 minecraft:notice 로 교체, action 을 button(label+inner action)
구조로 감싼다. dynamic/run_command 템플릿은 그대로 — $(text) 매크로 치환.
2026-05-13 23:36:29 +09:00
Claude (owner)
083297bf50 music_quiz: songs 의 volume 을 필수 기본 정보로 승격 (전 곡 기본 1.0)
기존엔 volume 이 선택 필드여서 미지정 시 config.audio.volume 으로 fallback 되었는데,
곡마다 음량 조절을 자주 하기 위해 모든 곡 엔트리에 volume 을 명시 (모두 1.0). 런타임
quiz/play_sound 흐름은 그대로 — songs[i].volume 을 그대로 사용.
2026-05-13 22:27:17 +09:00
Claude (owner)
cfe6a41888 music_quiz: chat_answer 모드 활성화 시 로그인 메세지 표시
mq:load 가 storage chat_answer:status active 를 0b 로 초기화. chat_answer 모드의
PlayerLoggedInEvent / ServerPlayConnectionEvents.JOIN 핸들러가 첫 로그인 직후
1b 로 set 하므로, mq:players/login 에서 1b 인 경우 "[채팅정답] 모드가 활성화되어
있습니다. 정답 입력 시 채팅으로 바로 제출할 수 있습니다." tellraw.
2026-05-13 22:21:17 +09:00
Claude (owner)
7adcda68de music_quiz: dialog 기반 정답 입력 + 큐 기반 first-submit-wins 추가
정답 입력 UI 를 dialog 로 제공 (mq:dialog/answer), #minecraft:quick_actions 태그
등록으로 빠른행동키에서 바로 열 수 있게 함. 동시 제출 시 먼저 제출한 사람이 정답으로
인정되도록 mq:answer/ 에 submit_seq 기반 FIFO 큐 + 매크로 기반 제출자 lookup 으로
처리. tick/stop/setanswer 도 새 큐 흐름에 맞춰 업데이트.
2026-05-13 22:20:58 +09:00
80 changed files with 828 additions and 80 deletions

View File

@@ -77,8 +77,17 @@
- `start` / `stop` / `skip` / `hint` / `replay` / `test` - `start` / `stop` / `skip` / `hint` / `replay` / `test`
버튼 본체는 `interaction` 엔티티 + `redstone_block`-`red_wool` 토글 버튼 본체는 보이는 `stone_button` 블록 + 그 좌표에 덮인 `interaction`
패턴으로 디바운스를 처리한다. 엔티티로 구성된다. 클릭 처리는 항상 `interaction` 경로로 흐르므로
`on target as @s` 로 누른 플레이어가 식별되고, 다수결(`trigger $(n)`)
투표가 성립한다.
`interaction` 은 데이터팩이 직접 소환·관리한다 — `buttons` 점수가
`-1` (초기화) 일 때마다 같은 태그의 기존 entity 를 정리하고 정확히
1개를 (재)소환한다. `/reload``commands/stop` 을 호출해 `buttons`
점수를 `-1` 로 재설정하므로, 리로드 시 자동 보장된다. `/kill @e`
지워졌어도 다음 `/reload` 한 번으로 복구. 월드 회로(커맨드블럭) 의존은
없다.
### 파일 구조 ### 파일 구조

View File

@@ -0,0 +1,184 @@
# mc_video_player_mod — 음악퀴즈 데이터팩 연동 사양
음악퀴즈 데이터팩(`music_quiz`)이 영상재생 모드의 **서버 측 + 클라이언트 측**
설치 여부를 둘 다 검증할 수 있도록, 모드의 서버 컴포넌트가 두 가지 점수를
갱신한다.
| holder | 의미 | 갱신 주체 / 시점 |
|--------|------|------------------|
| `#server mq_video_mod` | 서버에 모드 jar 존재 | 서버 컴포넌트가 매 server tick 마다 1 |
| `<player> mq_video_mod` | 해당 플레이어 클라에 모드 존재 | 서버 컴포넌트가 client→server payload 수신 시 1 |
둘 다 같은 objective `mq_video_mod` (dummy) 를 쓰고 holder 이름으로 의미를
구분한다. 데이터팩 `mq:commands/start` 가드는 두 단계로 검사:
서버 부재 → 전원 안내 후 차단 / 일부 플레이어 클라 부재 → 본인 안내 + 차단.
> 참고: 자매 모드 `mc_chat_answer_mod` 는 채팅 가로채기가 본질적으로 서버
> 측 동작이라 `#server mq_chat_mod` 한 가지만 쓴다 (per-player handshake 무의미).
> 본 모드는 클라이언트가 직접 영상 렌더링하므로 per-player handshake 도 필요함.
## 데이터팩 측이 이미 제공하는 것
`music_quiz/data/mq/function/load.mcfunction`
```
scoreboard objectives add mq_video_mod dummy
```
`music_quiz/data/mq/function/players/login.mcfunction` 에서 로그인 시 0
으로 초기화 (handshake 없는 플레이어는 0 유지).
```
scoreboard players set @s mq_video_mod 0
```
`mq:commands/start.mcfunction` 가드:
```mcfunction
# 서버 부재 우선 차단
execute unless score #server mq_video_mod matches 1 run return run function mq:tellraw {"text":"영상재생 모드가 서버에 미설치 — 서버 관리자에게 문의해주세요.","color":"red","msg":""}
# unset 매치 안 되므로 materialize
scoreboard players add @a mq_video_mod 0
# 본인 안내 (tellraw @s 직접 — mq:tellraw 는 @a broadcast 라 부적합)
execute as @a[scores={mq_video_mod=..0}] run tellraw @s ["",{"text":"영상재생 모드 미설치 — 모드 적용 후 다시 입장해주세요.","color":"red"}]
# 한 명이라도 누락이면 시작 차단
execute if entity @a[scores={mq_video_mod=..0}] run return run function mq:tellraw {"text":"필수 모드 미설치 플레이어가 있어 시작할 수 없습니다.","color":"red","msg":""}
```
`load.mcfunction` 에서 `#server mq_video_mod = 0` 으로 미리 깔아둠 → 서버
컴포넌트가 한 tick 도 안 돌면 0 유지 → 가드 차단.
즉, **서버 컴포넌트가 (a) 매 tick `#server mq_video_mod=1` 갱신,
(b) client payload 수신 시 송신 플레이어 점수=1 갱신**, 이 두 가지만 하면
나머지는 데이터팩이 알아서 한다.
## 모드가 구현해야 하는 동작
### 권장: 커스텀 payload handshake (Fabric Networking API / NeoForge Network)
가장 깔끔하고 위변조 방어도 자연스러움. `/trigger` 방식보다 추천.
#### Payload 정의 (공용)
식별자: `mq_video_mod:hello` (또는 자체 modid 네임스페이스).
페이로드 본문은 비어도 되고, 버전 정수 한 개 정도면 충분.
```java
public record HelloPayload(int version) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<HelloPayload> TYPE =
new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath("mq_video_mod", "hello"));
public static final StreamCodec<FriendlyByteBuf, HelloPayload> CODEC =
StreamCodec.composite(ByteBufCodecs.VAR_INT, HelloPayload::version, HelloPayload::new);
@Override public Type<?> type() { return TYPE; }
}
```
#### 클라이언트 측
- payload 를 `PayloadTypeRegistry.playC2S()` 에 등록 (Fabric) /
`IPayloadRegistrar` 에 등록 (NeoForge).
- `ClientPlayConnectionEvents.JOIN` (Fabric) 또는 동등 NeoForge 이벤트에서
서버로 1회 전송 + **이후 5초마다 주기적으로 재전송 (필수)**. 데이터팩
`mq:players/login` 이 spawn dialog 통과 시점에 점수를 0 으로 리셋하기
때문에, JOIN 시점의 1 회 전송만으로는 login 의 리셋이 뒤에 들어와 가드
통과가 실패한다. 주기 재전송으로 login 이후 늦어도 다음 5 초 안에 다시
1 로 복구되어야 정상 동작.
```java
// JOIN 직후 1 회
ClientPlayNetworking.send(new HelloPayload(1));
// + 주기 재전송 (필수). ClientTickEvents.END_CLIENT_TICK 등에서 카운터.
// 100 tick = 5 초 (client 20 tps). 너무 빈번해도 부담 없으니 1~5 초
// 사이로 자유롭게.
```
#### 서버 측 (이 모드의 서버 컴포넌트)
서버 컴포넌트는 **두 가지** 를 한다.
(1) 매 server tick 마다 `#server mq_video_mod = 1` 갱신 (server presence).
점수 값이 변하지 않으면 packet 미전송이므로 매 tick 호출해도 비용 없음.
```java
public static void onServerTick(MinecraftServer server) {
Scoreboard sb = server.getScoreboard();
Objective obj = sb.getObjective("mq_video_mod");
if (obj == null) return; // 데이터팩 미적용 or 아직 load 전
sb.getOrCreatePlayerScore(ScoreHolder.forNameOnly("#server"), obj).set(1);
}
```
Fabric: `ServerTickEvents.END_SERVER_TICK.register(...)`.
NeoForge: `NeoForge.EVENT_BUS.addListener(ServerTickEvent.Post::...)`.
(2) payload 등록 + 수신 시 송신 플레이어 점수 갱신 (client presence).
같은 payload 를 `PayloadTypeRegistry.playC2S()` 에 등록한 뒤:
```java
ServerPlayNetworking.registerGlobalReceiver(HelloPayload.TYPE, (payload, context) -> {
ServerPlayer player = context.player();
MinecraftServer server = player.getServer();
if (server == null) return;
Scoreboard sb = server.getScoreboard();
Objective obj = sb.getObjective("mq_video_mod");
if (obj == null) return; // 데이터팩 미적용 or 아직 load 전
// ServerPlayer 자체가 ScoreHolder — @s selector 와 정확히 매칭
sb.getOrCreatePlayerScore(player, obj).set(1);
});
```
> 참고 구현: `mc_chat_answer_mod` 의 `ChatAnswerCore::markModPresence` 가
> 똑같이 `#server` holder 패턴을 쓴다 (단 그 쪽은 client payload 부분 없음).
#### 타이밍 / 안전망
- 데이터팩 `mq:players/login` 이 spawn dialog 통과 시점에 점수를 0 으로
리셋하므로, **클라 payload 주기 재전송은 필수**. login 이후 다음 재전송
까지의 짧은 공백에 호스트가 start 를 누르면 가드가 차단되니, 재전송
간격은 5 초 이하 권장.
- 서버가 set 한 점수는 다음 join 시 login 에서 다시 0 으로 리셋됨 → 모드를
빼고 재접속한 플레이어가 stale 1 을 가질 일 없음.
- payload 송신 → 서버 처리는 ms 단위 → 주기 재전송이 살아 있는 한 start
타이밍 race 없음.
### 대안: `/trigger` 방식 (별도 서버 컴포넌트 불필요)
데이터팩이 trigger objective 를 만들고 클라 모드가 `/trigger mq_video_mod set 1`
을 채팅 명령으로 전송하는 방식. 단점:
- 데이터팩에 `scoreboard objectives add mq_video_mod trigger` 와 매 join 시
`scoreboard players enable @a mq_video_mod` 가 추가로 필요 (현재는 dummy).
- 클라 모드가 commands 권한으로 채팅 명령을 보내야 함.
- 명령어 packet 이 chat history 에 흔적이 남을 수 있음.
커스텀 payload 가 추천이지만, 서버 컴포넌트를 추가하기 싫다면 이 경로도
가능. 그 경우 데이터팩 측 변경이 필요하니 별도 요청해주세요.
## 동작 흐름 (권장 경로)
1. 서버 시작 → 데이터팩 load → `mq_video_mod` objective 생성.
2. 클라(모드 있음) 접속 → `ClientPlayConnectionEvents.JOIN` → payload 1 회 전송.
3. 서버 모드 수신 → 해당 플레이어 `mq_video_mod = 1`.
4. 플레이어 spawn dialog 통과 → `mq:players/login` 이 점수를 0 으로 리셋.
5. **클라 모드가 주기 재전송 (5 초 이하 권장, 필수)** → 늦어도 다음 주기에
서버 모드가 다시 `mq_video_mod = 1` 로 갱신.
6. 호스트 start → 가드가 `@a[scores={mq_video_mod=..0}]` 검사 → 클라 모드
미설치 플레이어 있으면 시작 차단.
## 테스트
1. **서버 미설치**: 서버에 모드 jar 가 없는 상태에서 데이터팩만 적용 →
호스트 start → "영상재생 모드가 서버에 미설치 — 서버 관리자에게..." 한
줄 출력 후 차단. (`#server mq_video_mod` 가 갱신되지 않음.)
2. **서버 설치 + 일부 클라 미설치**: 모드를 클라에 설치한 플레이어와 안
한 플레이어 혼재 → 호스트 start → 클라 미설치 본인에게 "영상재생 모드
미설치" + 전원에게 "필수 모드 미설치 플레이어가 있어..." 후 차단.
3. **서버 + 모든 클라 설치**: 모두 정상 → start 정상 진행.
4. `/scoreboard players list #server` 로 server presence 점수 확인,
`/scoreboard players list <player>` 로 client presence 점수 확인.
## 참고: 자매 모드 `mc_chat_answer_mod` 의 다른 접근
`mc_chat_answer_mod/common/.../ChatAnswerCore.java::markModPresence` 참고.
서버 전용 모드라 fake player `#server` 한 곳에만 set 한다. 클라이언트
렌더링 모드인 본 모드는 이 패턴이 아닌 per-player handshake 가 정답.

View File

@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"mq:answer"
]
}

View File

@@ -0,0 +1,32 @@
{
"type": "minecraft:notice",
"title": {
"text": "정답 입력",
"bold": true
},
"body": [],
"inputs": [
{
"type": "minecraft:text",
"key": "text",
"label": {
"text": "정답"
},
"width": 300,
"max_length": 64,
"initial": ""
}
],
"can_close_with_escape": true,
"pause": false,
"after_action": "close",
"action": {
"label": {
"text": "제출"
},
"action": {
"type": "minecraft:dynamic/run_command",
"template": "function mq:answer/submit {text:'$(text)'}"
}
}
}

View File

@@ -0,0 +1,12 @@
# aliases 배열 첫 원소와 비교 → 매치 시 즉시 종료, 아니면 pop 후 재귀
execute store result score alen func.temp run data get storage mq:tmp aliases
execute if score alen func.temp matches 0 run return 0
data modify storage mq:tmp norm.in set from storage mq:tmp aliases[0]
function mq:answer/normalize
data modify storage mq:tmp judge.answer set from storage mq:tmp norm.acc
function mq:answer/macro/match with storage mq:tmp judge
data remove storage mq:tmp aliases[0]
execute if score @s answer matches 1 run return 0
function mq:answer/iter_aliases

View File

@@ -0,0 +1,19 @@
# 제출자(@s) 로 실행됨 — answer.title 및 alias 들과 비교
# 매치되면 @s answer = 1 → check_answer 가 정답처리 흐름으로 진입
# 매치 안되면 @s answer = 2 → check_answer 가 reset 처리 (1회 비교 후 초기화)
# 1) 제목과 비교 (정규화 후)
data modify storage mq:tmp norm.in set from storage mq:main answer.title
function mq:answer/normalize
data modify storage mq:tmp judge.answer set from storage mq:tmp norm.acc
function mq:answer/macro/match with storage mq:tmp judge
# 2) 제목 매치 실패 시 alias 들과 순차 비교 (조기 종료)
execute unless score @s answer matches 1 run data modify storage mq:tmp aliases set from storage mq:main answer.alias
execute unless score @s answer matches 1 run function mq:answer/iter_aliases
# 3) 끝까지 매치 실패면 오답 처리 (check_answer 가 reset)
execute unless score @s answer matches 1 run scoreboard players set @s answer 2
# 4) 처리 완료 — 이 제출자의 submit_seq 정리 (다음 큐 항목과 혼동 방지)
scoreboard players reset @s submit_seq

View File

@@ -0,0 +1,2 @@
# queue entry 의 seq 점수를 가진 플레이어(@s) 컨텍스트로 judge 실행
$execute as @a[scores={submit_seq=$(seq)},limit=1] run function mq:answer/judge

View File

@@ -0,0 +1,3 @@
# judge.input 과 judge.answer 가 같으면 @s answer = 1
# 매크로 치환으로 answer 필드를 NBT predicate 의 리터럴로 박아넣음
$execute if data storage mq:tmp judge{input:"$(answer)"} run scoreboard players set @s answer 1

View File

@@ -0,0 +1,13 @@
# 입력 문자열 정규화 — 소문자 변환 + 공백 제거 (대소문자/공백 무시 비교용)
#
# 입력 : storage mq:tmp norm.in (원본 문자열)
# 출력 : storage mq:tmp norm.acc (정규화 결과)
#
# 호출 예:
# data modify storage mq:tmp norm.in set from storage mq:main answer.title
# function mq:answer/normalize
# # 이후 storage mq:tmp norm.acc 에 결과
#
# 동작: norm.in 을 한 글자씩 떼어내며 normalize/step 으로 재귀
data modify storage mq:tmp norm.acc set value ""
function mq:answer/normalize/step

View File

@@ -0,0 +1,3 @@
# acc 끝에 c 를 매크로로 concat
# 매크로 인자: storage mq:tmp norm → $(acc), $(c)
$data modify storage mq:tmp norm.acc set value "$(acc)$(c)"

View File

@@ -0,0 +1,47 @@
# normalize 의 1-문자 처리 + 재귀 스텝
# 사전조건: storage mq:tmp norm.in 비어있지 않을 수 있음 / norm.acc 누적값
execute store result score n.len func.temp run data get storage mq:tmp norm.in
execute if score n.len func.temp matches 0 run return 0
# 머리글자 추출 → norm.c
data modify storage mq:tmp norm.c set string from storage mq:tmp norm.in 0 1
# 공백 제거 (스킵)
execute if data storage mq:tmp norm{c:" "} run data modify storage mq:tmp norm.c set value ""
# 대문자 A-Z → 소문자 a-z
execute if data storage mq:tmp norm{c:"A"} run data modify storage mq:tmp norm.c set value "a"
execute if data storage mq:tmp norm{c:"B"} run data modify storage mq:tmp norm.c set value "b"
execute if data storage mq:tmp norm{c:"C"} run data modify storage mq:tmp norm.c set value "c"
execute if data storage mq:tmp norm{c:"D"} run data modify storage mq:tmp norm.c set value "d"
execute if data storage mq:tmp norm{c:"E"} run data modify storage mq:tmp norm.c set value "e"
execute if data storage mq:tmp norm{c:"F"} run data modify storage mq:tmp norm.c set value "f"
execute if data storage mq:tmp norm{c:"G"} run data modify storage mq:tmp norm.c set value "g"
execute if data storage mq:tmp norm{c:"H"} run data modify storage mq:tmp norm.c set value "h"
execute if data storage mq:tmp norm{c:"I"} run data modify storage mq:tmp norm.c set value "i"
execute if data storage mq:tmp norm{c:"J"} run data modify storage mq:tmp norm.c set value "j"
execute if data storage mq:tmp norm{c:"K"} run data modify storage mq:tmp norm.c set value "k"
execute if data storage mq:tmp norm{c:"L"} run data modify storage mq:tmp norm.c set value "l"
execute if data storage mq:tmp norm{c:"M"} run data modify storage mq:tmp norm.c set value "m"
execute if data storage mq:tmp norm{c:"N"} run data modify storage mq:tmp norm.c set value "n"
execute if data storage mq:tmp norm{c:"O"} run data modify storage mq:tmp norm.c set value "o"
execute if data storage mq:tmp norm{c:"P"} run data modify storage mq:tmp norm.c set value "p"
execute if data storage mq:tmp norm{c:"Q"} run data modify storage mq:tmp norm.c set value "q"
execute if data storage mq:tmp norm{c:"R"} run data modify storage mq:tmp norm.c set value "r"
execute if data storage mq:tmp norm{c:"S"} run data modify storage mq:tmp norm.c set value "s"
execute if data storage mq:tmp norm{c:"T"} run data modify storage mq:tmp norm.c set value "t"
execute if data storage mq:tmp norm{c:"U"} run data modify storage mq:tmp norm.c set value "u"
execute if data storage mq:tmp norm{c:"V"} run data modify storage mq:tmp norm.c set value "v"
execute if data storage mq:tmp norm{c:"W"} run data modify storage mq:tmp norm.c set value "w"
execute if data storage mq:tmp norm{c:"X"} run data modify storage mq:tmp norm.c set value "x"
execute if data storage mq:tmp norm{c:"Y"} run data modify storage mq:tmp norm.c set value "y"
execute if data storage mq:tmp norm{c:"Z"} run data modify storage mq:tmp norm.c set value "z"
# acc = acc + c (매크로 결합)
function mq:answer/normalize/append with storage mq:tmp norm
# 나머지로 진행
data modify storage mq:tmp norm.in set string from storage mq:tmp norm.in 1
function mq:answer/normalize/step

View File

@@ -0,0 +1,4 @@
# trigger input 으로 호출됨 — 다음 trigger 사용을 위해 즉시 재활성화 후 dialog 표시
scoreboard players reset @s input
scoreboard players enable @s input
dialog show @s mq:answer

View File

@@ -0,0 +1,21 @@
# tick 에서 매 tick 호출됨 — 큐의 가장 먼저 제출된 항목 1개를 처리
# 1 tick 당 1건 처리 — 동시 다발 제출은 FIFO 순서로 자동 직렬화됨
execute store result score qlen func.temp run data get storage mq:input queue
execute if score qlen func.temp matches 0 run return 0
# 첫번째 항목 = 가장 먼저 제출된 것
data modify storage mq:tmp judge set value {input:"", answer:""}
# 입력 정규화 (소문자 + 공백제거) — 정답과 비교는 둘 다 정규화된 형태로
data modify storage mq:tmp norm.in set from storage mq:input queue[0].text
function mq:answer/normalize
data modify storage mq:tmp judge.input set from storage mq:tmp norm.acc
# 매크로로 해당 seq 를 가진 플레이어 찾아서 judge 실행
data modify storage mq:tmp _find set value {seq:0}
data modify storage mq:tmp _find.seq set from storage mq:input queue[0].seq
# 항목 제거 (한 번 비교했으면 큐에서 빼기)
data remove storage mq:input queue[0]
function mq:answer/macro/find_submitter with storage mq:tmp _find

View File

@@ -0,0 +1,14 @@
# 정답 dialog 제출 핸들러 — 매크로 인자 {text:"..."} 받음
# 퀴즈 진행 중(init=5) 이 아니면 제출 무시
execute unless score init main matches 5 run return 0
# 제출 순번 부여 — seq 는 setanswer 에서 0 으로 초기화됨
# 동일 tick 내 동시 제출도 명령 실행 순서대로 1씩 증가하여 고유한 seq 가 부여됨
scoreboard players add seq func.temp 1
scoreboard players operation @s submit_seq = seq func.temp
# 큐에 {text, seq} append — process 는 queue[0] (가장 먼저 제출된 것) 부터 처리
data modify storage mq:tmp new_entry set value {text:"", seq:0}
execute store result storage mq:tmp new_entry.seq int 1 run scoreboard players get @s submit_seq
$data modify storage mq:tmp new_entry.text set value "$(text)"
data modify storage mq:input queue append from storage mq:tmp new_entry

View File

@@ -1,5 +1,36 @@
execute if score init main matches 10 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":""}
# ---- 외부 모드 설치 검증 ----
# 두 모드는 성격이 달라서 검증 방식이 다름:
#
# * mq_chat_mod : mc_chat_answer_mod = 서버 전용 모드 (채팅 가로채기는
# 서버에서 일어남, 클라 설치 불필요). 따라서 fake player `#server`
# 점수를 모드가 매 server tick 마다 1 로 set. 서버에 모드가 없으면
# 이 점수가 갱신되지 않음.
#
# * mq_video_mod : mc_video_player_mod = 클라이언트 측 렌더링 + 서버 측
# 컴포넌트. 같은 objective 안에 holder 두 종류 사용:
# - `#server mq_video_mod` : 서버 컴포넌트가 매 tick 1 로 갱신 (server
# presence). 없으면 0 → 서버에 모드 미설치.
# - `<player> mq_video_mod` : 클라 join 시 payload 가 서버로 오면 서버
# 컴포넌트가 해당 플레이어 점수를 1 로 set (client presence). 클라
# 미설치면 0 유지.
# 이렇게 분리해야 "서버 미설치"와 "특정 플레이어 클라 미설치"가 안내에서
# 구분된다.
#
# 1) 서버 측 모드 부재 — 전원 차단, 단일 안내. 서버 부재는 클라 검사보다
# 우선해야 — 클라가 다 설치되어 있어도 서버가 없으면 동작 안 한다.
execute unless score #server mq_chat_mod matches 1 run return run function mq:tellraw {"text":"채팅정답 모드가 서버에 미설치 — 서버 관리자에게 문의해주세요.","color":"red","msg":""}
execute unless score #server mq_video_mod matches 1 run return run function mq:tellraw {"text":"영상재생 모드가 서버에 미설치 — 서버 관리자에게 문의해주세요.","color":"red","msg":""}
# 2) 클라이언트 측 모드 (mc_video_player_mod) 부재 — 본인 누락 안내 + 차단.
# selector `scores={X=..0}` 는 점수 미존재를 매치하지 않으므로 직전에
# `add @a ... 0` 으로 materialize. 개인 안내는 tellraw @s 직접 (mq:tellraw
# 는 내부 @a broadcast 라 부적합).
scoreboard players add @a mq_video_mod 0
execute as @a[scores={mq_video_mod=..0}] run tellraw @s ["",{"text":"영상재생 모드 미설치 — 모드 적용 후 다시 입장해주세요.","color":"red"}]
execute if entity @a[scores={mq_video_mod=..0}] run return run function mq:tellraw {"text":"필수 모드 미설치 플레이어가 있어 시작할 수 없습니다.","color":"red","msg":""}
setblock ~ ~ ~ minecraft:air setblock ~ ~ ~ minecraft:air
function mq:quiz/stop_sound function mq:quiz/stop_sound

View File

@@ -13,6 +13,11 @@ scoreboard players set test buttons -1
scoreboard players reset @a answer scoreboard players reset @a answer
# 정답 입력 큐 / 제출 순번 초기화
data remove storage mq:input queue
scoreboard players reset @a submit_seq
scoreboard players set seq func.temp 0
# 트리거 시작 # 트리거 시작
scoreboard objectives remove ready scoreboard objectives remove ready
scoreboard objectives add ready trigger scoreboard objectives add ready trigger
@@ -31,8 +36,15 @@ scoreboard objectives add hint trigger
scoreboard objectives remove replay scoreboard objectives remove replay
scoreboard objectives add replay trigger scoreboard objectives add replay trigger
scoreboard objectives remove input
scoreboard objectives add input trigger
# 트리거 끝 # 트리거 끝
# 제출자 식별용 순번 점수 (queue entry 의 seq 와 매칭)
scoreboard objectives remove submit_seq
scoreboard objectives add submit_seq dummy
scoreboard objectives setdisplay sidebar scoreboard objectives setdisplay sidebar
scoreboard objectives remove score scoreboard objectives remove score
scoreboard objectives add score dummy {"text":"점수","bold":true} scoreboard objectives add score dummy {"text":"점수","bold":true}

View File

@@ -1,7 +1,25 @@
# 버튼 정의. 각 항목 의미:
# n : 이름 (= 트리거/태그) x,y,z : 버튼 블록 좌표 f : facing
# c : 클릭 시 실행 명령 (init=0 직접, 그 외 trigger $(n) 투표)
# ox,oy,oz : interaction entity 소환 위치 오프셋 (블록 좌표 기준)
# w,h : interaction width / height (float)
#
# stone_button[face=wall] hitbox: 가로 6/16 (0.375), 세로 4/16 (0.25),
# 두께 2/16 (0.125), 벽 반대편으로만 튀어나옴. interaction 의 horizontal
# hitbox 는 width × width 정사각형 강제 → width 를 가로(0.375) 에 맞추고
# 위치 보정으로 "튀어나온 쪽 면 = visible face" 가 되게 함. 반대편은
# 벽 블록 속으로 들어가 invisible.
#
# facing 별 오프셋:
# south : ox=0.5 oy=0.375 oz=-0.0625
# north : ox=0.5 oy=0.375 oz=1.0625
# east : ox=-0.0625 oy=0.375 oz=0.5
# west : ox=1.0625 oy=0.375 oz=0.5
data modify storage mq:main button_defs set value [] 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:"start", x:140, y:62, z:-225, f:"south", c:"function mq:commands/start with storage mq:main", ox:"0.5", oy:"0.375", oz:"-0.0625", w:"0.375", h:"0.25"}
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:"stop", x:142, y:62, z:-225, f:"south", c:"function mq:commands/stop with storage mq:main", ox:"0.5", oy:"0.375", oz:"-0.0625", w:"0.375", h:"0.25"}
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:"skip", x:144, y:62, z:-225, f:"south", c:"function mq:commands/skip", ox:"0.5", oy:"0.375", oz:"-0.0625", w:"0.375", h:"0.25"}
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:"hint", x:146, y:62, z:-225, f:"south", c:"function mq:commands/hint", ox:"0.5", oy:"0.375", oz:"-0.0625", w:"0.375", h:"0.25"}
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:"replay", x:148, y:62, z:-225, f:"south", c:"function mq:commands/replay", ox:"0.5", oy:"0.375", oz:"-0.0625", w:"0.375", h:"0.25"}
data modify storage mq:main button_defs append value {n:"test", x:144, y:62, z:-213, f:"north", c:"function mq:commands/test"} data modify storage mq:main button_defs append value {n:"test", x:144, y:62, z:-213, f:"north", c:"function mq:commands/test", ox:"0.5", oy:"0.375", oz:"1.0625", w:"0.375", h:"0.25"}

View File

@@ -11,11 +11,11 @@ data modify storage mq:main spawn set value {x: 144, y: 61, z: -219, r: 180, f:
# pitch — 1.0 = 원본 속도 # pitch — 1.0 = 원본 속도
data modify storage mq:main audio set value {namespace: "musicquiz", source: "weather", volume: 1.0, 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 # 정답 페인팅 — 데이터팩의 mq:cover_NN painting_variant (텍스처는 리소스팩 musicquiz:cover_NN)
# namespace — painting_variant 네임스페이스 (기본 "musicquiz") # namespace — painting_variant 네임스페이스 (기본 "mq")
# x,y,z — 페인팅 entity 좌표 (벽면 앞쪽 블록 위치) # x,y,z — 페인팅 entity 좌표 (벽면 앞쪽 블록 위치)
# facing — 페인팅이 바라보는 방향: south=0 / west=1 / north=2 / east=3 # 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} data modify storage mq:main image set value {namespace: "mq", x: 144, y: 84, z: -261, facing: 0b}
# 정답 입력용 marker entity 소환 좌표 # 정답 입력용 marker entity 소환 좌표
data modify storage mq:main marker set value {x: 144, y: 59, z: -219} data modify storage mq:main marker set value {x: 144, y: 59, z: -219}

View File

@@ -1,60 +1,59 @@
# 곡 한 개 = 한 줄. # 곡 한 개 = 한 줄.
# 필수 — title, author, alias # 필수 — title, author, alias, volume
# 선택 — volume (이 곡만의 /playsound 음량. 미지정시 init/config.mcfunction # volume: /playsound 음량. 1.0 = 기본. 곡마다 음량 조절 가능.
# 의 audio.volume 사용)
# 곡 순서가 리소스팩의 track_NN / cover_NN 인덱스와 1:1 매칭된다. # 곡 순서가 리소스팩의 track_NN / cover_NN 인덱스와 1:1 매칭된다.
# 예) {title:"Quiet Song", author:"...", alias:[...], volume:2.0} # 예) {title:"Quiet Song", author:"...", alias:[...], volume:2.0}
data modify storage mq:main songs set value [] data modify storage mq:main songs set value []
data modify storage mq:main songs append value {title:"Lose My Mind", author:"Don Toliver", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"Lose My Mind", author:"Don Toliver", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"The Chase", author:"Hearts2Hearts", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"The Chase", author:"Hearts2Hearts", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"HOT SAUCE", author:"BABYMONSTER", alias:[" "," "]} data modify storage mq:main songs append value {title:"HOT SAUCE", author:"BABYMONSTER", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Golden", author:"HUNTR/X", alias:[""," "]} data modify storage mq:main songs append value {title:"Golden", author:"HUNTR/X", alias:[""," "], volume:1.0}
data modify storage mq:main songs append value {title:"돌림판", author:"머쉬베놈", alias:["Spin the wheel"]} data modify storage mq:main songs append value {title:"돌림판", author:"머쉬베놈", alias:["Spin the wheel"], volume:1.0}
data modify storage mq:main songs append value {title:"OVERDRIVE", author:"TWS", alias:["",""]} data modify storage mq:main songs append value {title:"OVERDRIVE", author:"TWS", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"눈물참기", author:"QWER", alias:[]} data modify storage mq:main songs append value {title:"눈물참기", author:"QWER", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"깨어", author:"tripleS", alias:[]} data modify storage mq:main songs append value {title:"깨어", author:"tripleS", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"like JENNIE", author:"제니", alias:[" "," "," "," "]} data modify storage mq:main songs append value {title:"like JENNIE", author:"제니", alias:[" "," "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Rich Man", author:"aespa", alias:[" "," "]} data modify storage mq:main songs append value {title:"Rich Man", author:"aespa", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"I DO ME", author:"KiiiKiii", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"I DO ME", author:"KiiiKiii", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"SIGN", author:"izna", alias:["",""]} data modify storage mq:main songs append value {title:"SIGN", author:"izna", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"WICKED", author:"ALLDAY PROJECT", alias:["",""]} data modify storage mq:main songs append value {title:"WICKED", author:"ALLDAY PROJECT", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"Good Thing", author:"i-dle", alias:["굿 "," "," "]} data modify storage mq:main songs append value {title:"Good Thing", author:"i-dle", alias:["굿 "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"0+0", author:"한로로", alias:[]} data modify storage mq:main songs append value {title:"0+0", author:"한로로", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"HANDS UP", author:"MEOVV", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"HANDS UP", author:"MEOVV", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Blue Valentine", author:"NMIXX", alias:[" "," "]} data modify storage mq:main songs append value {title:"Blue Valentine", author:"NMIXX", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Flower", author:"오반", alias:["",""]} data modify storage mq:main songs append value {title:"Flower", author:"오반", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"Soda Pop", author:"Saja Boys", alias:[" "," "]} data modify storage mq:main songs append value {title:"Soda Pop", author:"Saja Boys", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"REBEL HEART", author:"IVE", alias:[" "," "]} data modify storage mq:main songs append value {title:"REBEL HEART", author:"IVE", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"GO!", author:"CORTIS", alias:["","!","GO","","!"]} data modify storage mq:main songs append value {title:"GO!", author:"CORTIS", alias:["","!","GO","","!"], volume:1.0}
data modify storage mq:main songs append value {title:"BEEP", author:"izna", alias:["","",""]} data modify storage mq:main songs append value {title:"BEEP", author:"izna", alias:["","",""], volume:1.0}
data modify storage mq:main songs append value {title:"Pookie", author:"FIFTY FIFTY", alias:["",""]} data modify storage mq:main songs append value {title:"Pookie", author:"FIFTY FIFTY", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"DAISIES", author:"Justin Bieber", alias:["","",""]} data modify storage mq:main songs append value {title:"DAISIES", author:"Justin Bieber", alias:["","",""], volume:1.0}
data modify storage mq:main songs append value {title:"빌려온 고양이", author:"ILLIT", alias:[]} data modify storage mq:main songs append value {title:"빌려온 고양이", author:"ILLIT", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"TOO BAD", author:"OfficialGDRAGON", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"TOO BAD", author:"OfficialGDRAGON", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"시작의 아이", author:"마크툽", alias:[]} data modify storage mq:main songs append value {title:"시작의 아이", author:"마크툽", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"STYLE", author:"Hearts2Hearts", alias:["",""]} data modify storage mq:main songs append value {title:"STYLE", author:"Hearts2Hearts", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"Good Goodbye", author:"화사", alias:["굿 굿"," "," "]} data modify storage mq:main songs append value {title:"Good Goodbye", author:"화사", alias:["굿 굿"," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"너에게 닿기를", author:"10CM", alias:[]} data modify storage mq:main songs append value {title:"너에게 닿기를", author:"10CM", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"IRIS OUT", author:"Kenshi Yonezu", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"IRIS OUT", author:"Kenshi Yonezu", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Sugar On My Tongue", author:"Tyler, The Creator", alias:[" "," "," "]} data modify storage mq:main songs append value {title:"Sugar On My Tongue", author:"Tyler, The Creator", alias:[" "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"Hollywood Action", author:"BOYNEXTDOOR", alias:[" "," "]} data modify storage mq:main songs append value {title:"Hollywood Action", author:"BOYNEXTDOOR", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"SPAGHETTI", author:"LE SSERAFIM", alias:["",""]} data modify storage mq:main songs append value {title:"SPAGHETTI", author:"LE SSERAFIM", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"Gabriela", author:"KATSEYE", alias:["",""]} data modify storage mq:main songs append value {title:"Gabriela", author:"KATSEYE", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"LIKE YOU BETTER", author:"프로미스나인", alias:[" "," "," "," "," "]} data modify storage mq:main songs append value {title:"LIKE YOU BETTER", author:"프로미스나인", alias:[" "," "," "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"뛰어", author:"BLACKPINK", alias:["jump"]} data modify storage mq:main songs append value {title:"뛰어", author:"BLACKPINK", alias:["jump"], volume:1.0}
data modify storage mq:main songs append value {title:"CHANEL", author:"Tyla", alias:["",""]} data modify storage mq:main songs append value {title:"CHANEL", author:"Tyla", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"오늘만 I LOVE YOU", author:"BOYNEXTDOOR", alias:[" "," "]} data modify storage mq:main songs append value {title:"오늘만 I LOVE YOU", author:"BOYNEXTDOOR", alias:[" "," "], volume:1.0}
data modify storage mq:main songs append value {title:"earthquake", author:"지수", alias:["","",""," "]} data modify storage mq:main songs append value {title:"earthquake", author:"지수", alias:["","",""," "], volume:1.0}
data modify storage mq:main songs append value {title:"윽!", author:"염따", alias:[""]} data modify storage mq:main songs append value {title:"윽!", author:"염따", alias:[""], volume:1.0}
data modify storage mq:main songs append value {title:"Abracadabra", author:"Lady Gaga", alias:["",""]} data modify storage mq:main songs append value {title:"Abracadabra", author:"Lady Gaga", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"멸종위기사랑", author:"이찬혁", alias:[]} data modify storage mq:main songs append value {title:"멸종위기사랑", author:"이찬혁", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"Dirty Work", author:"aespa", alias:[" "," "," "," "," "]} data modify storage mq:main songs append value {title:"Dirty Work", author:"aespa", alias:[" "," "," "," "," "], volume:1.0}
data modify storage mq:main songs append value {title:"HOT", author:"LE SSERAFIM", alias:["",""]} data modify storage mq:main songs append value {title:"HOT", author:"LE SSERAFIM", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"FAMOUS", author:"ALLDAY PROJECT", alias:["",""]} data modify storage mq:main songs append value {title:"FAMOUS", author:"ALLDAY PROJECT", alias:["",""], volume:1.0}
data modify storage mq:main songs append value {title:"XOXZ", author:"IVE", alias:[" "," ",""]} data modify storage mq:main songs append value {title:"XOXZ", author:"IVE", alias:[" "," ",""], volume:1.0}
data modify storage mq:main songs append value {title:"여름이었다", author:"H1-KEY", alias:[]} data modify storage mq:main songs append value {title:"여름이었다", author:"H1-KEY", alias:[], volume:1.0}
data modify storage mq:main songs append value {title:"LOV3", author:"식케이", alias:["3","3"]} data modify storage mq:main songs append value {title:"LOV3", author:"식케이", alias:["3","3"], volume:1.0}
data modify storage mq:main songs append value {title:"Drive", author:"Ed Sheeran", alias:["",""]} data modify storage mq:main songs append value {title:"Drive", author:"Ed Sheeran", alias:["",""], volume:1.0}
# 곡 개수는 songs 배열 길이에서 자동 계산됨 # 곡 개수는 songs 배열 길이에서 자동 계산됨
execute store result storage mq:main max_index int 1 run data get storage mq:main songs execute store result storage mq:main max_index int 1 run data get storage mq:main songs

View File

@@ -21,6 +21,24 @@ scoreboard objectives add buttons dummy
scoreboard objectives add answer dummy scoreboard objectives add answer dummy
scoreboard objectives add leave_game custom:leave_game scoreboard objectives add leave_game custom:leave_game
# 외부 모드 존재 확인용 점수.
# mq_chat_mod : 서버 전용 모드(mc_chat_answer_mod). 모드가 매 server tick
# 마다 fake player `#server` 점수를 1 로 set. 모드가 서버에 없으면 0 유지.
# mq_video_mod : 클라이언트 모드(mc_video_player_mod). 클라 join 시 서버로
# handshake payload 전송 → 서버 측 모드가 해당 플레이어 점수를 1 로 set.
# 클라에 모드가 없으면 0 유지. (login.mcfunction 에서 플레이어별 0 초기화.)
scoreboard objectives remove mq_chat_mod
scoreboard objectives remove mq_video_mod
scoreboard objectives add mq_chat_mod dummy
scoreboard objectives add mq_video_mod dummy
# /reload 후 모드가 한 tick 도 돌기 전에 start 가 호출될 수 있으니
# #server 점수도 0 으로 materialize. 모드가 살아 있으면 다음 tick 에 1 로 갱신.
# mq_video_mod 도 같은 objective 안에서 holder 만 다르게 — `#server` 는 서버
# 컴포넌트 존재 (서버 측 모드가 매 tick 1 로 갱신), `<player>` 는 클라 측
# 존재 (payload 수신 시 1 로 갱신).
scoreboard players set #server mq_chat_mod 0
scoreboard players set #server mq_video_mod 0
scoreboard players set two func.temp 2 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}] 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}]

View File

@@ -1,6 +1,15 @@
tag @s add player tag @s add player
scoreboard players reset @s leave_game scoreboard players reset @s leave_game
# 외부 모드 검증 점수 초기화 (per-player 검증 대상만).
# mq_video_mod : 클라이언트 모드(mc_video_player_mod) 가 join 시 handshake
# payload 를 서버로 보내면 서버 모드가 해당 플레이어 점수를 1 로 set 한다.
# 여기서 0 으로 미리 깔아 두면 handshake 가 없는 플레이어는 0 유지 →
# start 가드 차단. handshake 가 오면 곧바로 1 로 갱신됨.
# mq_chat_mod 는 서버 전용 모드라 fake player(#server) 로 검증 — per-player
# 초기화 불필요.
scoreboard players set @s mq_video_mod 0
title @s times 10t 80t 10t title @s times 10t 80t 10t
title @s subtitle "" title @s subtitle ""
title @s title "" title @s title ""
@@ -8,3 +17,6 @@ title @s title ""
$setworldspawn $(x) $(y) $(z) $(r) $(f) $setworldspawn $(x) $(y) $(z) $(r) $(f)
$tp @s $(x) $(y) $(z) $(r) $(f) $tp @s $(x) $(y) $(z) $(r) $(f)
gamemode adventure @s gamemode adventure @s
# 채팅정답 모드 활성 알림은 모드가 직접 PlayerLoggedInEvent 핸들러에서
# mq:players/mod_active_notice 를 호출해서 표시한다 (race-free).

View File

@@ -0,0 +1,3 @@
# chat_answer 모드의 PlayerLoggedInEvent 핸들러가 직접 호출.
# 모드가 없으면 이 함수가 호출될 일이 없으므로 메세지가 안 뜬다.
tellraw @s ["",{"text":"[채팅정답] ","color":"green","bold":true},{"text":"모드가 활성화되어 있습니다.","color":"gray"},{"text":" 정답 입력 시 ","color":"gray"},{"text":"채팅","color":"yellow","bold":true},{"text":"으로 바로 제출할 수 있습니다.","color":"gray"}]

View File

@@ -12,6 +12,14 @@ scoreboard players set skip buttons -1
scoreboard players set hint buttons -1 scoreboard players set hint buttons -1
scoreboard players set replay buttons -1 scoreboard players set replay buttons -1
# 이전 문제의 미처리 정답 입력 정리 + 새 문제의 input trigger 활성화
# input trigger 는 유지 (mod 없는 환경에서 /trigger input 으로 dialog 열기 가능).
# 채팅 안내 메시지는 제거 — mc_chat_answer_mod 가 채팅 직접 입력을 처리함.
data remove storage mq:input queue
scoreboard players reset @a submit_seq
scoreboard players set seq func.temp 0
scoreboard players enable @a input
scoreboard players set init main 5 scoreboard players set init main 5
function mq:quiz/play_sound function mq:quiz/play_sound

View File

@@ -1,28 +1,39 @@
# 버튼 1개에 대한 매 tick 처리.
# 매크로 인자: n, x, y, z, f, c, ox, oy, oz, w, h
# ox/oy/oz : interaction 소환 위치 오프셋 (블록 좌표 기준, facing 별)
# w / h : interaction width / height (float, 버튼 hitbox 정합용)
# buttons 점수 상태:
# ..-2 : 비활성 (버튼 블록 제거, interaction 응답 차단)
# -1 : 초기화 단계 (버튼 블록 배치 + interaction entity 보장 후 0 으로)
# 0 : 정상 (interaction 클릭 대기)
#
# interaction entity 는 데이터팩이 직접 summon — /reload 시 commands/stop
# 에서 buttons 가 -1 로 재설정되어 다음 tick 에 ensure 로직이 실행됨.
# -1 단계에서 같은 태그 entity 를 모두 kill 후 정확히 1개 summon → dup
# 누적 없이 항상 "버튼당 1개, 올바른 좌표" 상태로 수렴 (idempotent).
# ---- 비활성: 버튼 제거 + interaction 응답 차단 후 종료 ----
$execute if score $(n) buttons matches ..-2 run setblock $(x) $(y) $(z) minecraft:air $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 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 if score $(n) buttons matches ..-2 run return 0
# ---- 초기화: 버튼 블록 배치 + interaction entity 보장 ----
# 기존 mq/$(n) interaction 을 전부 제거 후 정확히 1개 소환.
# 옛 월드 cmd block 으로 누적 소환된 dup 이나 엉뚱한 좌표에 남은 잔존
# entity 까지 정리 → "정상 상태(버튼당 정확히 1개, 올바른 좌표)" 가 보장됨.
$execute unless score $(n) buttons matches -1.. run scoreboard players set $(n) buttons -1 $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 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 run kill @e[type=minecraft:interaction,tag=mq,tag=$(n)]
$execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) run setblock ~ ~-3 ~ minecraft:red_wool $execute if score $(n) buttons matches -1 positioned $(x) $(y) $(z) positioned ~$(ox) ~$(oy) ~$(oz) run summon minecraft:interaction ~ ~ ~ {Tags:["mq","$(n)"],width:$(w)f,height:$(h)f,response:0b}
$execute if score $(n) buttons matches -1 run scoreboard players set $(n) buttons 0 $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] \ # ---- 상시: interaction 클릭/타격 → playsound + 명령/투표 실행 ----
if score $(n) buttons matches 0 \ # init main = 0 (퀴즈 시작 전 설정 단계) : 명령 직접 실행
run scoreboard players set $(n) buttons 1 # 그 외 : trigger 투표 경로
$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) 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) 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),limit=1] on target as @s positioned $(x) $(y) $(z) unless score init main matches 0 run trigger $(n)
# ---- 처리 후 attack/interaction NBT 클리어 (다음 tick 중복 발화 방지) ----
$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 attack
$execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s interaction $execute as @e[type=minecraft:interaction,tag=mq,tag=$(n)] at @s run data remove entity @s interaction

View File

@@ -4,3 +4,7 @@ function mq:repeat/triggers/handler
execute if score init main matches 2.. run function mq:repeat/timer 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 execute if score init main matches 5..6 run function mq:repeat/check_answer
# 정답 입력 다이얼로그: init=5 (곡 재생 중) 일 때만 열림 / 제출 처리
execute if score init main matches 5 as @a[scores={input=1..}] run function mq:answer/open
execute if score init main matches 5 run function mq:answer/process

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_01",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_02",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_03",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_04",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_05",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_06",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_07",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_08",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_09",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_10",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_11",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_12",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_13",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_14",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_15",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_16",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_17",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_18",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_19",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_20",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_21",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_22",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_23",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_24",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_25",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_26",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_27",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_28",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_29",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_30",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_31",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_32",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_33",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_34",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_35",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_36",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_37",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_38",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_39",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_40",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_41",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_42",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_43",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_44",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_45",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_46",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_47",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_48",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_49",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:cover_50",
"width": 1,
"height": 1
}

View File

@@ -0,0 +1,5 @@
{
"asset_id": "musicquiz:gif",
"width": 1,
"height": 1
}

View File

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

BIN
music_quiz/pack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
temp/gif.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

6
temp/gif.png.mcmeta Normal file
View File

@@ -0,0 +1,6 @@
{
"animation": {
"frametime": 1,
"interpolate": false
}
}