본문으로 건너뛰기
버전: Minecraft 26.2

이벤트

이벤트는 mod.events().register(이벤트 키, 리스너)로 등록합니다. 리스너는 람다로 씁니다.

mod.events().register(PlayerEvents.JOINED, player -> player.sendMessage("어서 오세요"));
  • 등록은 어느 스레드에서든 언제나 할 수 있으며, 보통은 엔트리포인트에서 합니다. 이벤트가 진행되는 중에 등록한 리스너는 다음 번부터 불립니다.
  • 리스너가 예외를 던지면 Poly 모드 로더가 그 리스너를 비활성화하고 POLY-EVENT-001로 기록합니다.
  • 리스너가 받은 뷰는 그 호출이 진행되는 동안에만 유효합니다.

우선순위​

mod.events().register(BlockEvents.BREAK, EventPriority.HIGH, (world, pos, block, player) ->
claimed(pos) ? EventResult.DENY : EventResult.PASS); // 먼저 결정
mod.events().register(BlockEvents.BROKEN, (world, pos, block, player) -> { /* … */ }); // 우선순위 없으면 NORMAL
  • 리스너는 EventPriority의 HIGHEST, HIGH, NORMAL, LOW, LOWEST 순서로 실행됩니다. 우선순위가 같으면 등록한 순서대로 실행되므로, 엔트리포인트에서 등록한 리스너는 모드 로드 순서를 따릅니다.
  • 우선순위 없이 register(key, listener)로 등록하면 NORMAL입니다. 우선순위가 생기기 전에 만든 모드도 이전과 똑같이 동작합니다.
  • 결정 이벤트에서 먼저 판단해야 한다면 높은 우선순위를 씁니다(예: 보호 구역에서 블록 부수기를 막는 보호 모드). 다른 모드가 건드리지 않은 결과만 보고 싶다면 낮은 우선순위를 씁니다.
  • 이름과 순서는 NeoForge의 EventPriority와 같습니다. Fabric의 이벤트 단계(phase)도 이 우선순위에 대응합니다(기본 단계는 NORMAL).
  • 우선순위는 EntityEvents.ALLOW_DAMAGE, MODIFY_DAMAGE, PlayerEvents.LOGIN_CHECK를 포함한 모든 이벤트에 적용됩니다.

결정 이벤트와 결과​

결정 이벤트는 게임이 하려는 동작을 리스너가 취소하거나 강제할 수 있는 이벤트입니다. 리스너는 EventResult(dev.polymodloader.api.event)를 돌려주고, 사용(use) 이벤트는 InteractionResult를 돌려줍니다.

EventResult뜻
PASS의견 없음. 다음 리스너가 실행되고, 아무도 결정하지 않으면 게임이 원래대로 진행합니다
ALLOW동작을 허락하고 남은 리스너를 건너뜁니다. 게임 자체의 검사(도구, 권한 등)는 그대로 적용됩니다
DENY동작을 취소하고 남은 리스너를 건너뜁니다
  • 모든 결정 이벤트의 규칙은 같습니다. 리스너가 우선순위 순서로 실행되다가, PASS가 아닌 값을 처음 돌려준 리스너가 결과를 정하고, 그 뒤의 리스너는 불리지 않습니다.
  • 결정 리스너가 예외를 던지면 PASS로 치고 그 리스너를 비활성화합니다(POLY-EVENT-001). null을 돌려준 리스너도 똑같이 처리합니다(POLY-EVENT-003).
  • 기존 이벤트는 원래 규칙을 지킵니다. EntityEvents.ALLOW_DAMAGE는 모든 리스너를 실행하고 하나라도 false면 취소하며, MODIFY_DAMAGE는 앞 리스너의 결과를 다음 리스너에 넘기고, PlayerEvents.LOGIN_CHECK는 처음 나온 거절 사유를 씁니다.

서버​

