[Minecraft] Plugin Message Channel

phdljr·2025년 5월 12일

마인크래프트

목록 보기
1/1

서론

멀티 서버에 접속한 모든 사용자에게 사용자의 메세지를 전달하는 확성기 기능을 가진 마인크래프트 플러그인을 개발하게 되었다.

서버의 구조는 다음과 같이 구성돼있다.

보통 확성기 기능은 해당 서버의 broadcast 메서드를 통해 구현하는 듯 하지만, 여기선 사용자가 접속한 서버에서만 전체 전달이 된다.

어느 서버에 접속해있든, 확성기를 사용하면 모든 서버에 메세지가 전달되도록 하는 방법이 뭐가 없을까 싶어서 구글링을 해보니, 플러그인 메세지 채널이라는 기능을 알게 되었다.

이에 대해서 좀 더 알아보는 시간을 가져보도록 하겠다.

Plugin Message Channel

  • 서로 다른 플러그인 간, 또는 서버와 클라이언트 간 커스텀 메시지를 주고받기 위해 사용하는 비동기 통신 방식
    • RabbitMQ, Kafka처럼 메세지 브로커 역할을 하는 것처럼 보임
  • 채널이라는 통로를 통해 프록시 서버와 백엔드 서버 간 메세지를 주고받을 수 있음
  • 이미 만들어진 채널인 BungeeCord을 활용할 수도 있고, 개발자가 직접 채널을 새로 만들어서 통신할 수도 있음

채널 이름 규칙

  • 1.13 버전 이후 기준, (namespace):(name) 형태를 갖추는 것을 추천
    • 다른 플러그인과의 채널 명 충돌을 방지하기 위한 수단
    • Velocity API에선 ChannelIdentifier를 만들기 위해, 위와 같은 형태로 사용 중
    • 특이 케이스로, BungeeCord 채널은 내부적으로 bungeecord:main로 변환됨

BungeeCord 채널 예제 코드 - 점프한 유저를 특정 서버 이동

  • 사용자가 점프를 하면 wild 서버로 이동시키는 코드
  • 점프를 할 시, BungeeCord 채널로 Connect 프로토콜을 활용하여 wild 서버로 이동시키도록 채널로 메세지 전송
class PluginMessageChannel : JavaPlugin(), Listener {

    private val BUNGEE_CORD = "BungeeCord"

    override fun onEnable() {
    	// 해당 클래스를 이벤트 리스너로 등록
        server.pluginManager.registerEvents(this, this)
        
        // BungeeCord 채널로 메세지를 보낼 수 있도록 설정
        server.messenger.registerOutgoingPluginChannel(this, BUNGEE_CORD)
    }
	
    // 플레이어가 점프를 하면, wild 서버로 이동시키도록 BungeeCord 채널로 메세지 전송
    // BungeeCord 채널에서만 적용되는 프로토콜이 존재함으로, 이를 활용한 모습을 나타냄
    @EventHandler
    fun onPlayerJump(event: PlayerJumpEvent) {
        val player = event.getPlayer()

        val out = ByteStreams.newDataOutput()
        out.writeUTF("Connect")
        out.writeUTF("wild")
        player.sendPluginMessage(this, BUNGEE_CORD, out.toByteArray())
    }

    override fun onDisable() {}
}

BungeeCord 채널 예제 코드 - 특정 서버 유저 수 조회

// 플러그인 메세지 응답을 조회하기 위해선, PluginMessageListener를 구현해야 한다.
class PluginMessageChannel : JavaPlugin(), PluginMessageListener {

    private val BUNGEE_CORD = "BungeeCord"
    
    override fun onEnable() {
        server.pluginManager.registerEvents(this, this)
        server.messenger.registerOutgoingPluginChannel(this, BUNGEE_CORD)
        // 해당 클래스에서 BungeeCord 채널 메세지를 수신할 수 있도록 등록
        server.messenger.registerIncomingPluginChannel(this, BUNGEE_CORD, this)
		
        // 메세지를 전송하기 위해선 플레이어 객체가 필수이다.
        // 해당 예제에선 ... 으로 편의상 대체한다.
        val player = ...
        val output = ByteStreams.newDataOutput()
        out.writeUTF("PlayerCount")
        out.writeUTF("lobby")
        player.sendPluginMessage(this, BUNGEE_CORD, output.toByteArray())
    }

