21 Commits

Author SHA1 Message Date
claude-owner
d252ce256b v1.4.0 — 정식배포 릴리스 (1.3.x 라인 이어서 버전 라벨 승격)
코드 변경 없음. v1.3.8 안정 상태를 정식 v1.4.0 으로 릴리스.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-18 12:10:48 +09:00
Claude (owner)
966884fd69 remove HANDOVER.md — 더 이상 필요 없음 (사용자 요청) 2026-05-20 10:51:41 +09:00
claude-owner
ffda94de91 merge origin/main into owner branch for initial sync 2026-05-20 10:50:45 +09:00
Claude (owner)
b79eff26b7 add HANDOVER.md — mod 작업이 별도 채팅으로 분리되면서 콜드 스타트용 인계 문서
내용:
- 한 줄 요약, 리포/인증, 최신 버전
- 폴더 구조, 빌드 명령
- 핵심 동작 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 <noreply@anthropic.com>
2026-05-20 10:45:12 +09:00
Claude (owner)
fa5b1148b4 v1.3.8 — 정답 단계 채팅 broadcast 차단 해제
사용자 요청: 정답 입력 단계에서도 친 채팅이 다른 플레이어한테 보이게.
정답 보호는 데이터팩이 아니라 룸 운영자의 신뢰 기반 운영으로 처리한다는
방침. 다른 단계는 원래도 차단하지 않았으니 변경 없음.

- ChatAnswerCore.handleChat 가 항상 true 반환. 정답 단계(state 5) 일
  때만 부가적으로 mq:answer/submit 호출하고 broadcast 는 그대로 허용.
- Fabric ALLOW_CHAT_MESSAGE / NeoForge ServerChatEvent 핸들러는 그대로
  반환값을 전달 — 항상 true 라 cancel 안 됨.
- README.md: 차단 동작 설명을 v1.3.8 부터 broadcast 허용으로 정정.

호환: 데이터팩 mq:answer/submit 함수는 동일하게 호출되므로 음악퀴즈
v1.0.26 + chat_answer v1.3.8 조합으로 그대로 동작. 정답 보이는 게
싫으면 v1.3.7 으로 다운그레이드.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 10:27:48 +09:00
Claude (owner)
8540d693a0 v1.3.7 — END_DATA_PACK_RELOAD 훅 추가로 /reload 직후 false negative 회피
v1.3.6 에서 SERVER_STARTED + JOIN + ServerTick 셋에 presence pulse 를
달았지만 한 케이스가 남아 있었음:

데이터팩의 load.mcfunction 이 /reload 때마다 mq_chat_mod objective 를
remove/add 하고 #server 점수를 0 으로 재설정. 그런데 tick 이벤트가
죽은 호스트 + 이미 접속 중인 플레이어 조합이면 SERVER_STARTED 도
JOIN 도 발화 안 되어 reload 후 영영 다시 1 로 안 올라감 → 같은
false negative 가 reload 단위로 재발.

이번 변경:
- Fabric (1216, 2612): ServerLifecycleEvents.END_DATA_PACK_RELOAD
  등록. success=true 일 때만 markModPresence (실패한 reload 는 가드
  통과시키면 안 됨).
- NeoForge (1216): OnDatapackSyncEvent 등록. /reload 끝나면 player=null
  로 한 번 broadcast 되므로 reload 직후 presence 가 다시 찍힘.
- ChatAnswerCore.onDataPackReload 추가 (markModPresence + 진단 log).

v1.3.6 jar 는 retire — 사용자 환경이 /reload 기반 워크플로라 reload
케이스 fix 가 필수.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:22:06 +09:00
Claude (owner)
48d73daaf7 v1.3.6 — presence pulse 다중 hook 으로 false negative 회피
음악퀴즈 데이터팩의 `#server mq_chat_mod` 가드가 일부 호스트에서
false negative 로 시작 차단되던 문제. 기존엔 매 server tick 한 곳에서만
markModPresence 를 호출했는데, banner/mohist 같은 fabric-bukkit
하이브리드 호스트에서 ServerTickEvents.END_SERVER_TICK 이 안 들어와
점수가 영영 1 로 안 올라갔음.

이번 변경: presence pulse 호출 지점을 셋으로 확장 — 어느 한 이벤트만
firing 돼도 가드가 통과.

- ServerLifecycleEvents.SERVER_STARTED (fabric) / ServerStartedEvent
  (neoforge) — 서버 부팅 완료 직후 한 번
- onPlayerJoin — 플레이어 로그인 시점 (server tick 가 죽어도 발화)
- ServerTickEvents.END_SERVER_TICK — 정상 호스트에서의 steady-state

ChatAnswerCore 에 onServerStarted public method 추가, onPlayerJoin
에서도 player.level().getServer() 로 server 받아 markModPresence 호출.
세 로더 entrypoint (fabric-1216, fabric-2612, neoforge-1216) 모두에서
SERVER_STARTED 등록.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:14:13 +09:00
Claude (owner)
5aaa3c2ace bump version to 1.3.5
v1.3.5 — datapack 측 server-presence 검증 가드를 위한 fake player tick.

전 버전 v1.3.4 이후 변경:
- 8057fa1 add mq_chat_mod presence tick: ChatAnswerCore.markModPresence 가 매
  server tick 마다 점수 객체 mq_chat_mod 에 fake player #server 점수를 1 로 set.
  objective 미존재(=데이터팩이 안 깔린 환경) 시 조용히 skip.
- 41fcc82 fix mq_chat_mod presence: server-only mod 이므로 per-player iterate 를
  제거하고 #server 한 곳만 갱신. mc_datapack v1.0.13+ 의 start.mcfunction
  presence 가드와 정확히 매칭.
