NimBlock 로그인

플러그인 문법

specVersion 1

NimBlock 플러그인은 코드가 아니에요. 규칙을 기술하는 JSON 파일이고, 모두를 위해 한 번만 작성된 Paper 엔진이 이걸 읽고 실행해요. 이 페이지는 여기에 정확히 무엇을 쓸 수 있는지 알려주고, 그 외에는 아무것도 쓸 수 없어요.

규칙, 오직 규칙 하나

전체 문법은 한 문장에 담겨요: 이런 일이 일어나면, 이런 조건을 만족하면, 그때 이렇게 한다. 플러그인은 규칙들의 목록이고, 규칙 하나는 트리거 하나에 조건 0~20개, 액션 1~50개로 이루어져요.

어디에서도 Java 코드가 생성되지 않고, 컴파일도 없어요: 서버는 스펙을 읽는 엔진을 불러올 뿐이에요. 이 특성이 나중에 중요한 이유는, Minecraft의 새 버전이 나와도 그 대가는 이 엔진 하나에서만 치르면 되고, 이걸로 만든 플러그인 하나하나에서 치르지 않아도 되기 때문이에요.

스펙의 형태

{
    "specVersion": 1,
    "name": "환영",
    "rules": [
        {
            "id": "환영",
            "name": "환영 메시지",
            "match": "all",
            "trigger": { "type": "player.join" },
            "conditions": [
                { "type": "player.has_permission", "params": { "permission": "nimblock.vip" } }
            ],
            "actions": [
                { "type": "player.send_message", "params": { "message": "&6환영합니다, {player}!" } }
            ]
        }
    ]
}

matchall(기본값) 또는 any이고, 조건에는 "not": true를 붙일 수 있어요. enabled: false는 규칙을 파일에는 남겨두되 작동시키지는 않아요.

EBNF로 본 문법

아래 터미널 목록은 카탈로그에서 자동으로 생성돼요: 엔진이 실제로 실행할 수 있는 식별자 그 자체이지, 사람이 손으로 옮겨 적은 게 아니에요.

<plugin>       ::= "{" "specVersion" ":" 1 ","
                       [ "id" ":" <slug64> "," ]
                       "name" ":" <label> ","
                       [ "description" ":" <text500> "," ]
                       "rules" ":" "[" <rule> { "," <rule> } "]" "}"      (* 1..200 *)

<rule>         ::= "{" [ "id" ":" <slug32> "," ]
                       "name" ":" <label> ","
                       [ "enabled" ":" <boolean> "," ]                   (* = true *)
                       [ "match" ":" ( "all" | "any" ) "," ]              (* = "all" *)
                       "trigger" ":" <trigger> ","
                       [ "conditions" ":" "[" [ <condition>
                             { "," <condition> } ] "]" "," ]              (* 0..20 *)
                       "actions" ":" "[" <action>
                             { "," <action> } "]" "}"                     (* 1..50 *)

<trigger>      ::= "{" "type" ":" <trigger-id>   [ "," "params" ":" <params> ] "}"
<condition>    ::= "{" "type" ":" <condition-id> [ "," "not" ":" <boolean> ]
                                                 [ "," "params" ":" <params> ] "}"
<action>       ::= "{" "type" ":" <action-id>    [ "," "params" ":" <params> ] "}"

<params>       ::= "{" [ <param-name> ":" <param-value>
                       { "," <param-name> ":" <param-value> } ] "}"
<param-value>  ::= <template> | <identifier> | <integer> | <number>
                 | <boolean> | <choice>

<template>     ::= { <literal> | <variable> | <colour> }                (* 0..2000 *)
<literal>      ::= /[^{}]/ | "{{" | "}}"
<variable>     ::= "{" /[a-zA-Z][a-zA-Z0-9_]*/ "}"
<colour>       ::= "&" ( "0".."9" | "a".."f" | "k".."o" | "r" )

<identifier>   ::= /[A-Za-z][A-Za-z0-9_:.\/-]{0,63}/
<slug32>       ::= /[A-Za-z0-9_-]{1,32}/
<slug64>       ::= /.{0,64}/
<label>        ::= /.{1,64}/
<text500>      ::= /.{0,500}/

