Files
mc_datapack/docs/mc_video_player_mod_integration.md
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

9.1 KiB

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 가드:

# 서버 부재 우선 차단
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 네임스페이스). 페이로드 본문은 비어도 되고, 버전 정수 한 개 정도면 충분.

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 로 복구되어야 정상 동작.
// 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 호출해도 비용 없음.

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() 에 등록한 뒤:

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_modChatAnswerCore::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 가 정답.