2026-05-17 00:15:19 +09:00
claude
41fcc82953 fix mq_chat_mod presence: server-only mod 이므로 fake player #server 점수만 set — per-player iterate 제거. 클라 미설치 검증은 의미 없으니(채팅 가로채기는 서버 측 동작) #server 한 곳만 갱신해 데이터팩의 server presence 가드에 정확히 매칭.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:22:43 +09:00
claude
8057fa1112 add mq_chat_mod presence tick — datapack가 모드 설치 여부 검증할 수 있도록 매 server tick 마다 온라인 플레이어의 mq_chat_mod 점수를 1 로 set. objective 미존재 시 조용히 skip.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 22:12:26 +09:00
Claude
8f989ee135 v1.3.4 — defer mod_active_notice by 20 ticks to fix chat-not-delivered race
v1.3.3 에서 PlayerJoinEvent 시점에 즉시 `execute as <uuid> ... run function`
으로 데이터팩 함수를 호출했는데, JOIN 이벤트 시점은 플레이어가 PlayerList 에
막 들어간 직후라 클라이언트가 시스템 chat 패킷을 받을 준비가 안 됐고
tellraw 가 사라지는 race 가 있었음.

사용자 로그에서 확인: 모드의 `mod_active_notice invoked` 가 03:22:42 에
찍혔으나 클라이언트엔 메세지 안 도착, 1초 뒤 (03:22:43) mq:load 가 보낸
같은 시스템의 tellraw 는 정상 도착, 9초 뒤 수동 /function 호출도 정상.

수정: JOIN 시 즉시 호출하지 않고 UUID → 남은 틱 수 맵에 적재, server tick
마다 카운트 다운, 20 ticks (1초) 후 player 자체를 source 로 한
CommandSourceStack 으로 `function mq:players/mod_active_notice` 호출.

엔트리포인트 변경:
- fabric-1216/2612: ServerTickEvents.END_SERVER_TICK 추가 등록
- neoforge-1216: ServerTickEvent.Post 리스너 추가
2026-05-14 03:26:27 +09:00
Claude
a67ec47f89 v1.3.3 — replace storage flag with direct function call (race-free)
이전 v1.3.2: onPlayerJoin 에서 storage chat_answer:status active=1b 를 set.
하지만 통합 서버 (integrated singleplayer) 에서 데이터팩의 mq:load 가
player join 이후에 도는 케이스가 있어 모드가 써놓은 1b 를 데이터팩이
0b 로 덮어쓰는 race 가 있었고, repeat/players 의 첫 tick 체크 시점에
이미 0b 라서 알림 메세지가 안 떴음.

v1.3.3: storage flag 자체를 폐기. onPlayerJoin 에서
  execute as <uuid> at @s run function mq:players/mod_active_notice
를 호출. 데이터팩이 메세지 텍스트를 정의하고, 모드는 "지금 들어온 이
플레이어에게 보여라" 만 트리거한다. 데이터팩 자체가 없으면 함수가 없어
suppressed source 의 command 실패로 silent → 안전.

데이터팩(music_quiz) 도 동일 커밋으로 함수 추가 및 flag 제거됨.
2026-05-14 03:15:04 +09:00
Claude
d1c6504973 v1.3.2 — verbose logging to diagnose missing active=1b write
증상: chat_answer:status active 가 0b 그대로 → 데이터팩 login 메시지 안 뜸.
mq:load 는 정상 작동(0b 초기화 됨)인데 모드의 onPlayerJoin 이 1b 로 안 씀.

추가 로그:
  - ChatAnswerFabric.onInitialize: 진입/완료, event register 예외시 ERROR
  - ChatAnswerCore.onPlayerJoin: 진입 (player 이름 포함), 성공 시 INFO,
    실패 catch 를 LOG.debug → LOG.warn 으로 승격해 latest.log 에 보이게.

이 로그를 보고 다음 중 어디서 깨지는지 좁힐 수 있음:
  - "Fabric entrypoint onInitialize starting" 안 보임 → nested jar 미로드
  - "onInitialize starting" 보이고 "registered" 안 보임 → fabric-api ABI mismatch
  - "onPlayerJoin fired" 안 보임 → JOIN 이벤트 미발화
  - "active=1b set" 안 보이고 "failed" 만 보임 → command 실행 실패

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 03:02:00 +09:00
Claude
939505c861 v1.3.1 — fix: declare nested fabric jars in outer fabric.mod.json
v1.3.0 의 nested fabric jar 가 실제로 로드되지 않던 버그 수정.

Fabric Loader 는 META-INF/jars/ 디렉토리를 자동 스캔하지 않고, 부모 jar 의
fabric.mod.json 에 "jars" 배열로 명시된 파일만 처리한다. v1.3.0 에선 jars
배열이 비어 있어서 outer chat_answer 컨테이너만 로드되고 (entrypoint 없으니
no-op), 실제 채팅 hook 을 담은 nested fabric jar 는 그대로 무시됐다.

수정:
  - container-resources/fabric.mod.json: "jars" 배열에 두 nested 경로 명시
  - root build.gradle: containerJar 의 nested jar 파일명을 버전 suffix 없는
    고정 이름 (chat_answer-fabric-1216.jar / -2612.jar) 으로 변경. outer
    fabric.mod.json 의 jars 항목과 일치해야 Fabric Loader 가 찾는다.

증상: 음악퀴즈 데이터팩 맵 접속 시 "모드 활성화" 메시지 안 뜸
      (ServerPlayConnectionEvents.JOIN 이 실행 안 되어 storage chat_answer:status
       active 가 0b 로 유지).
원인: 위와 같이 nested jar 가 로드 안 됨.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:50:25 +09:00
Claude
e01137ee31 v1.3.0 — single jar covers Fabric 1.21.6 + Fabric 26.1.2 + NeoForge 1.21.6
이전엔 1.21.6 (v1.1.1) 와 26.1.2 (v1.2.1) 가 분리된 jar 였음. 사용자 입장에서
버전별로 다른 파일을 받아야 했고 합친 의미가 없어서, 한 jar 가 어느 환경에든
들어갈 수 있도록 컨테이너 구조로 재작업.

