BetterModel 기반 요리 가구 시스템 — Paper 1.21.4 / Java 21
가구를 설치하고 재료를 순서대로 넣으면, 그 조합에 맞는 요리 모델이 가구 위에 나타난다. 레시피가 맞으면 조리가 진행되고 결과물이 나온다.
가구 4종과 요리 모델 32종의 .bbmodel 을 models/ 에 함께 넣어뒀다.
| 버전 | |
|---|---|
| 서버 | Paper 1.21.4 |
| Java | 21 |
| BetterModel | 2.x (2.2.0 에서 개발) |
BetterModel 은 ModelEngine 의 무료 대체제다. ItemsAdder / ModelEngine / MythicMobs 는 필요 없다.
Simmer-x.y.z.jar를plugins/에 넣는다.models/의.bbmodel36개를plugins/BetterModel/models/에 넣는다.- 서버를 켠 뒤
/bettermodel reload로 리소스팩을 다시 굽는다. - 생성된 리소스팩을 클라이언트에 적용한다.
모델을 안 넣고 켜면 시작 로그에 어떤 모델이 없는지 경고가 뜬다.
/simmer give <종류> [플레이어] 가구 아이템 지급
/simmer place <종류> [월드 x y z] [재료...] 가구 직접 설치 (콘솔/커맨드블록 가능)
/simmer remove 보고 있는 가구 철거
/simmer rotate [각도] 보고 있는 가구 회전 (생략하면 한 칸)
/simmer list 설치된 가구 목록
/simmer status 모델 스폰 상태와 좌표
/simmer models 등록된 요리 모델
/simmer recipes 등록된 레시피
/simmer reload 설정 다시 읽기
가구 종류: frypan pot fryer chopping_board
| 동작 | 조작 |
|---|---|
| 설치 | 가구 아이템 들고 블록 우클릭 (보는 방향으로 놓인다) |
| 재료 넣기 | 재료 들고 가구 우클릭 |
| 재료 빼기 | 가구 좌클릭 — 마지막에 넣은 것부터 |
| 조리 시작 | 빈손으로 가구 우클릭 |
| 완성품 수령 | 가구 우클릭 |
| 철거 | 웅크리고 가구 우클릭 |
클릭 판정은 가구 모양에 맞춘 Interaction 엔티티가 받는다 (CraftEngine 가구와 같은
방식). 예전에는 배리어 블록을 깔았는데, 그러면 납작한 도마도 한 칸을 통째로 먹고
모양과 판정이 따로 놀았다.
| 가구 | 판정 (가로 × 높이, 칸) |
|---|---|
| 프라이팬 | 14 × 5 |
| 냄비 | 14 × 11 |
| 튀김기 | 15 × 11 |
| 도마 | 15 × 4 |
- 통과된다. 가구 위로 걸어다닐 수 있고, 물이나 풀 위에도 놓인다.
- 저장되지 않는다. 청크가 내려가면 사라지고, 다시 열릴 때 만든다. 그래서 플러그인이 없는 사이에 지워진 가구가 안 보이는 벽으로 남는 일이 없다.
- 가구가 선 칸에는 블록을 못 놓는다. 모델이 파묻히면 눌리지도 않기 때문이다.
- 예전 판이 깔아 둔 배리어는 가구를 불러올 때 알아서 치운다.
맞는 레시피가 없어도 조리는 진행되고 failure.result 가 나온다. 다 될 때까지는
성공인지 실패인지 알려주지 않는다. 가구는 어떤 경우에도 재료가 낀 채로 잠기지 않는다 —
좌클릭으로 빼거나, 조리해서 실패시키거나, 철거하면 전부 돌려받는다.
완성품은 기본적으로 가구가 들고 있다가 우클릭할 때 준다(result-mode: STATION).
조리시킨 사람이 자리를 비워도 바닥에서 소멸하지 않는다. 그동안 요리 모델은 가구 위에 그대로 남는다.
권한: simmer.use (기본 전체) / simmer.admin (기본 OP)
steak:
meg: cook_meat1 # bbmodel 파일명 (확장자 제외). 요리 하나당 파일 하나
bone: meat1 # 표시할 본. 리스트로 주면 랜덤
state: cook # 재생할 애니메이션
furniture: frypan # frypan / pot / fryer / chopping_board
stage:
1: BEEF # 재료 순서. 최대 7단계정확히 맞는 모델이 없으면 같은 가구 + 같은 단계 수인 모델 중 하나가 랜덤으로 뜬다.
grilled_steak:
furniture: frypan
ingredients:
- BEEF
ordered: true # 없으면 config 의 default-recipe-ordered
cook-ticks: 120
result:
material: COOKED_BEEF
amount: 1
name: '&f구운 스테이크'
lore:
- '&7팬에서 알맞게 구워졌다.'ordered: false 면 넣은 순서와 상관없이 재료 구성만 맞으면 된다.
재료는 두 가지로 적을 수 있다.
ingredients:
- BEEF # 바닐라
- wild:venison_raw # CraftEngine 아이템재질만으로는 안 된다. 커스텀 아이템은 대부분 흔한 바닐라 재질을 바탕으로
만들어진다. 늑대 가죽·사슴 가죽·거친 가죽이 전부 leather 이고, 생 사슴고기는
rabbit 이다. 재질만 보면 무엇을 넣든 같은 재료가 된다.
판별은 두 갈래다.
| 자리 | 규칙 |
|---|---|
커스텀 (namespace:id) |
CraftEngine id 가 정확히 같아야 한다 |
바닐라 (BEEF) |
재질이 같고 커스텀 아이템이 아니어야 한다 |
두 번째 규칙이 중요하다. 이게 없으면 LEATHER 자리에 늑대 가죽이 들어간다.
models.yml 의 stage 도 같은 형식을 쓴다.
result:
craftengine-item: "dish:hunter_stew"
material: RABBIT_STEW # CraftEngine 이 없을 때 쓰는 대체품
amount: 1그림·이름·설명은 그 아이템 정의에 있으므로 여기 또 적지 않아도 된다.
이 아이템은 조리가 끝나는 순간에 만든다. 설정을 읽는 시점에는 만들 수 없다 — CraftEngine 이 자기 팩을 다 읽는 시점이 이 플러그인이 켜지는 시점보다 늦다.
max-stages, default-cook-ticks, max-stations-per-player, 메시지 등.
view-range: 48 # 모델 스폰 패킷을 보낼 거리 (블록). 8 ~ 128
placement-rotation: SNAP_4 # NONE / SNAP_4 / SNAP_8 / FREE
face-player: true # 가구 앞면이 설치한 사람을 보게 (상자·화로와 같은 규칙)
result-mode: STATION # STATION(가구가 들고 있음) / INVENTORY / DROP
allow-take-back: true # 좌클릭으로 재료 도로 빼기
default-recipe-ordered: true # 재료 순서를 따질지. 레시피별 ordered 로 덮어쓴다
failure: # 레시피에 없는 조합으로 조리했을 때
cook-ticks: 100
result:
material: CHARCOAL
name: '&8실패한 요리'
animations: # 언제 무엇을 재생할지. bbmodel 안의 이름이다
add: add # 가구 — 재료를 받을 때
cook: cook # 가구 — 조리 중 계속
done: done # 가구 — 다 됐을 때 한 번
dish-drop: drop # 요리 — 떨어져 자리잡는 동작
dish-cook: cook # 요리 — 조리 중 계속옛 키 cook-animation · add-animation 도 계속 읽는다. 플러그인만 갈아끼운 서버에서
설정이 조용히 무시되면 "애니메이션이 갑자기 안 나온다" 로 나타나기 때문이다.
설치 방향 — 모델 회전은 스폰 위치의 yaw 를 그대로 따라간다. BetterModel 트래커가 매 틱
자기 위치에서 yaw 를 읽으므로, 다시 띄우지 않고 yaw 만 갈아끼우면 방향이 바뀐다.
→ Station#modelLocation, StationManager#rotate
메시지 키가 사용자 config.yml 에 없으면 jar 안 기본값을 쓴다. 플러그인만 갈아끼워도
새 메시지가 빈칸으로 나가지 않는다.
recipes.yml 을 손으로 고치지 않고 게임 안에서 만들고 고칠 수 있다.
/simmer edit 새 레시피를 만든다
/simmer edit <이름> 이미 있는 레시피를 불러온다 (탭 완성)
simmer.admin 권한이 필요하다 (기본값 op).
┌─ 재료 7단계 ─┐ │ 결과물
│ 0 1 2 3 4 5 6│ │
└──────────────┘ │
가구 시간 순서 이름 │
안내 저장 취소 삭제
왼쪽부터 넣을 순서대로 놓는다. 가운데를 비우면 저장되지 않는다 — 재료는 단계라서 중간이 비면 뜻이 흐려진다.
재료·결과 칸에는 아이템을 실제로 넣는다. 창을 닫으면 전부 돌려주므로 소모되지 않는다. CraftEngine 아이템을 놓으면 그 id 로, 바닐라면 재질로 저장된다.
| 버튼 | 하는 일 |
|---|---|
| 가구 | 프라이팬 · 냄비 · 튀김기 · 도마 |
| 시간 | 좌클릭 +1초 / 우클릭 −1초 / 쉬프트 ±5초 |
| 순서 | 순서를 지켜야 하는지 토글 |
| 이름 | 채팅으로 입력. 영소문자·숫자·밑줄만 |
| 저장 | recipes.yml 에 쓰고 다시 읽는다 |
| 삭제 | 쉬프트를 눌러야 지워진다 |
주의. 저장하면
recipes.yml의 주석이 사라진다. Bukkit 의 YAML 저장이 주석을 보존하지 않는다. 저장 직전 원본을recipes.yml.bak-editor로 복사해 두므로 필요하면 거기서 되살리면 된다.
models/ 안에 가구 4개 + 요리 32개, 총 36개의 .bbmodel 이 있다.
전부 512×512 아틀라스 하나를 공유한다 — uv 한 칸에 32px 이라 바닐라 블록의 두 배다.
그림은 여전히 16칸 좌표로 그리고, 생성기가 32px 로 옮기면서 32px 에서만 보이는 마무리를 얹는다. 결(잡티) · 가장자리 그늘 · 부드러운 빛 얼룩 · 한 픽셀 굵기의 선 (칼자국 · 나뭇결 · 면발 · 고기 마블링)이 그것이다. 아틀라스만 키우고 그림이 그대로면 커진 값을 하나도 못 쓴다.
가구마다 몸통 말고 따로 움직이는 본이 있다. 예전에는 전부 본 하나여서 흔드는 것 말고는 할 수 있는 게 없었다.
| 가구 | 본 | 하는 일 |
|---|---|---|
| 프라이팬 | pan handle oil |
자루가 한 박자 늦게 따라오고, 기름막이 반대로 쏠린다 |
| 냄비 | pot lid soup |
뚜껑이 여닫힌다. 김에 밀려 달칵거린다 |
| 튀김기 | fryer oil basket |
바스켓이 기름에 잠겼다 건져진다 |
| 도마 | board knife |
칼이 썬다. 세 번 썰고 한 박자 쉰다 |
애니메이션은 넷씩이다 — idle · add(재료 받기) · cook(조리 중) · done(완성).
요리 모델에는 drop 이 하나 더 있다: 위에서 떨어져 두 번 튕기고, 닿는 순간 눌렸다가
돌아온다. 세로로만 내리면 물건이 아니라 승강기로 보인다.
뚜껑은 열린 자세로 만들어 둔다. 그게 아무것도 재생하지 않을 때의 모습이라야 안이 보인다. 닫힌 자세로 만들면 애니메이션이 꺼지는 모든 순간(리로드·시야 밖)에 뚜껑이 덮여서 요리가 안 보인다. 같은 이유로 튀김기 바스켓도 건져 올린 자세다.
크게 젖혀 두는 자세는 본 회전으로 준다. 큐브 회전은 ±45°(22.5° 단위)까지만 바닐라 모델로 표현돼서, 뚜껑을 104° 돌리면 큐브마다 파트가 하나씩 더 생긴다. 본 회전은 디스플레이 엔티티가 통째로 처리하므로 각도 제한도 파트 증가도 없다.
| 파일 | 본 |
|---|---|
frypan pot fryer chopping_board |
가구 (본 3~4개) |
cook_meat1 cook_meat2 cook_meat3 |
고기 |
cook_fish1 cook_fish2 cook_octopus cook_kelp |
물고기 |
cook_carrot1·2 cook_beetroot1·2 cook_potato1·2 cook_green_onion cook_tomato |
채소 |
cook_glow_berries1·2 cook_sweet_berries1·2 cook_apple cook_pineapple cook_melon1·2 |
과일 |
cook_bread1·2 cook_butter cook_cheese |
유제품 |
cook_egg cook_chilli_sauce cook_oil cook_soy cook_ramen |
기타 |
요리 파일은 본 하나만 들고 있고, 파일명은 cook_<본이름> 이다.
자세한 건 models/MODELS.md.
BetterModel 은 모델 파트 하나당 디스플레이 엔티티 하나를 만들고, 그건 파일 단위로 뜬다. 파일 안의 어떤 본을 숨겨도 엔티티는 이미 만들어진 뒤다.
처음엔 요리를 카테고리 6파일로 묶었는데, 그러면 냄비에 잼 하나 올리려고 cook_fruit
33파트가 통째로 생겼다. 실제로 보이는 건 그중 한 본뿐이라 나머지 32파트는 전부 낭비였다.
그래서 요리 하나당 파일 하나로 쪼갰다.
| 쪼개기 전 | 쪼갠 뒤 | |
|---|---|---|
| 가구 1개가 띄우는 파트 | 가구 1 |
가구 1 |
| 최악 (냄비+과일) | 8 + 33 = 41 | 8 + 4 = 12 |
| 평균 | 약 22 | 약 8 |
요리 32개 파트 합계는 99로 그대로다(같은 지오메트리다). 줄어든 건 한 번에 뜨는 양이다.
bbmodel 에서는 큐브를 아무 각도로나 돌릴 수 있지만, 바닐라 모델 JSON 은 한 축 ±45° (22.5° 단위)만 표현한다. 그래서 그 범위를 벗어난 회전을 가진 큐브는 각각 별도 파트로 분리돼 나간다. 서버에서 구운 팩을 실측한 결과:
같은 본에서 같은 각도로 돌아간 큐브들은 한 파트로 묶인다. 그래서 팔각 테두리 위에 띠를 한 켜 더 얹어도 파트는 안 늘어난다 — 각도가 이미 있는 여덟 가지 그대로이기 때문이다. 파트 수는 결국 (본 × 서로 다른 회전) 가짓수다.
| 모델 | 파트 | 내역 |
|---|---|---|
frypan |
10 | 팔각 8 + handle + oil |
pot |
9 | 팔각 8(뚜껑·국물 포함) + 나머지 |
fryer |
4 | fryer, oil, basket x2 |
chopping_board |
2 | board, knife — 회전 큐브가 없다 |
cook_octopus |
9 | 다리 8개가 전부 회전 |
cook_glow_berries1 cook_sweet_berries1 |
8 / 8 | |
| 나머지 요리 29개 | 1~5 (평균 2.6) | |
| 합계 | 124 | 가구 25 + 요리 99 |
부품을 넷씩 떼어 내고 애니메이션을 넷씩 넣었는데 가구 파트는 21 → 25 로 넷만 늘었다. 움직이는 본을 새로 만든 값이고, 그 위에 얹은 장식(테두리 말이 · 다리 · 칼날 겹)은 기존 각도를 다시 쓴 덕에 공짜였다.
예전 실측은 서버가 구운 plugins/BetterModel/build.zip 의 modern_item/*.json 개수로
셌다. 위 표의 새 값은 그 규칙으로 계산한 것이고 아직 구운 팩으로 확인하지 않았다.
BetterModel 은 텍스처가 붙은 면이 하나라도 있으면 그 본을 굽는다.
visibility:false 는 이걸 막지 못한다. 실측으로 확인했다:
- 기본 제공
qmob_blobfish의hitbox는 면에texture키가 없어서 팩에 안 들어간다 - 같은 본을
texture: 0만 넣어 만들었더니frypan_hitbox_1.json이 팩에 생겼다 → 안 보이는 본이 디스플레이 엔티티를 하나씩 잡아먹고 있었다
그래서 생성기의 cube(..., textured=False) 로 hitbox 면에서 texture 키를 뺐다.
가구 4종에서 파트 4개가 사라졌다 (124 → 120).
요리 본은 파일에서 전부 '보임' 상태다. 플러그인이 스폰 직후 전부 끈 뒤
필요한 본 하나만 켠다. → ModelBridge#showOnly
cd models
python addcook_models.py # out/ 에 36개 bbmodel + 아틀라스 재생성팔레트·타일·지오메트리가 전부 데이터로 분리돼 있다. 요리 추가는
@food(카테고리, 이름) 데코레이터로 함수 하나 쓰면 된다. PIL 이 필요하다.
BetterModel 2.x 는 Maven Central 에 없다 (센트럴은 1.15.2 까지고 2.x 는 API 패키지가 다르다). 그래서 서버에 설치된 jar 에 직접 컴파일한다.
./gradlew setupBetterModel -PserverDir=<서버경로> # BetterModel jar 를 libs/ 로 복사
./gradlew build # build/libs/Simmer-x.y.z.jar
./gradlew deployToServer -PserverDir=<서버경로> # 서버 plugins/ 로 배포serverDir 을 생략하면 C:/MinecraftServer/QWER/servers/fgh 를 쓴다.
libs/ 에 이미 BetterModel*.jar 가 있거나 -PbetterModelJar=<경로> 를 주면 그걸 쓴다.
Paper 1.21.4 실서버에서 5회 기동해 확인했다. 예외 0건, TPS 20.0.
| 항목 | 결과 |
|---|---|
| 플러그인 로드 / 설정 파싱 | 예외 0건, 모델 32 / 레시피 12 |
| 리소스팩 굽기 | 36개 모델 120파트 |
가구 설치 (/simmer place) |
4종 전부 모델 스폰 성공 |
| 모델 좌표 | 블록 중앙 정확히 (100.50 / 70.00 / 100.50) |
| 재시작 후 복원 | stations.yml 4개 복원 + 모델 재스폰 |
| 재료 조합 → 본 선택 | BEEF→meat1, EGG→egg, SWEET_BERRIES>SUGAR→sweet_berries2, DRIED_KELP>WHEAT>EGG→ramen |
| 폴백 규칙 | DIAMOND(모델 없음) → 같은 단계 수 중 랜덤(meat2) |
/bettermodel reload 대응 |
트래커 재스폰 확인 |
아직 확인 못 한 것 — 전부 클라이언트가 있어야 한다:
- 모델이 화면에 어떻게 보이는지.
togglePart호출은 예외 없이 통과하지만 선택한 본 하나만 실제로 보이는지는 눈으로 봐야 안다 - 조리 타이머 완주 → 결과물 지급, 액션바 진행 표시
4. 가구를 설치해도 모델이 아무에게도 안 보이던 문제 — DummyTracker 는 엔티티에
안 붙어서 BetterModel 이 시야를 관리해주지 않는다. ModelRenderer#create 는 서버 쪽에
모델을 만들 뿐이고, 스폰 패킷은 DummyTracker#spawn(player) 를 부른 사람에게만 간다.
BetterModel 자체 명령의 바이트코드도 create 바로 다음 줄에서 spawn 을 부른다.
/simmer status 는 "모델 스폰 성공" 으로 나오는데 화면에는 아무것도 없는 이유가 이거였다.
1초마다 도는 갱신 태스크로 view-range 안에 들어온 사람에게 띄우고 나간 사람에게서 지운다.
접속 종료·리스폰·월드 이동은 클라이언트가 엔티티를 통째로 버리는 순간이라, 서버 쪽
'보고 있다' 기록도 같이 지워야 다시 띄운다. → ModelBridge#showTo, StationManager#syncViewers
/simmer status 에 보는 사람 수를 추가했다. 0 이면 이 버그다.
5. 재료 7개를 채우면 완성도 실패도 안 되고 가구가 잠기던 문제 — 레시피는 재료
순서까지 맞아야 하는데, 안 맞으면 조리를 시작조차 안 하고 거부했다. 재료를 뺄 방법은
철거뿐이라 그대로 잠겼다. 이제 맞는 레시피가 없으면 실패로 조리가 진행되고
failure.result 가 나온다. 재료를 넣을 때마다 지금 조합이 완성 가능한지, 이미 글렀는지도
알려준다 (hasRecipeStartingWith). 크리에이티브에서는 재료를 소모하지 않는다.
1. 가구가 모델 없이 뜨던 문제 — onEnable 에서 바로 복원했는데,
BetterModel 은 모델을 비동기로 읽는다. 실측하면 BetterModel enable 후 약 10초 뒤에야
모델 조회가 된다. PluginEndReloadEvent 구독 + 즉시 확인 + 60초 타임아웃 3중으로
바꿨다. → SimmerPlugin#awaitBetterModel
2. 모델이 반 블록씩 어긋나던 문제 — Block#getLocation() 은 블록 모서리(정수)
좌표를 준다. 그대로 모델을 띄우면 네 블록이 만나는 꼭짓점에 걸친다. x/z 에 0.5 를
더해야 한다. → Station#modelLocation
3. hitbox 본이 디스플레이를 낭비하던 문제 — 위 "판정용 본은 면에 텍스처를..." 참고.
MIT