diff --git a/common/src/main/java/kr/tkrmagid/chatanswer/core/ChatAnswerCore.java b/common/src/main/java/kr/tkrmagid/chatanswer/core/ChatAnswerCore.java index c314c37..e26f89d 100644 --- a/common/src/main/java/kr/tkrmagid/chatanswer/core/ChatAnswerCore.java +++ b/common/src/main/java/kr/tkrmagid/chatanswer/core/ChatAnswerCore.java @@ -1,5 +1,9 @@ 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; @@ -29,36 +33,55 @@ public final class ChatAnswerCore { private static final String SCOREBOARD_HOLDER = "init"; private static final int ACCEPTING_ANSWER_STATE = 5; + /** JOIN 이벤트 시점엔 클라이언트가 chat HUD 를 받을 준비가 안 됐을 수 있어 + * tellraw 패킷이 사라지는 경우가 있다. 그래서 N 틱 늦춰서 호출한다. */ + private static final int NOTICE_DELAY_TICKS = 20; + private static final Map PENDING_NOTICES = new ConcurrentHashMap<>(); + private ChatAnswerCore() {} /** - * 플레이어 로그인 직후 호출. 음악퀴즈 데이터팩의 + * 플레이어 로그인 시점에 호출. 음악퀴즈 데이터팩의 * mq:players/mod_active_notice - * 함수를 해당 플레이어 컨텍스트로 직접 호출한다. 데이터팩이 메세지를 정의하고, - * 모드는 "지금 막 들어온 이 플레이어에게 보여라" 만 트리거한다. - * - * 이전엔 storage chat_answer:status active=1b 플래그를 썼는데, 통합 서버에서 - * mq:load 의 0b 초기화가 player join 이후에 도는 케이스 때문에 race 가 발생했다. - * 함수를 모드가 직접 호출하면 race 가 사라지고, 데이터팩이 없을 땐 함수가 - * 존재하지 않아 커맨드가 (suppressed source 라 채팅엔 안 뜨고 log warn 으로) - * 무시된다. + * 함수를 해당 플레이어 컨텍스트로 호출한다. 단, 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 {}", MOD_ID, name); - MinecraftServer server = player.level().getServer(); - if (server == null) { - LOG.warn("[{}] onPlayerJoin: server is null, skipping notice for {}", MOD_ID, name); - return; + LOG.info("[{}] onPlayerJoin fired for {}, scheduling notice in {} ticks", + MOD_ID, name, NOTICE_DELAY_TICKS); + PENDING_NOTICES.put(player.getUUID(), NOTICE_DELAY_TICKS); + } + + /** 각 로더 entrypoint 가 매 server tick 마다 호출해야 한다. */ + public static void onServerTick(MinecraftServer server) { + if (PENDING_NOTICES.isEmpty()) return; + Iterator> it = PENDING_NOTICES.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry 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); } - CommandSourceStack source = server.createCommandSourceStack().withSuppressedOutput(); - String command = "execute as " + player.getStringUUID() - + " at @s run function mq:players/mod_active_notice"; + } + + 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, command); - LOG.info("[{}] mod_active_notice invoked for {}", MOD_ID, name); + 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 invoke mod_active_notice for {}: {}", MOD_ID, name, e.toString(), e); + LOG.warn("[{}] failed to deliver mod_active_notice for {}: {}", MOD_ID, name, e.toString(), e); } } diff --git a/fabric-1216/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java b/fabric-1216/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java index ea8de97..fcb2165 100644 --- a/fabric-1216/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java +++ b/fabric-1216/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java @@ -2,6 +2,7 @@ package kr.tkrmagid.chatanswer.fabric; import kr.tkrmagid.chatanswer.core.ChatAnswerCore; import net.fabricmc.api.ModInitializer; +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; @@ -20,7 +21,8 @@ public final class ChatAnswerFabric implements ModInitializer { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ChatAnswerCore.onPlayerJoin(handler.player) ); - LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + JOIN", ChatAnswerCore.MOD_ID); + ServerTickEvents.END_SERVER_TICK.register(ChatAnswerCore::onServerTick); + LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + JOIN + TICK", ChatAnswerCore.MOD_ID); } catch (Throwable t) { LOG.error("[{}] Fabric entrypoint event registration failed", ChatAnswerCore.MOD_ID, t); throw t; diff --git a/fabric-2612/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java b/fabric-2612/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java index ea8de97..fcb2165 100644 --- a/fabric-2612/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java +++ b/fabric-2612/src/main/java/kr/tkrmagid/chatanswer/fabric/ChatAnswerFabric.java @@ -2,6 +2,7 @@ package kr.tkrmagid.chatanswer.fabric; import kr.tkrmagid.chatanswer.core.ChatAnswerCore; import net.fabricmc.api.ModInitializer; +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; @@ -20,7 +21,8 @@ public final class ChatAnswerFabric implements ModInitializer { ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> ChatAnswerCore.onPlayerJoin(handler.player) ); - LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + JOIN", ChatAnswerCore.MOD_ID); + ServerTickEvents.END_SERVER_TICK.register(ChatAnswerCore::onServerTick); + LOG.info("[{}] Fabric entrypoint registered: ALLOW_CHAT_MESSAGE + JOIN + TICK", ChatAnswerCore.MOD_ID); } catch (Throwable t) { LOG.error("[{}] Fabric entrypoint event registration failed", ChatAnswerCore.MOD_ID, t); throw t; diff --git a/gradle.properties b/gradle.properties index 985d773..08494b9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.parallel=true # ───── mod metadata ───────────────────────────────────────────────────────── mod_id=chat_answer -mod_version=1.3.3 +mod_version=1.3.4 mod_group=kr.tkrmagid.chatanswer mod_name=채팅정답 diff --git a/neoforge-1216/src/main/java/kr/tkrmagid/chatanswer/neoforge/ChatAnswerNeoForge.java b/neoforge-1216/src/main/java/kr/tkrmagid/chatanswer/neoforge/ChatAnswerNeoForge.java index b5cf676..326ef6a 100644 --- a/neoforge-1216/src/main/java/kr/tkrmagid/chatanswer/neoforge/ChatAnswerNeoForge.java +++ b/neoforge-1216/src/main/java/kr/tkrmagid/chatanswer/neoforge/ChatAnswerNeoForge.java @@ -8,12 +8,14 @@ import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.ServerChatEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent; +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::onPlayerLogin); + NeoForge.EVENT_BUS.addListener(ChatAnswerNeoForge::onServerTick); } @SubscribeEvent @@ -30,4 +32,9 @@ public final class ChatAnswerNeoForge { ChatAnswerCore.onPlayerJoin(player); } } + + @SubscribeEvent + public static void onServerTick(ServerTickEvent.Post event) { + ChatAnswerCore.onServerTick(event.getServer()); + } }