구조:
  - outer chat_answer-1.3.0.jar
    ├── fabric.mod.json         (entrypoint 없는 컨테이너 메타. MC dep 없음.)
    ├── META-INF/neoforge.mods.toml  (NeoForge 1.21.6 모드 본체 메타)
    ├── kr/.../neoforge/        (NeoForge 1.21.6 entry + core, Mojang 매핑)
    ├── kr/.../core/            (NeoForge 가 쓰는 공유 core 사본)
    └── META-INF/jars/          (Fabric Loader 가 자동 스캔)
        ├── chat_answer-fabric-1216-1.3.0.jar  (MC ">=1.21.6 <1.22")
        └── chat_answer-fabric-2612-1.3.0.jar  (MC ">=26.1.2")

로더별 동작:
  - Fabric 1.21.6  → outer 는 no-op 컨테이너, 1216 nested 가 활성 (intermediary class_NNNN 리매핑됨)
  - Fabric 26.1.2  → outer 는 no-op 컨테이너, 2612 nested 가 활성 (intermediary 0.0.0 identity)
  - NeoForge 1.21.6 → outer 의 NeoForge entry 가 동작. Fabric 메타와 nested jars 는 NeoForge 가 무시.

핵심 트레이드오프:
  - 1.21.6 fabric subproject 는 modImplementation 필수 (intermediary 매핑 리맵 필요)
  - 26.1.2 fabric subproject 는 implementation 으로 충분 (서버 jar unobfuscated → identity 매핑)
  - 26.1.2 NeoForge 는 moddev plugin 이 아직 26.x 를 파싱 못 함 → 1.21.6 만 지원
  - 두 nested fabric jar 는 mod id 동일 (chat_answer_fabric) — depends.minecraft 가 상호 배타라
    한 환경에서 둘이 동시 candidate 가 되지 않으므로 충돌 없음.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 02:45:54 +09:00
Claude
785efe24b9 v1.2.1 — add icon, drop contact homepage
- Embed music quiz icon at assets/chat_answer/icon.png; reference from
  fabric.mod.json so ModMenu shows it.
- Remove contact.homepage (private Gitea — owner-only).
2026-05-14 02:28:20 +09:00
Claude
e4bfda783a v1.2.0 — target MC 26.1.2
User reported NoClassDefFoundError: net/minecraft/class_7471 on MC 26.1.2.
Root cause: v1.1.1 was built for 1.21.6 intermediary, which uses
class_NNNN obfuscated names. MC 26.x ships an unobfuscated server jar
with Mojang names directly, so intermediary lookups for class_7471
(=PlayerChatMessage in 1.21.6) fail at runtime.

Build retargeted to 26.1.2:
- minecraft 26.1.2 / loader 0.19.2 / fabric-api 0.148.2+26.1.2
- Loom 1.16-SNAPSHOT, Shadow 9.4.1 (Java 25 bytecode support)
- Gradle 9.5.1, JDK 25 toolchain
- Drop officialMojangMappings(); use intermediary:0.0.0 identity
  (Mojang stopped publishing proguard mappings for 26.x).
- Adapt code: ServerPlayer.getServer() removed in 26.1.2 → use
  player.level().getServer() (ServerPlayer.level() returns ServerLevel).
- NeoForge dropped from this build — moddev plugin can't parse 26.1.2
  yet, falls back to 1.21.5.
- 1.2.0 = Fabric only; 1.21.6 users stay on 1.1.1.
2026-05-14 01:55:02 +09:00
Claude
fd9d17e818 v1.1.1 — make mod load on client (single-player) too
fabric.mod.json 의 environment 가 "server" 라 client 설치에선 Fabric Loader
가 모드를 건너뛰어 ModMenu 에 안 나타나는 문제. "*" 로 바꿔서 client (싱글
플레이 integrated server 포함) + dedicated server 양쪽에서 로드되게 함.
NeoForge 쪽도 side = "BOTH" 로 변경. 실제 로직은 변함없이 server 측 채팅
이벤트만 hook 하므로 client 단독에서는 no-op.
2026-05-14 00:46:11 +09:00
Claude
464762589c Add player-join hook to signal mod presence to datapack (v1.1.0)
ChatAnswerCore.onPlayerJoin 이 storage chat_answer:status active=1b 를 set.
Fabric 측은 ServerPlayConnectionEvents.JOIN, NeoForge 측은 PlayerLoggedInEvent
에서 호출. 데이터팩이 mq:load 단계에서 0b 로 clear 해 두므로, 모드가 빠진
환경에선 이 hook 이 일어나지 않아 0b 가 유지되고, 모드가 있으면 첫 로그인
직후 1b 로 갱신되어 데이터팩이 활성 상태를 분기할 수 있다.
2026-05-13 22:20:00 +09:00
Claude
2c35e77b8b Restructure into multi-loader (Fabric + NeoForge) with merged jar
common 디렉토리에 로더 비종속 ChatAnswerCore 를 두고, fabric/ 과 neoforge/
서브프로젝트가 각자 진입점만 갖도록 분리. 두 결과물을 하나의 jar 로 묶기 위해
Fabric 측 common 클래스를 Shadow 의 relocate 로 kr.tkrmagid.chatanswer.fabric.core
패키지로 옮긴다 (Fabric 은 intermediary, NeoForge 는 Mojang 매핑이라 같은 클래스
경로에 그대로 두면 충돌). 루트의 mergedJar 태스크가 :fabric:relocatedJar 와
:neoforge:jar 를 합쳐서 build/libs/chat_answer-<version>-all.jar 생성.