ServerEvents는 전용 서버, 통합 서버(싱글 플레이), 게임 테스트 서버의 서버 스레드에서 STARTING → STARTED → TICK(반복) → STOPPING → STOPPED 순서로 불립니다.

이벤트리스너시점
ServerEvents.STARTINGserver -> … (ServerView)레지스트리와 데이터 팩을 불러온 뒤, 월드를 만들기 직전
ServerEvents.STARTEDserver -> …서버마다 첫 틱 직전에 1회
ServerEvents.TICKserver -> …매 틱 시작
ServerEvents.STOPPINGserver -> …정지를 시작할 때(월드 저장 전)
ServerEvents.STOPPEDserver -> …플레이어와 월드를 저장하고 월드를 닫은 뒤

ServerView: tickCount(), playerCount(), motd(), dedicated()

mod.events().register(ServerEvents.STARTING, server -> LOGGER.info("서버 시작 중 (dedicated={})", server.dedicated()));
mod.events().register(ServerEvents.STOPPED, server -> LOGGER.info("{}틱 뒤 서버 정지", server.tickCount()));

플레이어​

이벤트리스너시점
PlayerEvents.JOINEDplayer -> …플레이어가 접속을 마치고 월드에 들어올 때
PlayerEvents.LEAVINGplayer -> …플레이어가 서버에서 제거되기 직전(접속 종료, 추방)
PlayerEvents.LOGIN_CHECK(uuid, name) -> Optional<String>로그인 중에 호출. 사유를 돌려주면 접속을 거절
PlayerEvents.COPY(oldPlayer, newPlayer, returnedFromEnd) -> …리스폰할 때 게임이 새 플레이어 객체를 만들고 옛 객체의 데이터를 옮기는 중
PlayerEvents.RESPAWNED(player, returnedFromEnd) -> …새 플레이어 객체가 월드에 들어간 뒤

모두 서버 스레드에서 호출됩니다. LOGIN_CHECK는 운영에서 자세히 설명합니다.

리스폰​

플레이어가 죽은 뒤나 End에서 나올 때, 게임은 새 플레이어 객체를 만듭니다.

  • COPY는 그 객체를 만드는 도중에 불립니다. 게임이 남기는 것(keepInventory일 때 인벤토리, End에서 나올 때는 전부)과 Poly 모드 로더의 EntityDataKey 데이터는 이미 복사된 상태입니다. 그 밖에 모드가 플레이어 객체마다 들고 있는 값은 여기서 옮기세요. 새 플레이어는 아직 월드에 없습니다.
  • RESPAWNED는 새 플레이어 객체가 월드에 놓인 뒤에 불립니다.
  • returnedFromEnd는 End 출구 포털로 살아서 나왔을 때 true, 죽은 뒤라면 false입니다.
mod.events().register(PlayerEvents.COPY, (oldPlayer, newPlayer, returnedFromEnd) -> {
// 엔티티 데이터는 이미 복사됨. 새 플레이어 객체에 리스폰 횟수를 셉니다.
StoredData data = newPlayer.data(respawns);
data.data().putInt("count", data.data().getInt("count", 0) + 1);
data.markChanged();
});
mod.events().register(PlayerEvents.RESPAWNED, (player, returnedFromEnd) -> { /* … */ });

상호작용​

InteractionEvents(dev.polymodloader.api.event.interaction)는 서버가 플레이어의 클릭을 처리할 때 서버 스레드에서 불립니다. 접속한 플레이어와 모의 플레이어 모두 해당하며, 모두 결정 이벤트입니다.