<trigger-id>   ::= "player.join"
                 | "player.quit"
                 | "player.chat"
                 | "player.death"
                 | "player.respawn"
                 | "block.break"
                 | "block.place"
                 | "command"
                 | "schedule.repeat"
<condition-id> ::= "player.has_permission"
                 | "player.is_op"
                 | "player.in_world"
                 | "player.health_below"
                 | "text.contains"
                 | "text.equals"
                 | "number.compare"
                 | "chance"
<action-id>    ::= "player.send_message"
                 | "player.send_actionbar"
                 | "player.send_title"
                 | "player.play_sound"
                 | "player.give_item"
                 | "player.give_effect"
                 | "player.heal"
                 | "player.teleport"
                 | "player.kick"
                 | "broadcast"
                 | "server.run_command"
                 | "cancel_event"
                 | "log"
                 | "delay"

이 생성 규칙만으로는 알 수 없는 게 두 가지 있어요. 형식이 아니라 정합성에 관한 문제이기 때문이에요. 플레이어를 요구하는 조건이나 액션은 플레이어를 제공하는 트리거 아래에서만 쓸 수 있어요: "플레이어를 치료한다"는 "N초마다"라는 트리거 아래에서는 의미가 없어요. 그리고 cancel_event는 취소 가능한 트리거 아래에서만 쓸 수 있어요. 둘 다 실행 시점이 아니라 작성 시점에 거부돼요.

템플릿