산출물: 9.9KB 통합 jar 가 Fabric / NeoForge 양쪽에서 작동.
2026-05-13 22:10:30 +09:00
Claude
3e59d08db1 Initial Fabric mod for chat-based quiz answer submission
Hooks ServerMessageEvents.ALLOW_CHAT_MESSAGE to intercept chat while the
music_quiz datapack is in answer-accepting state (init=5 on objective
'main'). Intercepted messages are forwarded to mq:answer/submit with the
player's UUID context and the chat broadcast is suppressed so the answer
stays hidden from other players. Outside that state chat passes through
normally.

Target: Minecraft 1.21.6+, Java 21, Fabric Loader 0.16+, Fabric API.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 21:44:16 +09:00
25 changed files with 1126 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# Gradle
.gradle/
build/
out/
*.iml
.idea/
# Loom
.fabric/
run/
remappedSrc/
# OS
.DS_Store
Thumbs.db

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 tkrmagid
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

61
README.md Normal file
View File

@@ -0,0 +1,61 @@
# chat_answer (채팅정답)
음악퀴즈(`mq`) 데이터팩의 짝이 되는 **서버사이드** 모드.
정답 입력을 받는 상태(`scoreboard players get init main == 5`) 동안 플레이어가
채팅을 입력하면 메시지를 가로채서 다음을 실행한다:
```
execute as <플레이어 UUID> run function mq:answer/submit {text:'<채팅 내용>'}
```
v1.3.8 부터는 정답 단계여도 채팅이 평소대로 broadcast 된다 (다른 플레이어
화면에 그대로 노출됨). 정답 보호는 데이터팩이 아니라 룸 운영자의 신뢰 기반
운영으로 처리한다. v1.3.7 까지는 정답 단계에서 채팅 broadcast 가 차단됐었다.
## 빌드
JDK 21 필요.
```
./gradlew buildAll
```
산출물:
- `build/libs/chat_answer-<version>-all.jar`**Fabric + NeoForge 통합 단일 jar** (권장)
- `fabric/build/libs/chat_answer-fabric-<version>.jar` — Fabric 전용
- `neoforge/build/libs/chat_answer-neoforge-<version>.jar` — NeoForge 전용
## 설치
서버의 `mods/` 폴더에 통합 jar (`*-all.jar`) 하나만 넣으면 된다. 로더가 Fabric 이든
NeoForge 든 자기 쪽 진입점만 인식해서 동작한다.
요구사항:
- Minecraft 1.21.6+ 서버
- Fabric: Fabric Loader 0.16+, Fabric API
- NeoForge: 21.6+
## 호환성
- 빌드 타깃: Minecraft 1.21.6 (Dialog 시스템 최초 도입 버전).
- 사용하는 API (`ServerMessageEvents.ALLOW_CHAT_MESSAGE` / `ServerChatEvent`,
`Scoreboard`, `MinecraftServer.getCommands()`) 는 1.21.x 전반에 안정적이라
같은 jar 가 보통 그대로 동작.
- Mojang 이 chat / scoreboard / command 시스템을 깨는 변경을 적용하면 재빌드 필요.
## 구조
- `common/` — 로더 비종속 핵심 로직 (Mojang 매핑 기반)
- `fabric/` — Fabric Loader 진입점 + `ServerMessageEvents`
- `neoforge/` — NeoForge 진입점 + `ServerChatEvent`
통합 jar 는 두 로더의 결과물을 하나로 묶되, Fabric 쪽 common 클래스는 패키지
재배치(`kr.tkrmagid.chatanswer.core``kr.tkrmagid.chatanswer.fabric.core`)로
NeoForge 쪽 같은 클래스와 충돌하지 않게 분리한다.
## 라이센스
MIT

82
build.gradle Normal file
View File

@@ -0,0 +1,82 @@
plugins {
id 'java'
}
allprojects {
apply plugin: 'java'
group = project.mod_group
version = project.mod_version
// 기본 JDK toolchain 은 Java 25 (26.x Loom 빌드 요구). subproject 가 필요하면
// 자체 release 21 등으로 다운그레이드.
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
tasks.withType(JavaCompile).configureEach {
options.release = 25
options.encoding = 'UTF-8'
}
repositories {
maven { url = 'https://maven.fabricmc.net/' }
maven { url = 'https://maven.neoforged.net/releases/' }
mavenCentral()
}
}
// ───── 단일 배포 jar 컨테이너 ────────────────────────────────────────────────
// 한 jar 가 어떤 환경에서도 동작하도록:
// * outer = NeoForge 1.21.6 모드 본체 (NeoForge 만 fabric.mod.json 을 무시)
// + 메타로 fabric.mod.json (entrypoint 없는 컨테이너)
// * META-INF/jars/ = Fabric 용 nested jar 둘 (1.21.6 / 26.1.2)
// Fabric Loader 가 depends.minecraft 로 자동 매칭. NeoForge 는 무시.
//
// 결과: chat_answer-<version>.jar 한 개를 Fabric 1.21.6 / Fabric 26.1.2 / NeoForge
// 1.21.6 어디에 넣어도 적절한 코드 경로가 활성화된다.
tasks.register('containerJar', Jar) {
dependsOn ':fabric-1216:remapJar',
':fabric-2612:remapJar',
':neoforge-1216:jar'
archiveBaseName = project.mod_id
archiveVersion = project.mod_version
archiveClassifier = ''
destinationDirectory = file('build/libs')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// 1. NeoForge 모드 본체 (classes + META-INF/neoforge.mods.toml + icon.png) 을 통째로.
// MANIFEST.MF 는 새 jar 가 자체적으로 생성하니 제외.
from(zipTree(project(':neoforge-1216').tasks.named('jar').flatMap { it.archiveFile })) {
exclude 'META-INF/MANIFEST.MF'
}
// 2. Fabric 컨테이너 메타데이터 (entrypoint 없이 그냥 "외피") 와 아이콘.
// fabric.mod.json 의 ${version} 만 치환.
filteringCharset = 'UTF-8'
from("${rootDir}/container-resources") {
filesMatching("fabric.mod.json") {
expand("version": project.mod_version)
}
}
// 3. Fabric nested jars. Fabric Loader 는 META-INF/jars/ 를 자동 스캔하지
// 않고 outer fabric.mod.json 의 "jars" 배열에 명시된 파일만 처리하므로,
// container-resources/fabric.mod.json 의 jars 항목과 일치하는 고정 파일명
// (버전 suffix 제거) 으로 넣는다.
into('META-INF/jars') {
from(project(':fabric-1216').tasks.named('remapJar').flatMap { it.archiveFile }) {
rename '.+\\.jar', 'chat_answer-fabric-1216.jar'
}
from(project(':fabric-2612').tasks.named('remapJar').flatMap { it.archiveFile }) {
rename '.+\\.jar', 'chat_answer-fabric-2612.jar'
}
}
}
tasks.register('buildAll') {
dependsOn 'containerJar'
}

