# mc_video_player_mod — 음악퀴즈 데이터팩 연동 사양 음악퀴즈 데이터팩(`music_quiz`)이 영상재생 모드의 **서버 측 + 클라이언트 측** 설치 여부를 둘 다 검증할 수 있도록, 모드의 서버 컴포넌트가 두 가지 점수를 갱신한다. | holder | 의미 | 갱신 주체 / 시점 | |--------|------|------------------| | `#server mq_video_mod` | 서버에 모드 jar 존재 | 서버 컴포넌트가 매 server tick 마다 1 | | ` mq_video_mod` | 해당 플레이어 클라에 모드 존재 | 서버 컴포넌트가 client→server payload 수신 시 1 | 둘 다 같은 objective `mq_video_mod` (dummy) 를 쓰고 holder 이름으로 의미를 구분한다. **영상재생 모드는 선택 모드라 미설치여도 `/start` 를 차단하지 않는다.** `mq:commands/start` 는 두 점수를 보고 **경고만** 출력한다: 서버 부재 → 전원에게 "영상 없이 진행됩니다" / 일부 플레이어 클라 부재 → 본인에게만 "본인 화면에는 영상이 표시되지 않습니다". 게임은 그대로 진행된다. > 참고: 자매 모드 `mc_chat_answer_mod` 는 채팅 가로채기가 본질적으로 서버 > 측 동작이라 `#server mq_chat_mod` 한 가지만 쓴다 (per-player handshake 무의미). > 그쪽은 정답 판정의 핵심이라 **없으면 `/start` 가 실제로 차단된다.** 본 > 모드는 클라이언트가 직접 영상 렌더링하므로 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 # unset 매치 안 되므로 materialize scoreboard players add @a mq_video_mod 0 # 서버 부재 — 전원에게 경고만, 그대로 진행 execute unless score #server mq_video_mod matches 1 run function mq:tellraw {"text":"영상재생 모드가 서버에 미설치 — 영상 없이 진행됩니다.","color":"yellow","msg":""} # 서버는 있는데 본인 클라만 부재 — 본인에게만 경고 execute if score #server mq_video_mod matches 1 as @a[scores={mq_video_mod=..0}] run tellraw @s ["",{"text":"영상재생 모드 미설치 — 본인 화면에는 영상이 표시되지 않습니다.","color":"yellow"}] ``` `load.mcfunction` 에서 `#server mq_video_mod = 0` 으로 미리 깔아둠 → 서버 컴포넌트가 한 tick 도 안 돌면 0 유지 → 서버 부재 경고 출력. 즉, **서버 컴포넌트가 (a) 매 tick `#server mq_video_mod=1` 갱신, (b) client payload 수신 시 송신 플레이어 점수=1 갱신**, 이 두 가지만 하면 나머지는 데이터팩이 알아서 한다. (점수가 0 이어도 게임은 진행되며, 경고 출력과 영상 표시 여부에만 영향을 준다.) ## 모드가 구현해야 하는 동작 ### 권장: 커스텀 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 TYPE = new CustomPacketPayload.Type<>(ResourceLocation.fromNamespaceAndPath("mq_video_mod", "hello")); public static final StreamCodec 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 단위 → 주기 재전송이 살아 있는 한 경고 오탐 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 ` 로 client presence 점수 확인. ## 참고: 자매 모드 `mc_chat_answer_mod` 의 다른 접근 `mc_chat_answer_mod/common/.../ChatAnswerCore.java::markModPresence` 참고. 서버 전용 모드라 fake player `#server` 한 곳에만 set 한다. 클라이언트 렌더링 모드인 본 모드는 이 패턴이 아닌 per-player handshake 가 정답.