text 타입의 파라미터는 템플릿이에요: {player}는 트리거가 넘겨준 값으로 바뀌고, {{는 중괄호 문자 자체를 나타내며, &a &l 같은 색상 코드는 실제 색으로 변환돼요.

트리거가 알지 못하는 변수는 오류이지, 텍스트가 아니에요. 트리거마다 무엇을 제공하는지 선언하고 있어요(아래 목록 참고): 그 목록 밖에는 읽을 것이 없고, 오타는 플레이어 채팅창에 그대로 나타나는 대신 작성 시점에 거부돼요. 트리거 자체의 파라미터는 템플릿이 아니에요: 치환할 대상이 생기기도 전인 서버 시작 시점에 읽히기 때문이에요.

Minecraft 이름은 무엇을 가리키느냐에 따라 두 가지 표기법을 써요. 아이템은 두 표기 중 아무거나 써도 돼요(DIAMOND_SWORD 또는 diamond_sword). 사운드이펙트는 Bukkit 상수가 아니라 Minecraft 키로 표기해요(entity.player.levelup, speed): 이 키는 그대로 클라이언트로 전달되고, 버전이 바뀌어도 그대로 남아요.

문법이 허용하지 않는 것

이 문법은 닫혀 있어요. 이 사실부터 먼저 말해둘게요. 카탈로그에 있는 것만 가능하고, 나머지는 불가능하며 앞으로도 그럴 거예요: 임의의 Java 코드를 실행할 방법은 없어요. 그 덕분에 스튜디오는 즉시 반영되고(컴파일할 게 없으니까요), 스펙은 어디서 왔든 설치해도 안전해요.

조건은 중첩되지 않아요: 이건 버전 1에서 의도적으로 둔 한계로, 불리언 트리는 블록으로 표현하기 어렵고, all / any / not만으로 거의 항상 충분해요. 유일한 탈출구는 "콘솔 권한으로 명령어 실행" 액션인데, 이건 그 서버에서 콘솔이 가진 권한을 딱 그만큼만 가져요.

트리거

규칙을 시작시키는 것, 그리고 각 트리거가 템플릿에 제공하는 변수예요.

player.join 플레이어가 접속했을 때

플레이어가 서버에 접속하는 순간이에요. 월드를 다 불러온 뒤에 일어나요.

제공: {player} 접속하는 플레이어, {world} 플레이어가 도착하는 월드

player.quit 플레이어가 나갔을 때

플레이어가 서버를 나가는 순간이에요. 아직 연결은 되어 있지만, 이때 보낸 메시지는 전달되지 않아요.

제공: {player} 나가는 플레이어, {world} 플레이어가 나가는 월드

player.chat 플레이어가 채팅을 입력했을 때 취소 가능

메시지가 다른 플레이어에게 전달되기 전이에요. 취소할 수 있으며, 취소하면 아무도 메시지를 보지 못해요.

제공: {player} 메시지를 작성한 플레이어, {world} 해당 플레이어의 월드, {message} 작성된 메시지

player.death 플레이어가 사망했을 때

플레이어가 사망한 순간이며, 리스폰 화면이 뜨기 전이에요.

제공: {player} 사망한 플레이어, {world} 해당 플레이어의 월드, {killer} 사망시킨 플레이어, 없으면 비어 있어요

player.respawn 플레이어가 리스폰했을 때

플레이어가 사망 후 다시 게임에 들어올 때예요.

제공: {player} 리스폰하는 플레이어, {world} 리스폰되는 월드

block.break 플레이어가 블록을 파괴했을 때 취소 가능

블록이 사라지기 전이에요. 취소할 수 있으며, 취소하면 블록이 그대로 남아요.

제공: {player} 블록을 파괴하는 플레이어, {world} 해당 플레이어의 월드, {block} 블록 종류 (DIAMOND_ORE), {x} 블록의 X 좌표, {y} 블록의 높이, {z} 블록의 Z 좌표

block.place 플레이어가 블록을 설치했을 때 취소 가능

블록이 설치되기 전이에요. 취소할 수 있으며, 취소하면 블록이 설치되지 않아요.

제공: {player} 블록을 설치하는 플레이어, {world} 해당 플레이어의 월드, {block} 설치된 블록 종류, {x} 블록의 X 좌표, {y} 블록의 높이, {z} 블록의 Z 좌표

command 플레이어가 명령어를 입력했을 때

서버에 새 명령어를 만들어요. ⚠️ 이전에 없던 이름의 명령어는 서버를 재시작해야 나타나요. Paper는 새 명령어 이름을 시작할 때만 인식하기 때문이에요. 반면 규칙의 내용은 재시작 없이 바로 반영돼요.

  • name text 명령어 이름 (슬래시 제외)
  • description text 선택 도움말에 표시될 설명
  • permission text 선택 필요한 권한, 비워두면 누구나 사용 가능

제공: {player} 명령어를 입력한 플레이어, {world} 해당 플레이어의 월드, {args} 명령어 뒤에 입력된 내용 그대로

schedule.repeat N초마다

서버가 켜져 있는 동안 규칙을 반복해요. 플레이어와 무관하게 실행되므로, 플레이어가 필요 없는 액션만 사용할 수 있어요.

  • seconds integer [1..86400] 간격 (초)

조건

걸러내는 역할을 해요. 규칙당 0~20개이고, match로 조합돼요.

player.has_permission 플레이어가 권한이 있음 플레이어 필요
  • permission text 권한
player.is_op 플레이어가 오피임 플레이어 필요
player.in_world 플레이어가 해당 월드에 있음 플레이어 필요
  • world text 월드 이름
player.health_below 플레이어의 하트가 X개 미만 플레이어 필요

체력 수치 기준이에요. 20이 하트 10개에 해당해요.

  • value number [0..1024] 체력 수치
text.contains 텍스트가 포함함
  • text text 검사할 텍스트
  • search text 찾을 내용
  • ignoreCase boolean 선택, 기본값: true 대소문자 무시
text.equals 텍스트가 같음
  • text text 검사할 텍스트
  • value text 기대하는 값
  • ignoreCase boolean 선택, 기본값: true 대소문자 무시
number.compare 두 숫자 비교

양쪽 값 모두 템플릿이에요. 코드를 작성하지 않아도 {y}를 62와 비교할 수 있어요.

  • left text 왼쪽 값
  • operator < | <= | == | != | >= | > 비교 연산자
  • right text 오른쪽 값
chance X%의 확률로
  • percent number [0..100] 확률 (%)

액션

실제로 일어나는 일이에요. 규칙당 1~50개이고, 순서대로 실행돼요.

player.send_message 플레이어에게 메시지 보내기 플레이어 필요
  • message text 메시지
player.send_actionbar 액션바 위에 메시지 표시하기 플레이어 필요
  • message text 메시지
player.send_title 큰 글씨로 타이틀 표시하기 플레이어 필요
  • title text 타이틀
  • subtitle text 선택 서브타이틀
  • fadeIn number [0..60] 선택, 기본값: 0.5 나타나는 시간 (초)
  • stay number [0..600] 선택, 기본값: 3 유지 시간 (초)
  • fadeOut number [0..60] 선택, 기본값: 0.5 사라지는 시간 (초)
player.play_sound 플레이어에게 소리 재생하기 플레이어 필요
  • sound identifier 소리, Minecraft 키 형식 (entity.player.levelup)
  • volume number [0..10] 선택, 기본값: 1 음량
  • pitch number [0.5..2] 선택, 기본값: 1 음높이
player.give_item 플레이어에게 아이템 지급하기 플레이어 필요

인벤토리에 들어가지 않는 만큼은 발밑에 떨어져요.

  • material identifier 아이템 (DIAMOND_SWORD)
  • amount integer [1..2304] 선택, 기본값: 1 개수
player.give_effect 플레이어에게 효과 적용하기 플레이어 필요
  • effect identifier 효과, Minecraft 키 형식 (night_vision)
  • seconds integer [1..86400] 선택, 기본값: 10 지속 시간 (초)
  • amplifier integer [0..255] 선택, 기본값: 0 레벨 (0부터 시작)
player.heal 플레이어 체력 회복하기 플레이어 필요

체력을 최대로 채우고, 허기도 채워줘요.

player.teleport 플레이어 텔레포트하기 플레이어 필요
  • x number [-30000000..30000000] X 좌표
  • y number [-512..1024] 높이
  • z number [-30000000..30000000] Z 좌표
  • world text 선택 월드, 비워두면 플레이어의 현재 월드
player.kick 플레이어 강퇴하기 플레이어 필요
  • reason text 표시할 사유
broadcast 서버 전체에 공지하기
  • message text 메시지
server.run_command 콘솔로 명령어 실행하기

문법의 탈출구예요: 콘솔이 할 수 있는 모든 것을 할 수 있어요. 이 서버에 대한 콘솔의 모든 권한이 걸려 있으니, 무엇을 하는지 알고 작성하세요.

  • command text 명령어 (슬래시 제외)
cancel_event 방금 일어난 일 취소하기 취소 가능한 트리거 필요

취소 가능한 트리거에만 존재해요. 메시지가 전달되지 않거나, 블록이 파괴되지 않아요.

log 서버 콘솔에 기록하기
  • message text 메시지
  • level info | warn 선택, 기본값: info 레벨
delay 대기하기

규칙의 나머지 액션 실행을 잠시 멈춰요. 대기 이후에는 아무것도 취소할 수 없으니, "취소"는 반드시 그 전에 와야 해요.

  • seconds number [0.05..3600]

공개된 파일

JSON Schema는 배포할 때마다 카탈로그에서 자동으로 생성되고, 기계가 읽을 수 있는 참고 자료 역할을 해요: 이를 따르는 에디터는 식별자를 자동으로 완성해주고 알 수 없는 파라미터는 거부해요.

  • plugin-1.schema.json : 스키마 파일이며, JSON Schema draft 2020-12 형식이에요.
  • catalogue-1.json : 카탈로그 그 자체이며, 나머지 모든 것(검증기, 스키마, 에디터 블록, Java 엔진)이 여기서 파생돼요.

카탈로그의 라벨은 소스 언어인 프랑스어로 되어 있어요. 반면 식별자는 번역되지 않아요: 엔진이 실행하는 건 바로 이 식별자들이니까요.

문법이 바뀌면 일어나는 일

트리거, 조건, 액션을 추가하는 건 아무것도 망가뜨리지 않아요: 예전에 작성된 스펙도 그대로 돌아가요. 반면 호환성이 깨지는 변경specVersion을 올리고, 그러면 엔진은 이해하지 못하는 걸 엉뚱하게 실행하는 대신 거부하면서 서버 콘솔에 그 사실을 알려줘요.

위 URL들은 이 번호를 그대로 담고 있어요: plugin-1.schema.json은 다른 문법이 생기더라도 항상 지금 이 문법을 가리켜요.