View File

@@ -0,0 +1,196 @@
package kr.tkrmagid.chatanswer.core;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.ReadOnlyScoreInfo;
import net.minecraft.world.scores.Scoreboard;
import net.minecraft.world.scores.ScoreHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 채팅정답 핵심 로직 — 로더 비종속.
*
* 정답 입력 상태(scoreboard main / init = 5) 일 때 채팅을 가로채서
* execute as <player UUID> run function mq:answer/submit {text:'<채팅>'}
* 을 OP 레벨로 실행한다.
*
* v1.3.8 부터 채팅은 어떤 단계에서도 broadcast 차단하지 않는다 — 정답 입력
* 단계에서도 친 채팅이 평소처럼 채팅창에 보인다. (사용자 요청: 정답 화면
* 노출을 데이터팩이 관리하지 않고 룸 운영자가 신뢰 기반으로 처리.)
* 따라서 {@link #handleChat} 는 항상 true 를 반환하며, 정답 단계일 때만
* 부가적으로 정답 제출 함수를 호출한다.
*/
public final class ChatAnswerCore {
public static final String MOD_ID = "chat_answer";
public static final String DISPLAY_NAME = "채팅정답";
private static final Logger LOG = LoggerFactory.getLogger(MOD_ID);
private static final String SCOREBOARD_OBJECTIVE = "main";
private static final String SCOREBOARD_HOLDER = "init";
private static final int ACCEPTING_ANSWER_STATE = 5;
/** 음악퀴즈 데이터팩이 선언한 "모드 존재 확인" 점수 이름.
* 본 모드는 서버 측에서 채팅을 가로채는 server-only 모드 — 클라이언트는
* 설치할 필요가 없고 server 한 곳에 있으면 모든 플레이어에게 적용된다.
* 따라서 per-player 검증은 무의미하고, fake player {@link #PRESENCE_HOLDER}
* 점수만 1 로 set 한다. 데이터팩의 start 가드는
* `score <PRESENCE_HOLDER> <OBJECTIVE> matches 1` 로 검사.
*
* presence pulse 는 여러 이벤트에서 중복 호출한다 — banner/mohist 같은
* fabric-bukkit 하이브리드 호스트에서 일부 Fabric 이벤트(특히
* ServerTickEvents.END_SERVER_TICK) 가 안 들어오는 케이스가 보고됨.
* SERVER_STARTED / PlayerJoin / TickEnd 셋 중 하나라도 firing 되면
* 데이터팩 가드가 통과하도록 모든 진입점에서 markModPresence 호출. */
private static final String MOD_PRESENCE_OBJECTIVE = "mq_chat_mod";
private static final String PRESENCE_HOLDER = "#server";
/** JOIN 이벤트 시점엔 클라이언트가 chat HUD 를 받을 준비가 안 됐을 수 있어
* tellraw 패킷이 사라지는 경우가 있다. 그래서 N 틱 늦춰서 호출한다. */
private static final int NOTICE_DELAY_TICKS = 20;
private static final Map<UUID, Integer> PENDING_NOTICES = new ConcurrentHashMap<>();
private ChatAnswerCore() {}
/**
* 플레이어 로그인 시점에 호출. 음악퀴즈 데이터팩의
* mq:players/mod_active_notice
* 함수를 해당 플레이어 컨텍스트로 호출한다. 단, JOIN 이벤트가 너무 일러서
* 즉시 호출 시 tellraw 가 클라이언트에 도달하지 못하는 race 가 있어
* {@link #NOTICE_DELAY_TICKS} 만큼 늦춘다 ({@link #onServerTick} 가 처리).
*/
public static void onPlayerJoin(ServerPlayer player) {
String name = player.getName().getString();
LOG.info("[{}] onPlayerJoin fired for {}, scheduling notice in {} ticks",
MOD_ID, name, NOTICE_DELAY_TICKS);
PENDING_NOTICES.put(player.getUUID(), NOTICE_DELAY_TICKS);
// tick 이벤트가 안 들어오는 호스트 대비 — join 시점에도 presence 한 번 찍는다.
MinecraftServer server = player.level().getServer();
if (server != null) markModPresence(server);
}
/** 각 로더 entrypoint 가 서버 부팅 완료 시점에 호출. tick 이벤트가
* 발화되지 않는 환경(banner/mohist) 에서 최소 한 번은 presence 가 찍히도록.
* 데이터팩 load 가 SERVER_STARTED 보다 먼저 끝나므로 objective 도 이미 존재. */
public static void onServerStarted(MinecraftServer server) {
LOG.info("[{}] onServerStarted fired, marking presence", MOD_ID);
markModPresence(server);
}
/** /reload 직후 호출. load.mcfunction 이 mq_chat_mod objective 를 remove/add
* 하고 `#server` 점수를 0 으로 재설정하므로, reload 끝난 직후 즉시
* 다시 1 로 찍어야 함. tick 이벤트가 죽은 호스트 + 이미 접속 중인
* 플레이어 조합에서 SERVER_STARTED/JOIN 둘 다 발화 안 되는 케이스 커버. */
public static void onDataPackReload(MinecraftServer server) {
LOG.info("[{}] onDataPackReload fired, re-marking presence", MOD_ID);
markModPresence(server);
}
/** 각 로더 entrypoint 가 매 server tick 마다 호출해야 한다. */
public static void onServerTick(MinecraftServer server) {
markModPresence(server);
if (PENDING_NOTICES.isEmpty()) return;
Iterator<Map.Entry<UUID, Integer>> it = PENDING_NOTICES.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<UUID, Integer> e = it.next();
int remaining = e.getValue() - 1;
if (remaining > 0) {
e.setValue(remaining);
continue;
}
UUID uuid = e.getKey();
it.remove();
ServerPlayer player = server.getPlayerList().getPlayer(uuid);
if (player == null) continue;
deliverNotice(server, player);
}
}
/**
* 데이터팩의 mq_chat_mod 점수(fake player #server 키) 를 1 로 set.
* 데이터팩이 아직 load 되지 않아 objective 가 없으면 조용히 skip.
* 점수 값이 이미 1 이면 Minecraft 가 packet 전송을 생략하므로
* 매 tick 호출해도 트래픽은 늘지 않는다.
*/
private static void markModPresence(MinecraftServer server) {
Scoreboard scoreboard = server.getScoreboard();
Objective objective = scoreboard.getObjective(MOD_PRESENCE_OBJECTIVE);
if (objective == null) return;
scoreboard.getOrCreatePlayerScore(ScoreHolder.forNameOnly(PRESENCE_HOLDER), objective).set(1);
}
private static void deliverNotice(MinecraftServer server, ServerPlayer player) {
String name = player.getName().getString();
// 플레이어 자체를 source 로 써서 함수 안의 @s 가 그대로 player.
CommandSourceStack source = player.createCommandSourceStack().withSuppressedOutput();
try {
server.getCommands().performPrefixedCommand(source, "function mq:players/mod_active_notice");
LOG.info("[{}] mod_active_notice delivered for {}", MOD_ID, name);
} catch (Exception e) {
LOG.warn("[{}] failed to deliver mod_active_notice for {}: {}", MOD_ID, name, e.toString(), e);
}
}
/**
* 항상 true 반환 — 어떤 단계에서도 채팅을 차단하지 않는다.
* 정답 단계(state 5) 일 때만 부가적으로 정답 제출 함수를 호출한다.
*
* @return 항상 true (broadcast 허용). 로더 진입점은 반환값을 그대로 이벤트
* allow/cancel 결정에 전달하면 된다.
*/
public static boolean handleChat(ServerPlayer sender, String rawText) {
MinecraftServer server = sender.level().getServer();
if (server == null) return true;
if (isAcceptingAnswer(server)) {
submitAnswer(server, sender, rawText);
}
return true;
}
private static boolean isAcceptingAnswer(MinecraftServer server) {
Scoreboard scoreboard = server.getScoreboard();
Objective objective = scoreboard.getObjective(SCOREBOARD_OBJECTIVE);
if (objective == null) return false;
ReadOnlyScoreInfo score = scoreboard.getPlayerScoreInfo(ScoreHolder.forNameOnly(SCOREBOARD_HOLDER), objective);
if (score == null) return false;
return score.value() == ACCEPTING_ANSWER_STATE;
}
private static void submitAnswer(MinecraftServer server, ServerPlayer sender, String rawText) {
String safe = sanitize(rawText);
if (safe.isEmpty()) return;
String nbt = safe.replace("\\", "\\\\").replace("'", "\\'");
String command = "execute as " + sender.getStringUUID()
+ " run function mq:answer/submit {text:'" + nbt + "'}";
CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput();
try {
server.getCommands().performPrefixedCommand(source, command);
} catch (Exception e) {
LOG.error("[{}] failed to submit answer for {}: {}", MOD_ID, sender.getName().getString(), e.toString());
}
}
/**
* 매크로 라인 ($data ... set value "$(text)") 가 큰따옴표로 값을 감싸므로
* 큰따옴표/백슬래시는 제거. 제어문자도 NBT 호환을 위해 제거.
*/
private static String sanitize(String text) {
if (text == null) return "";
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '"' || c == '\\') continue;
if (c < 0x20 || c == 0x7f) continue;
sb.append(c);
}
return sb.toString().strip();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,19 @@
{
"schemaVersion": 1,
"id": "chat_answer",
"version": "${version}",
"name": "채팅정답",
"description": "음악퀴즈(mq) 데이터팩이 정답 입력을 받는 상태(init=5)에서 채팅을 가로채 mq:answer/submit 함수로 전달합니다. 단일 jar 에 1.21.6 (Fabric/NeoForge) + 26.1.2 (Fabric) 빌드가 모두 들어있어 어느 환경에서도 그대로 동작합니다.",
"authors": [ "tkrmagid" ],
"license": "MIT",
"icon": "assets/chat_answer/icon.png",
"environment": "*",
"jars": [
{ "file": "META-INF/jars/chat_answer-fabric-1216.jar" },
{ "file": "META-INF/jars/chat_answer-fabric-2612.jar" }
],
"depends": {
"fabricloader": ">=0.16.0",
"java": ">=21"
}
}

