플러그인 문법
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}!" } }
]
}
]
}
match는 all(기본값) 또는 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는 새 명령어 이름을 시작할 때만 인식하기 때문이에요. 반면 규칙의 내용은 재시작 없이 바로 반영돼요.
-
nametext 명령어 이름 (슬래시 제외) -
descriptiontext 선택 도움말에 표시될 설명 -
permissiontext 선택 필요한 권한, 비워두면 누구나 사용 가능
제공:
{player} 명령어를 입력한 플레이어,
{world} 해당 플레이어의 월드,
{args} 명령어 뒤에 입력된 내용 그대로
schedule.repeat
N초마다
서버가 켜져 있는 동안 규칙을 반복해요. 플레이어와 무관하게 실행되므로, 플레이어가 필요 없는 액션만 사용할 수 있어요.
-
secondsinteger [1..86400] 간격 (초)
조건
걸러내는 역할을 해요. 규칙당 0~20개이고, match로 조합돼요.
player.has_permission
플레이어가 권한이 있음
플레이어 필요
-
permissiontext 권한
player.is_op
플레이어가 오피임
플레이어 필요
player.in_world
플레이어가 해당 월드에 있음
플레이어 필요
-
worldtext 월드 이름
player.health_below
플레이어의 하트가 X개 미만
플레이어 필요
체력 수치 기준이에요. 20이 하트 10개에 해당해요.
-
valuenumber [0..1024] 체력 수치
text.contains
텍스트가 포함함
-
texttext 검사할 텍스트 -
searchtext 찾을 내용 -
ignoreCaseboolean 선택, 기본값:true대소문자 무시
text.equals
텍스트가 같음
-
texttext 검사할 텍스트 -
valuetext 기대하는 값 -
ignoreCaseboolean 선택, 기본값:true대소문자 무시
number.compare
두 숫자 비교
양쪽 값 모두 템플릿이에요. 코드를 작성하지 않아도 {y}를 62와 비교할 수 있어요.
-
lefttext 왼쪽 값 -
operator< | <= | == | != | >= | > 비교 연산자 -
righttext 오른쪽 값
chance
X%의 확률로
-
percentnumber [0..100] 확률 (%)
액션
실제로 일어나는 일이에요. 규칙당 1~50개이고, 순서대로 실행돼요.
player.send_message
플레이어에게 메시지 보내기
플레이어 필요
-
messagetext 메시지
player.send_actionbar
액션바 위에 메시지 표시하기
플레이어 필요
-
messagetext 메시지
player.send_title
큰 글씨로 타이틀 표시하기
플레이어 필요
-
titletext 타이틀 -
subtitletext 선택 서브타이틀 -
fadeInnumber [0..60] 선택, 기본값:0.5나타나는 시간 (초) -
staynumber [0..600] 선택, 기본값:3유지 시간 (초) -
fadeOutnumber [0..60] 선택, 기본값:0.5사라지는 시간 (초)
player.play_sound
플레이어에게 소리 재생하기
플레이어 필요
-
soundidentifier 소리, Minecraft 키 형식 (entity.player.levelup) -
volumenumber [0..10] 선택, 기본값:1음량 -
pitchnumber [0.5..2] 선택, 기본값:1음높이
player.give_item
플레이어에게 아이템 지급하기
플레이어 필요
인벤토리에 들어가지 않는 만큼은 발밑에 떨어져요.
-
materialidentifier 아이템 (DIAMOND_SWORD) -
amountinteger [1..2304] 선택, 기본값:1개수
player.give_effect
플레이어에게 효과 적용하기
플레이어 필요
-
effectidentifier 효과, Minecraft 키 형식 (night_vision) -
secondsinteger [1..86400] 선택, 기본값:10지속 시간 (초) -
amplifierinteger [0..255] 선택, 기본값:0레벨 (0부터 시작)
player.heal
플레이어 체력 회복하기
플레이어 필요
체력을 최대로 채우고, 허기도 채워줘요.
player.teleport
플레이어 텔레포트하기
플레이어 필요
-
xnumber [-30000000..30000000] X 좌표 -
ynumber [-512..1024] 높이 -
znumber [-30000000..30000000] Z 좌표 -
worldtext 선택 월드, 비워두면 플레이어의 현재 월드
player.kick
플레이어 강퇴하기
플레이어 필요
-
reasontext 표시할 사유
broadcast
서버 전체에 공지하기
-
messagetext 메시지
server.run_command
콘솔로 명령어 실행하기
문법의 탈출구예요: 콘솔이 할 수 있는 모든 것을 할 수 있어요. 이 서버에 대한 콘솔의 모든 권한이 걸려 있으니, 무엇을 하는지 알고 작성하세요.
-
commandtext 명령어 (슬래시 제외)
cancel_event
방금 일어난 일 취소하기
취소 가능한 트리거 필요
취소 가능한 트리거에만 존재해요. 메시지가 전달되지 않거나, 블록이 파괴되지 않아요.
log
서버 콘솔에 기록하기
-
messagetext 메시지 -
levelinfo | warn 선택, 기본값:info레벨
delay
대기하기
규칙의 나머지 액션 실행을 잠시 멈춰요. 대기 이후에는 아무것도 취소할 수 없으니, "취소"는 반드시 그 전에 와야 해요.
-
secondsnumber [0.05..3600] 초
공개된 파일
JSON Schema는 배포할 때마다 카탈로그에서 자동으로 생성되고, 기계가 읽을 수 있는 참고 자료 역할을 해요: 이를 따르는 에디터는 식별자를 자동으로 완성해주고 알 수 없는 파라미터는 거부해요.
- plugin-1.schema.json : 스키마 파일이며, JSON Schema draft 2020-12 형식이에요.
- catalogue-1.json : 카탈로그 그 자체이며, 나머지 모든 것(검증기, 스키마, 에디터 블록, Java 엔진)이 여기서 파생돼요.
카탈로그의 라벨은 소스 언어인 프랑스어로 되어 있어요. 반면 식별자는 번역되지 않아요: 엔진이 실행하는 건 바로 이 식별자들이니까요.
문법이 바뀌면 일어나는 일
트리거, 조건, 액션을 추가하는 건 아무것도 망가뜨리지 않아요: 예전에 작성된
스펙도 그대로 돌아가요. 반면 호환성이 깨지는 변경은
specVersion을 올리고, 그러면 엔진은 이해하지 못하는 걸 엉뚱하게
실행하는 대신 거부하면서 서버 콘솔에 그 사실을 알려줘요.
위 URL들은 이 번호를 그대로 담고 있어요: plugin-1.schema.json은
다른 문법이 생기더라도 항상 지금 이 문법을 가리켜요.