이벤트리스너시점
InteractionEvents.USE_BLOCK(player, hand, stack, pos, face) -> InteractionResult플레이어가 블록을 우클릭할 때, 든 아이템이나 블록이 반응하기 전. 게임이 시도하는 손마다 한 번 불립니다(주 손 먼저, 주 손이 넘기면 보조 손)
InteractionEvents.USE_ITEM(player, hand, stack) -> InteractionResult블록이나 엔티티가 아닌 허공에 아이템을 우클릭할 때, 아이템이 반응하기 전. 빈손일 때도 불립니다
InteractionEvents.USE_ENTITY(player, hand, stack, target) -> InteractionResult플레이어가 엔티티를 우클릭할 때, 엔티티나 든 아이템이 반응하기 전
InteractionEvents.ATTACK_ENTITY(player, target) -> EventResult플레이어가 엔티티를 공격(좌클릭)할 때, 피해와 넉백 전
  • hand는 Hand(MAIN_HAND, OFF_HAND, dev.polymodloader.api.entity 패키지)입니다. stack은 그 손에 든 실제 스택이며 비어 있을 수 있습니다.
  • 사용 이벤트: PASS면 다음 리스너와 게임이 클릭을 처리합니다. SUCCESS나 CONSUME은 처리된 것으로 끝내므로 게임 자체 동작(상자 열기, 블록 놓기, 먹기, 거래, 탑승)이 일어나지 않습니다. FAIL은 거절로 끝냅니다. 결과가 정해지면 서버가 인벤토리를 다시 보내 클라이언트의 예측을 바로잡습니다.
  • 공격: DENY면 공격이 취소되어 피해, 넉백, 내구도 소모가 모두 없습니다.
  • 클라이언트가 클릭 결과를 먼저 예측해 그리는 쪽은 아직 이벤트가 없습니다.
mod.events().register(InteractionEvents.USE_BLOCK, (player, hand, stack, pos, face) ->
onWard(player.world(), pos) ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.USE_ITEM, (player, hand, stack) ->
stack.customName().filter("Sealed"::equals).isPresent() ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.USE_ENTITY, (player, hand, stack, target) ->
onWard(target.world(), target.blockPos()) ? InteractionResult.FAIL : InteractionResult.PASS);
mod.events().register(InteractionEvents.ATTACK_ENTITY, (player, target) ->
onWard(target.world(), target.blockPos()) ? EventResult.DENY : EventResult.PASS);

onWard(world, pos)는 예제 모드(examples/native-showcase-mod의 InteractionContent)의 도우미 메서드입니다. pos 아래 블록이 블록 태그에 들어 있는지를 태그의 world.isBlockIn으로 확인합니다.

블록​

이벤트리스너시점
BlockEvents.BREAK(world, pos, block, player) -> EventResult플레이어가 블록을 부수려 할 때(서바이벌·창작), 블록을 없애기 전. DENY면 블록이 남음
BlockEvents.BROKEN(world, pos, block, player) -> …플레이어가 블록을 부순 뒤(서바이벌·창작)
BlockEvents.PLACE(world, pos, block, player) -> EventResult플레이어가 블록 아이템(침대·문 포함)으로 블록을 놓으려 할 때, 게임이 자리를 확인한 뒤. DENY면 거절
BlockEvents.PLACED(world, pos, block, player) -> …블록을 놓고 블록 엔티티 데이터 적용과 아이템 소모까지 끝난 뒤

모두 서버 스레드에서 호출됩니다.

  • BREAK는 게임이 든 도구, 관리자 블록, 모험 모드를 검사하기 전에 불립니다. 그래서 게임이 결국 거절할 부수기에도 불립니다. DENY면 블록(과 블록 엔티티)이 그대로 있다고 플레이어의 클라이언트에 다시 알립니다. BREAK는 BROKEN보다 먼저 불립니다.
  • PLACE가 DENY를 돌려주면 아이템이 줄지 않고, 클릭 결과는 FAIL이 되며, 플레이어의 클라이언트를 다시 맞춥니다.
  • PLACE와 PLACED는 플레이어가 놓을 때만 불립니다. 디스펜서처럼 플레이어 없이 놓는 경우에는 불리지 않습니다.