53
fabric-1216/build.gradle Normal file
View File

@@ -0,0 +1,53 @@
plugins {
id 'fabric-loom' version '1.16-SNAPSHOT'
}
base.archivesName = "${project.mod_id}-fabric-1216"
// 1.21.6 은 Java 21 런타임. release 21 로 컴파일.
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
// common/ 디렉토리의 로더 비종속 소스 포함. Mojang 매핑으로 컴파일됨.
sourceSets {
main {
java {
srcDirs += "${rootDir}/common/src/main/java"
}
}
}
dependencies {
minecraft "com.mojang:minecraft:${project.mc_1216}"
mappings loom.officialMojangMappings()
modImplementation "net.fabricmc:fabric-loader:${project.fabric_loader_1216}"
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_1216}"
}
loom {
serverOnlyMinecraftJar()
}
processResources {
inputs.property "version", project.version
inputs.property "mod_id", project.mod_id
filteringCharset = 'UTF-8'
filesMatching("fabric.mod.json") {
expand(
"version": project.version,
"mod_id": project.mod_id
)
}
}
jar {
from(rootProject.file("LICENSE")) {
rename { "${it}_${project.mod_id}_fabric_1216" }
}
}

View File

@@ -0,0 +1,36 @@
package kr.tkrmagid.chatanswer.fabric;
import kr.tkrmagid.chatanswer.core.ChatAnswerCore;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.message.v1.ServerMessageEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ChatAnswerFabric implements ModInitializer {
private static final Logger LOG = LoggerFactory.getLogger(ChatAnswerCore.MOD_ID);
@Override
public void onInitialize() {
LOG.info("[{}] Fabric entrypoint onInitialize starting", ChatAnswerCore.MOD_ID);
try {
ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) ->
ChatAnswerCore.handleChat(sender, message.signedContent())
);
ServerLifecycleEvents.SERVER_STARTED.register(ChatAnswerCore::onServerStarted);
ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((server, resourceManager, success) -> {
if (success) ChatAnswerCore.onDataPackReload(server);
});
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) ->
ChatAnswerCore.onPlayerJoin(handler.player)
);
ServerTickEvents.END_SERVER_TICK.register(ChatAnswerCore::onServerTick);
LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + SERVER_STARTED + END_DATA_PACK_RELOAD + JOIN + TICK", ChatAnswerCore.MOD_ID);
} catch (Throwable t) {
LOG.error("[{}] Fabric entrypoint event registration failed", ChatAnswerCore.MOD_ID, t);
throw t;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "chat_answer_fabric",
"version": "${version}",
"name": "채팅정답 (Fabric impl)",
"description": "음악퀴즈(mq) 데이터팩이 정답 입력을 받는 상태(init=5)에서 채팅을 가로채 mq:answer/submit 함수로 전달합니다. (MC 1.21.6 변형)",
"authors": [ "tkrmagid" ],
"license": "MIT",
"icon": "assets/chat_answer/icon.png",
"environment": "*",
"entrypoints": {
"main": [ "kr.tkrmagid.chatanswer.fabric.ChatAnswerFabric" ]
},
"depends": {
"fabricloader": ">=0.16.0",
"minecraft": ">=1.21.6 <1.22",
"java": ">=21",
"fabric-api": "*"
}
}

