From b79eff26b7a9f42ea9861b2259838010df64163c Mon Sep 17 00:00:00 2001 From: "Claude (owner)" Date: Wed, 20 May 2026 10:45:12 +0900 Subject: [PATCH] =?UTF-8?q?add=20HANDOVER.md=20=E2=80=94=20mod=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=9D=B4=20=EB=B3=84=EB=8F=84=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=EB=90=98=EB=A9=B4?= =?UTF-8?q?=EC=84=9C=20=EC=BD=9C=EB=93=9C=20=EC=8A=A4=ED=83=80=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EC=9D=B8=EA=B3=84=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 내용: - 한 줄 요약, 리포/인증, 최신 버전 - 폴더 구조, 빌드 명령 - 핵심 동작 3 개 (handleChat, mod_active_notice, presence pulse) 동작 + 왜 그렇게 짰는지 - mc_datapack 측 통합 인터페이스 (함수/점수 표) - v1.1.0 ~ v1.3.8 버전 히스토리 요점 - 릴리스 절차 (gradle.properties 버전 → buildAll → tag/push → Gitea API) - 작업 환경 (클론 위치, JDK, git author override 패턴) 이 mod 채팅은 데이터팩 mc_datapack 채팅과 분리되므로, 호환 깰 가능성 있는 변경 시 데이터팩 쪽 채팅과 조율 필요. Co-Authored-By: Claude Opus 4.7 --- HANDOVER.md | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 HANDOVER.md diff --git a/HANDOVER.md b/HANDOVER.md new file mode 100644 index 0000000..f4796e0 --- /dev/null +++ b/HANDOVER.md @@ -0,0 +1,205 @@ +# mc_chat_answer_mod 인수인계 (Handover) + +이 문서는 본 모드 작업이 별도 채팅(Discord 룸)으로 분리되면서 새 채팅이 +콜드 스타트로 인계받을 수 있도록 그동안의 컨텍스트를 정리한 것입니다. +모드를 계속 유지보수할 때 이 파일을 먼저 읽으면 됩니다. + +## 한 줄 요약 + +음악퀴즈 데이터팩(`mc_datapack` / 데이터팩 namespace `mq`) 의 짝이 되는 +멀티로더(Fabric + NeoForge) **서버사이드** Minecraft 모드. 두 가지 일을 +한다: + +1. **정답 채팅 가로채기** — `init main == 5` (정답 입력 단계) 일 때 플레이어 + 채팅을 정답 제출로 OP 권한으로 처리. +2. **데이터팩 presence pulse** — `mq_chat_mod` objective 의 `#server` 점수를 + 1 로 set 해서 데이터팩이 "이 모드 설치돼 있는가" 를 검사할 수 있게 함. +3. (부가) **모드 활성화 안내** — 로그인 1 초 뒤 `mq:players/mod_active_notice` + 호출. + +## 리포 / 접근 + +- URL: https://git.tkrmagid.kr/tkrmagid/mc_chat_answer_mod +- 기본 브랜치: `main` +- 라이센스: MIT +- 인증: Gitea PAT — 글로벌 메모리 `/home/claude/.config/ejclaw/secrets.json` + 의 `credentials["git.tkrmagid.kr"].token`. HTTPS URL 에 임베드하거나 + `Authorization: token ` 헤더로 사용. +- git author (커밋 시): `-c user.name="Claude (owner)" -c user.email="claude@tkrmagid.kr"`. + 글로벌 git config 안 건드림. + +## 최신 버전 + +**v1.3.8** (2026-05-20). 릴리스: https://git.tkrmagid.kr/tkrmagid/mc_chat_answer_mod/releases/tag/v1.3.8 + +## 폴더 구조 + +- `common/` — 로더 비종속 핵심 로직 (Mojang 매핑 기준). + - `kr/tkrmagid/chatanswer/core/ChatAnswerCore.java` — 거의 모든 로직이 여기. +- `fabric-1216/` — Fabric Loader MC 1.21.6 진입점. +- `fabric-2612/` — Fabric Loader MC 26.1.2 진입점. +- `neoforge-1216/` — NeoForge MC 1.21.6 진입점. (26.x 는 NeoForge moddev + plugin 이 아직 인식 못 함.) +- `containerJar` 태스크가 위 세 로더의 결과물을 **단일 jar** 로 묶음 + (Fabric 은 META-INF/jars/ JiJ, NeoForge 는 outer 본체). 최종 산출: + `build/libs/chat_answer-.jar`. + +## 빌드 + +JDK 21 필요. + +``` +./gradlew buildAll +``` + +산출물: `build/libs/chat_answer-.jar` (서버 mods/ 에 이거 하나만 +넣으면 됨 — Fabric 이든 NeoForge 든 자기 진입점만 인식). + +## 핵심 동작 상세 + +### A. handleChat (정답 채팅 처리) + +`ChatAnswerCore.handleChat(ServerPlayer, String)`: + +- 정답 단계(`init main == 5`) 가 아니면 그대로 통과 (true 반환). +- 정답 단계면 `sanitize` 후 OP 권한으로 다음 실행: + ``` + execute as run function mq:answer/submit {text:'<채팅>'} + ``` +- **v1.3.7 까지**: 정답 단계에서 false 반환 → Fabric `ALLOW_CHAT_MESSAGE` + / NeoForge `ServerChatEvent.setCanceled(true)` 가 broadcast 차단. +- **v1.3.8 부터**: 항상 true 반환 → broadcast 차단 안 함. 정답 채팅도 + 다른 플레이어에게 평소대로 보임. 정답 보호는 룸 운영자 신뢰 기반. + +`sanitize` 는 큰따옴표/백슬래시/제어문자 제거 (매크로 NBT 호환). + +### B. mod_active_notice (입장 안내) + +`onPlayerJoin(ServerPlayer)` 가 UUID → 20 ticks 맵에 적재. 매 server tick +마다 카운트다운, 0 되면 player 본인을 source 로 한 CommandSourceStack 으로 +`function mq:players/mod_active_notice` 호출. + +**왜 지연하는가**: JOIN 이벤트는 플레이어가 PlayerList 에 막 들어간 직후라 +클라이언트가 system chat 패킷 받을 준비가 안 됐을 수 있음. 즉시 tellraw 를 +보내면 사라지는 race 가 v1.3.3 이하에서 재현됐음. 20 ticks (1 초) 지연으로 +해결. + +### C. presence pulse + +`markModPresence(MinecraftServer)`: +```java +scoreboard.getOrCreatePlayerScore( + ScoreHolder.forNameOnly("#server"), + scoreboard.getObjective("mq_chat_mod") +).set(1); +``` + +- objective 가 없으면 (= 데이터팩 미설치) 조용히 skip. +- 점수가 이미 1 이면 MC 가 packet 전송 생략 → 매 tick 호출해도 트래픽 + 안 늘어남. +- **호출 지점 4 개** (어느 하나만 firing 돼도 가드 통과): + - `ServerLifecycleEvents.SERVER_STARTED` / `ServerStartedEvent` — 부팅 직후 + - `onPlayerJoin` — 로그인 시 + - `ServerTickEvents.END_SERVER_TICK` / `ServerTickEvent.Post` — steady-state + - `ServerLifecycleEvents.END_DATA_PACK_RELOAD` (success=true 시) / + `OnDatapackSyncEvent` — /reload 직후 + +**왜 중복**: banner/mohist 같은 fabric-bukkit 하이브리드 호스트에서 +`END_SERVER_TICK` 이 안 들어오는 케이스, `/reload` 가 objective 를 +remove/add 해서 `#server` 점수를 0 으로 reset 하는 케이스 등이 보고됨. +하나만 의존하면 false negative 가 남음. + +## 데이터팩 (mc_datapack / mq) 측 통합 + +| 모드 → 데이터팩 | 모드가 호출하는 함수 / 쓰는 점수 | +|---|---| +| `mq:players/mod_active_notice` | 로그인 1 초 뒤 호출 | +| `mq:answer/submit {text:'...'}` | 정답 단계 채팅 시 호출 | +| `scoreboard mq_chat_mod` `#server` ← 1 | presence pulse | + +| 데이터팩 → 모드 | 데이터팩이 읽는 점수 | +|---|---| +| `scoreboard main` `init` | 5 면 정답 단계 | +| `scoreboard mq_chat_mod` `#server` | 1 이면 모드 설치됨 (가드 통과) | + +데이터팩의 `load.mcfunction` 이 `scoreboard objectives add mq_chat_mod dummy` +로 objective 를 생성하고 `#server` 를 0 으로 materialize. 모드는 그 objective +가 있어야 pulse 가 동작. 데이터팩의 `commands/start.mcfunction` 에서 +`execute if score #server mq_chat_mod matches 1` 로 검증, 미설치 시 +사유와 함께 차단. + +데이터팩 최신: v1.0.26. README 에 `mc_chat_answer_mod` 최소 권장 버전 +**v1.3.7+** 명시 (v1.3.8 은 상위 호환이라 README 갱신 불필요). + +## 버전 히스토리 (요점만) + +- **v1.1.0** — PlayerJoin 훅 추가, 데이터팩에 모드 설치 신호. +- **v1.1.1** — 싱글플레이어 (client side) 에서도 로드. +- **v1.2.0** — MC 26.1.2 타깃. +- **v1.2.1** — icon 추가. +- **v1.3.0** — 단일 jar 가 Fabric 1.21.6 + Fabric 26.1.2 + NeoForge 1.21.6 커버. +- **v1.3.1** — 중첩 fabric jar 를 outer fabric.mod.json 에 선언. +- **v1.3.2** — 진단 로깅. +- **v1.3.3** — storage flag 대신 직접 function 호출 (race 해소). +- **v1.3.4** — `mod_active_notice` 20 tick 지연 (chat-not-delivered race 해소). + **이 버전 미만이면 입장 안내 채팅이 가끔 사라짐.** +- **v1.3.5** — `#server mq_chat_mod` tick pulse 추가 (가드 도입). +- **v1.3.6** — fabric-bukkit 하이브리드에서 END_SERVER_TICK 안 들어오는 + false negative → SERVER_STARTED + JOIN 에도 pulse. +- **v1.3.7** — /reload 가 objective remove/add 로 점수 reset 하는데 + 죽은 호스트 + 이미 접속 중 조합에서 SERVER_STARTED/JOIN/Tick 모두 + 안 발화 → END_DATA_PACK_RELOAD / OnDatapackSyncEvent 추가. +- **v1.3.8** — 정답 단계 broadcast 차단 해제 (사용자 요청). + +## 알려진 이슈 + +v1.3.8 시점 기준 없음. 리뷰어 검증 통과 항목: + +- `handleChat` 가 항상 true 반환 (jar 바이트코드 기준). +- 정답 단계에서는 `mq:answer/submit` 호출 후 broadcast 유지. +- 다른 채팅 cancel 경로 repo 전체에 없음. +- `./gradlew buildAll` 성공. + +## 릴리스 절차 + +1. 코드 수정 + 테스트. +2. `gradle.properties` 의 `mod_version` 올림. +3. `./gradlew buildAll` 으로 jar 빌드 → `build/libs/chat_answer-.jar`. +4. 커밋 (위의 git author override 사용) + annotated tag `v` + push. +5. Gitea 릴리스 생성: + ``` + POST https://git.tkrmagid.kr/api/v1/repos/tkrmagid/mc_chat_answer_mod/releases + Authorization: token + Content-Type: application/json + {"tag_name":"v","name":"...","body":"..."} + ``` + 응답의 `id` 받아서 자산 업로드: + ``` + POST .../releases//assets?name=chat_answer-.jar + -F "attachment=@build/libs/chat_answer-.jar" + ``` +6. 사용자에게 mods 폴더 jar 교체 안내. + +호환성 깰 가능성이 있는 변경 (예: `mq:answer/submit` 호출 인자 변경, +`mq_chat_mod` objective 이름 변경 등) 은 반드시 mc_datapack 쪽 채팅과 +조율해야 함. + +## 작업 환경 메모 + +- 로컬 클론 경로 (참고): `/tmp/mc_chat_answer_mod`. owner Claude workspace + (`/home/claude/EJClaw/data/workspaces/mc_datapack/owner/`) 밖이므로 + owner 브랜치 protocol 적용 대상 아님. mod repo 는 `main` 직접 push. +- 빌드 도구: Gradle wrapper (`./gradlew`), JDK 21. +- Loom 1.16.2, NeoForge moddev plugin 사용. + +## 짝 데이터팩 (mc_datapack) + +- URL: https://git.tkrmagid.kr/tkrmagid/mc_datapack +- 별도 채팅(현 mc_datapack 채팅) 에서 유지보수. +- 본 모드 작업이 데이터팩 호환을 깨면 그쪽 채팅에 알려야 함. +- 데이터팩 namespace `mq`, 메인 디렉토리 `music_quiz/data/mq/function/`. + +--- + +이 문서는 mod repo 의 `HANDOVER.md` 로 커밋돼 있습니다. 새 채팅에서 +`git clone` 후 이 파일부터 읽으면 됩니다.