バニラマイクラ鯖で外部とデータをやり取りしてみる

**Minecraft Command Advent Calendar 2024 14日目の記事です**

https://qiita.com/advent-calendar/2024/mccmd

# はじめに
バニラ鯖でも外部通信、したくないですか？
今回は外部と通信する方法をいくつかまとめたので解説していきます

:::note warn
基本的に外部ツールが必須となっています
:::


# 早見表
| 方法 | 双方向性 | リアルタイム性 | シングルプレイ | 外部ツール | 転送可能データ量 | 備考 |
| :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| RCON | 〇 | 〇 | ✖ | 必要 | 多い | ログがかなり荒れる |
| チャットログ | △ | ◎ | ✖ | 必要 | 多い | ゲーム内チャットに表示される |
| 名前付きモブ| △ | ◎ | ✖ | 必要 | 多い | ログが若干荒れる |
| nbtファイル | △ | △ | 〇 | 必要 | とても多い | |
| スキンデータ | ✖ | ✖ | 〇 | 不要 | とても少ない | |

# 解説

## RCON
バニラ鯖で通信すると言えばこれ。

### 使い方
`server.properties`で`enable-rcon=true`に設定、サーバーアドレスと`rcon.password`と`rcon.port`に指定した値を接続ツール等に入れることで接続できる

コマンドが実行できるため基本的に何でもできる
ループ処理でリアルタイムに値を取得すると言ったことが可能
ただしtickを超える頻度でのアクセスを行った際の挙動は不明

### 実装例
Pythonのmcrconを使用

#### コード
```python
from mcrcon import MCRcon

with MCRcon("127.0.0.1", "test") as mcr:
    resp = mcr.command("/scoreboard players get #data data")
    print(resp)
```

#### レスポンス
```
#data has 1 [data]
```


## チャットログ

### 使い方
sayコマンド等のチャットログによる出力を利用

### 実装例

#### コード

送信用function
```nim
# データ送信用のモブを召喚
  summon minecraft:marker 0 0 0 {Tags:["message"],CustomName:'"#message"',NoAI:1b,Silent:1b}

# マクロ呼び出し
  execute positioned 0 0 0 as @n[type=marker,tag=message] run function message:say/macro with storage message

# kill
  execute positioned 0 0 0 run kill @n[type=minecraft:marker,tag=message,distance=..0.1]
```

マクロ
```nim
$say $(data)
```

ログ読み取り(python)

```python
import os
import re

current_line = ""
while True:
  # ログのリアルタイム読み取り
  with open('logs/latest.log', 'rb') as f:
      try:  # catch OSError in case of a one line file
          f.seek(-2, os.SEEK_END)
          while f.read(1) != b'\n':
              f.seek(-2, os.SEEK_CUR)
      except OSError:
          f.seek(0)
      last_line = f.readline().decode().rstrip("\n")
  # 最終行が更新されていたら
  if last_line != current_line:
    # 正規表現で名前付きMOBの死亡メッセージを抽出
    match_line = re.search(r"(?<=\[\d\d:\d\d:\d\d] \[Server thread/INFO\]: \[Not Secure\] \[\#message\] ).*(?=)", last_line)
    if match_line:
      print(match_line.group(0))
  # 現在行を更新
  current_line = last_line
```

#### 実行例
```nim
/data modify storage minecraft:message data set value {"test":[1,2,3,4]}
```
```nim
/function message:say
```


#### レスポンス
```
{test:[1,2,3,4]}
```



## 名前付きモブ

### 使い方
名前付きのモブをkillした際にログとして出力される挙動を利用
それなりに長めのデータも出力できる


### 実装例

#### コード

送信用function
※0 0 0に看板を設置します
```nim
# データ格納用のモブを召喚
  summon minecraft:allay 0 0 0 {Tags:["message"],CustomName:'""',NoAI:1b,Silent:1b}

# 一旦看板へデータを書き込む
  setblock 0 0 0 air
  setblock 0 0 0 oak_sign{front_text:{messages:['{"storage": "message","nbt":"data"}','""','""','""']}}

# モブのCustomNameへデータを代入
  execute positioned 0 0 0 run data modify entity @n[type=allay,tag=message,distance=..0.1] CustomName set from block 0 0 0 front_text.messages[0]
  # データの2重出力対策
  execute positioned 0 0 0 run damage @n[type=minecraft:allay,tag=message,distance=..0.1] 9999 message:nolog

# 看板を撤去
  setblock 0 0 0 air
```

ダメージタイプ
```json
{
  "message_id": " ",
  "exhaustion": 0,
  "scaling": "never"
}
```

ログの読み取り(python)

```python
import os
import re

current_line = ""
while True:
  # ログのリアルタイム読み取り
  with open('logs/latest.log', 'rb') as f:
      try:  # catch OSError in case of a one line file
          f.seek(-2, os.SEEK_END)
          while f.read(1) != b'\n':
              f.seek(-2, os.SEEK_CUR)
      except OSError:
          f.seek(0)
      last_line = f.readline().decode().rstrip("\n")
  # 最終行が更新されていたら
  if last_line != current_line:
    # 正規表現で名前付きMOBの死亡メッセージを抽出
    match_line = re.search(r"(?<=\[\d\d:\d\d:\d\d] \[Server thread/INFO\]: Named entity ...\[').*(?='/\d*. l\='ServerLevel\[.*\]', x\=\d*.\d*, y\=\d*.\d*, z\=\d*.\d*\] died: death.attack. )", last_line)
    if match_line:
      print(match_line.group(0))
  # 現在行を更新
  current_line = last_line
```

#### 実行例
```nim
/data modify storage minecraft:message data set value {"test":[1,2,3,4]}
```
```nim
/function message:send
```


#### レスポンス
```
{test:[1,2,3,4]}
```


## NBTファイル

### 使い方
ワールドデータ内にある`scoreboard.dat`や`command_storage_*.dat`を外部から読み込んで値を取得
ワールドを閉じるか`save-all`を実行するとファイルが更新される
pythonであれば[NBT](https://github.com/twoolie/NBT)のようなライブラリで読み書きができる。

## スキンデータ
#### 使い方
プレイヤーのスキンデータを利用したデータの取得

https://qiita.com/ChenCMD/items/9d4f733da11d8e4f7dc7

# まとめ
現状、RCONとログの読み取りによる送受信が最適解と思われる