42
fabric-2612/build.gradle Normal file
View File

@@ -0,0 +1,42 @@
plugins {
id 'fabric-loom' version '1.16-SNAPSHOT'
}
base.archivesName = "${project.mod_id}-fabric-2612"
// common/ 디렉토리의 로더 비종속 소스 포함.
sourceSets {
main {
java {
srcDirs += "${rootDir}/common/src/main/java"
}
}
}
dependencies {
// MC 26.x: server jar 가 unobfuscated. intermediary 0.0.0 = identity mapping.
minecraft "com.mojang:minecraft:${project.mc_2612}"
mappings "net.fabricmc:intermediary:0.0.0:v2"
implementation "net.fabricmc:fabric-loader:${project.fabric_loader_2612}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_2612}"
}
processResources {
inputs.property "version", project.version
inputs.property "mod_id", project.mod_id
filteringCharset = 'UTF-8'
filesMatching("fabric.mod.json") {
expand(
"version": project.version,
"mod_id": project.mod_id
)
}
}
jar {
from(rootProject.file("LICENSE")) {
rename { "${it}_${project.mod_id}_fabric_2612" }
}
}

View File

@@ -0,0 +1,36 @@
package kr.tkrmagid.chatanswer.fabric;
import kr.tkrmagid.chatanswer.core.ChatAnswerCore;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.message.v1.ServerMessageEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ChatAnswerFabric implements ModInitializer {
private static final Logger LOG = LoggerFactory.getLogger(ChatAnswerCore.MOD_ID);
@Override
public void onInitialize() {
LOG.info("[{}] Fabric entrypoint onInitialize starting", ChatAnswerCore.MOD_ID);
try {
ServerMessageEvents.ALLOW_CHAT_MESSAGE.register((message, sender, params) ->
ChatAnswerCore.handleChat(sender, message.signedContent())
);
ServerLifecycleEvents.SERVER_STARTED.register(ChatAnswerCore::onServerStarted);
ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((server, resourceManager, success) -> {
if (success) ChatAnswerCore.onDataPackReload(server);
});
ServerPlayConnectionEvents.JOIN.register((handler, sender, server) ->
ChatAnswerCore.onPlayerJoin(handler.player)
);
ServerTickEvents.END_SERVER_TICK.register(ChatAnswerCore::onServerTick);
LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + SERVER_STARTED + END_DATA_PACK_RELOAD + JOIN + TICK", ChatAnswerCore.MOD_ID);
} catch (Throwable t) {
LOG.error("[{}] Fabric entrypoint event registration failed", ChatAnswerCore.MOD_ID, t);
throw t;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"id": "chat_answer_fabric",
"version": "${version}",
"name": "채팅정답 (Fabric impl)",
"description": "음악퀴즈(mq) 데이터팩이 정답 입력을 받는 상태(init=5)에서 채팅을 가로채 mq:answer/submit 함수로 전달합니다. (MC 26.1.2 변형)",
"authors": [ "tkrmagid" ],
"license": "MIT",
"icon": "assets/chat_answer/icon.png",
"environment": "*",
"entrypoints": {
"main": [ "kr.tkrmagid.chatanswer.fabric.ChatAnswerFabric" ]
},
"depends": {
"fabricloader": ">=0.19.0",
"minecraft": ">=26.1.2",
"java": ">=21",
"fabric-api": "*"
}
}

26
gradle.properties Normal file
View File

@@ -0,0 +1,26 @@
org.gradle.jvmargs=-Xmx3G
org.gradle.parallel=true
# ───── mod metadata ─────────────────────────────────────────────────────────
mod_id=chat_answer
mod_version=1.4.0
mod_group=kr.tkrmagid.chatanswer
mod_name=채팅정답
# ───── per-target MC / loader versions ──────────────────────────────────────
# 한 jar 로 1.21.6 (Fabric/NeoForge) + 26.1.2 (Fabric) 전부 커버하기 위해
# 각 타겟마다 별도 subproject 가 자기 버전으로 빌드되고, 결과물을 outer
# container jar 가 묶는다 (Fabric 은 META-INF/jars/ JiJ, NeoForge 는 outer 본체).
# Fabric MC 1.21.6
mc_1216=1.21.6
fabric_api_1216=0.128.2+1.21.6
fabric_loader_1216=0.16.10
# Fabric MC 26.1.2 (26.x 서버 jar 는 unobfuscated. intermediary 0.0.0 = identity)
mc_2612=26.1.2
fabric_api_2612=0.148.2+26.1.2
fabric_loader_2612=0.19.2
# NeoForge MC 1.21.6 (26.x 는 NeoForge moddev plugin 이 아직 인식 못 함)
neoforge_1216=21.6.20-beta

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,49 @@
plugins {
id 'net.neoforged.moddev' version '2.0.97'
}
base.archivesName = "${project.mod_id}-neoforge-1216"
// NeoForge 1.21.6 은 Java 21. release 21 로 컴파일.
java {
toolchain.languageVersion = JavaLanguageVersion.of(25)
}
tasks.withType(JavaCompile).configureEach {
options.release = 21
}
sourceSets {
main {
java {
srcDirs += "${rootDir}/common/src/main/java"
}
}
}
neoForge {
version = project.neoforge_1216
}
processResources {
inputs.property "version", project.version
inputs.property "mod_id", project.mod_id
inputs.property "minecraft_version", project.mc_1216
inputs.property "neoforge_version", project.neoforge_1216
filteringCharset = 'UTF-8'
filesMatching("META-INF/neoforge.mods.toml") {
expand(
"version": project.version,
"mod_id": project.mod_id,
"minecraft_version": project.mc_1216,
"neoforge_version": project.neoforge_1216
)
}
}
jar {
from(rootProject.file("LICENSE")) {
rename { "${it}_${project.mod_id}_neoforge_1216" }
}
}

View File

@@ -0,0 +1,57 @@
package kr.tkrmagid.chatanswer.neoforge;
import kr.tkrmagid.chatanswer.core.ChatAnswerCore;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.OnDatapackSyncEvent;
import net.neoforged.neoforge.event.ServerChatEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
@Mod(ChatAnswerCore.MOD_ID)
public final class ChatAnswerNeoForge {
public ChatAnswerNeoForge(IEventBus modBus) {
NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onServerChat);
NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onServerStarted);
NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onDatapackSync);
NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onPlayerLogin);
NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onServerTick);
}
@SubscribeEvent
public static void onServerChat(ServerChatEvent event) {
boolean allow = ChatAnswerCore.handleChat(event.getPlayer(), event.getRawText());
if (!allow) {
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onServerStarted(ServerStartedEvent event) {
ChatAnswerCore.onServerStarted(event.getServer());
}
/** OnDatapackSyncEvent: /reload 끝나면 player=null 로 한 번 broadcast,
* 로그인 때마다 해당 player 로 한 번 더 fire. 어느 쪽이든 reload 직후
* presence 가 다시 찍히는 것이 목적이라 둘 다 OK. */
@SubscribeEvent
public static void onDatapackSync(OnDatapackSyncEvent event) {
ChatAnswerCore.onDataPackReload(event.getPlayerList().getServer());
}
@SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
if (event.getEntity() instanceof ServerPlayer player) {
ChatAnswerCore.onPlayerJoin(player);
}
}
@SubscribeEvent
public static void onServerTick(ServerTickEvent.Post event) {
ChatAnswerCore.onServerTick(event.getServer());
}
}