// 높은 우선순위: 다른 모드보다 보호 판정을 먼저 합니다.
mod.events().register(BlockEvents.BREAK, EventPriority.HIGH, (world, pos, block, player) -> {
if (block.equals(WARDED_BLOCK) && !player.mainHandItem().equals(SPARK)) {
return EventResult.DENY;
}
return EventResult.PASS;
});
mod.events().register(BlockEvents.PLACE, (world, pos, block, player) ->
onWard(world, pos) ? EventResult.DENY : EventResult.PASS);
mod.events().register(BlockEvents.PLACED, (world, pos, block, player) -> { /* … */ });

엔티티​

이벤트리스너시점
EntityEvents.JOINED_WORLD(entity, world) -> …플레이어·몹·아이템·투사체 등 모든 종류의 엔티티를 서버 월드가 추적하기 시작할 때(생성, 청크와 함께 로드, 다른 차원에서 이동)
EntityEvents.ALLOW_DAMAGE(entity, damage) -> boolean피해 직전. 하나라도 false면 취소
EntityEvents.MODIFY_DAMAGE(entity, damage) -> float취소되지 않은 피해의 양 수정
EntityEvents.DIED(entity, cause) -> …생명체가 죽을 때(서버 플레이어 포함)

모두 서버 스레드에서 호출됩니다. 자세한 내용은 피해와 상태 효과를 참고하세요.

  • JOINED_WORLD는 취소할 수 없습니다. 리스너 안에서 엔티티를 없애거나 옮기지 말고, 그런 변경은 WorldView.schedule로 미루세요.
  • 엔티티가 많은 청크는 자주 로드되므로 JOINED_WORLD 리스너는 가볍게 유지하세요.

월드​

차원마다, 서버를 시작할 때마다 LOADED → READY → SAVING(저장할 때마다) → UNLOADED 순서로 불립니다.

이벤트리스너시점
WorldEvents.LOADEDworld -> …서버가 모든 차원을 만든 뒤 차원마다 한 번. 스폰 청크를 준비하기 전, 첫 틱 전이라 아직 저장되거나 틱하지 않습니다
WorldEvents.READYworld -> …차원이 로드되거나 생성된 뒤 처음 틱할 때. 서버를 시작할 때마다 차원당 1회. storage를 읽기 좋은 시점
WorldEvents.SAVINGworld -> …차원을 저장하기 직전(자동 저장, /save-all, 정지). 청크와 레벨 데이터를 쓰기 전이므로 이때 바꾼 내용은 함께 저장됩니다
WorldEvents.UNLOADEDworld -> …정지하는 서버가 저장을 마치고 차원을 닫기 직전. 차원을 읽을 수 있는 마지막 시점이며, 이때 바꾼 내용은 저장되지 않습니다
WorldEvents.CHUNK_LOADED(world, chunkX, chunkZ) -> …서버에서 청크 로드가 끝난 뒤(디스크에서 읽었거나 새로 생성한 경우)
WorldEvents.CHUNK_UNLOADED(world, chunkX, chunkZ) -> …청크를 저장한 뒤 내리기 직전

모두 서버 스레드에서 호출됩니다. 청크 이벤트는 자주 호출되므로 리스너를 가볍게 유지하세요.

클라이언트​

클라이언트 이벤트는 클라이언트의 클라이언트 스레드에서만 호출됩니다.

이벤트리스너시점
ClientEvents.TICKview -> … (ClientView)클라이언트 틱이 끝날 때(초당 20회, 타이틀 화면에서도 호출)
ClientEvents.FIELD_OF_VIEWfov -> float매 프레임, 게임이 시야각(달리기, 물속)을 계산한 뒤
ClientEvents.JOINED_WORLDview -> …월드(싱글 플레이 또는 서버)에 들어가 플레이어와 월드가 생겼을 때. 연결마다 1회
ClientEvents.LEFT_WORLDview -> …월드를 떠나기 직전. 플레이어와 월드가 아직 남아 있음
ClientEvents.ITEM_TOOLTIP(stack, lines, advanced) -> …게임이 아이템 툴팁을 만들 때마다, 게임의 줄 뒤에. 아이템 참고

ClientView는 클라이언트 기본에서 설명합니다.

Buy me a coffee