	// PluginMessageListener 구현부
    // 해당 메서드에서 채널에 대한 메세지를 조회한다.
    override fun onPluginMessageReceived(channel: String, player: Player, message: ByteArray) {
        if (channel != BUNGEE_CORD) {
            return
        }
        val input = ByteStreams.newDataInput(message)
        val subchannel = input.readUTF()
        if (subchannel == "PlayerCount") {
            val server = input.readUTF()
            val playerCount = input.readInt()
        }
    }

    override fun onDisable() {}
    
}

커스텀 채널 예제 코드 - 모든 서버에 채팅을 전송하는 확성기

  • 개발자가 임의로 채널 명을 정할 수 있음
  • 예제에선 broadcast:main 채널명을 갖도록 진행

Velocity API 플러그인

@Plugin(
    id = "broadcast",
    name = "broadcast",
    version = BuildConstants.VERSION
)
class Broadcast @Inject constructor(
    private val logger: Logger,
    private val proxy: ProxyServer
) {

	// broadcast:main 채널 정보를 담은 객체
    private val channel: ChannelIdentifier = MinecraftChannelIdentifier.create("broadcast", "main")

    @Subscribe
    fun onProxyInitialize(event: ProxyInitializeEvent) {
        proxy.channelRegistrar.register(channel)
        logger.info("Shout channel registered")
    }

	// 플러그인 채널 메세지 수신 리스너
    @Subscribe
    fun onPluginMessage(event: PluginMessageEvent) {
        // broadcast:main 채널이 아니라면 무시
        if (event.identifier != channel)
            return

        try {
            val dataStream = DataInputStream(ByteArrayInputStream(event.data))
            val message = dataStream.readUTF()
            val broadcast = Component.text("[확성기] $message", NamedTextColor.LIGHT_PURPLE)
            // 프록시에 연결된 모든 서버에 접속한 유저들에게 메세지 전송
            proxy.allPlayers.forEach { it.sendMessage(broadcast) }

            logger.info("Broadcasted shout: $message")
        } catch (e: Exception) {
            logger.error("메시지 수신 중 오류 발생", e)
        }
    }
}

Paper API 플러그인

class BroadcastCommand(val plugin: JavaPlugin): CommandExecutor {

    override fun onCommand(
        sender: CommandSender,
        command: Command,
        label: String,
        args: Array<out String>
    ): Boolean {

        if (sender !is Player) {
            sender.sendMessage("[알림] 플레이어만 사용할 수 있습니다.")
            return true
        }

        if (args.isEmpty()) {
            sender.sendMessage("[알림] 사용법: /확성기 <메시지>")
            return true
        }

        val message: String? = args.joinToString(" ")

        // 플러그인 메시지 전송
        try {
            val output = ByteArrayOutputStream()
            val data = DataOutputStream(output)
            data.writeUTF(sender.name + ": " + message) // 메시지 내용

            sender.sendPluginMessage(plugin, "broadcast:main", output.toByteArray())

            sender.sendMessage("[알림] 확성기 메시지를 전송했습니다!")
        } catch (e: IOException) {
            sender.sendMessage("[알림] 전송 실패: " + e.message)
        }

        return true
    }
}

결과

예제 코드 - BungeeCord의 MessageRaw를 활용한 모든 서버에 채팅을 전송하는 확성기

  • 사실, BungeeCord 채널의 MessageRaw타입을 사용하면 프록시 플러그인 없이도 가능
public class MyPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        this.getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");

        Player player = ...;
        ByteArrayDataOutput out = ByteStreams.newDataOutput();
        out.writeUTF("MessageRaw");
        out.writeUTF("ALL");
        out.writeUTF(GsonComponentSerializer.gson().serialize(
                Component.text("Click Me!").clickEvent(ClickEvent.openUrl("https://papermc.io"))
        ));
        player.sendPluginMessage(this, "BungeeCord", out.toByteArray());
    }
}

결론

플러그인 메세지 채널을 활용하여 프록시 서버와 백엔드 서버간 통신이 가능하다.


참조

https://docs.papermc.io/paper/dev/plugin-messaging/

profile
난 Java도 좋고, 다른 것들도 좋아

0개의 댓글