View File

@@ -0,0 +1,25 @@
modLoader = "javafml"
loaderVersion = "[1,)"
license = "MIT"
[[mods]]
modId = "${mod_id}"
version = "${version}"
displayName = "채팅정답"
authors = "tkrmagid"
description = '''음악퀴즈(mq) 데이터팩이 정답 입력을 받는 상태(init=5)에서 채팅을 가로채 mq:answer/submit 함수로 전달합니다.'''
logoFile = "icon.png"
[[dependencies.${mod_id}]]
modId = "neoforge"
type = "required"
versionRange = "[${neoforge_version},)"
ordering = "NONE"
side = "BOTH"
[[dependencies.${mod_id}]]
modId = "minecraft"
type = "required"
versionRange = "[${minecraft_version},)"
ordering = "NONE"
side = "BOTH"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

16
settings.gradle Normal file
View File

@@ -0,0 +1,16 @@
pluginManagement {
repositories {
maven { url = 'https://maven.fabricmc.net/' }
maven { url = 'https://maven.neoforged.net/releases/' }
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = 'chat_answer'
// 세 개의 target-specific subproject. 각각 자기 MC/로더 버전으로 컴파일/리맵.
// rootProject 의 containerJar 가 셋의 산출물을 하나로 묶어 단일 jar 배포물 생성.
include 'fabric-1216'
include 'fabric-2612'
include 'neoforge-1216'