mcrconを使ってサーバー起動～データパック的動作を実装しよう

何言ってるのかよく分からないって？
大丈夫、私もこの1週間位で習得したからまだ大層なことはできてない。

# 実行環境
* Windows10(64bit) (32bitでも動作はするし、Mac/Linuxでも動作するはずだけど記述とかが違う)
* Minecraft公式サーバ (プラグイン鯖でも動くはず)
* Python (3以降なら多分大丈夫)

## 今回用いる技術
* python (多少知識がある前提で説明をします)
* mcrcon
* threading

# まず、mcrconって何？
mcrconは「MinecraftのRconを使いやすくしたライブラリ」のこと。
で、Rconは「Remote console」の略で、Minecraft内のコマンドを遠隔から実行できる機能のこと。
server.propertiesの中身を見たことある人は、
```properties
broadcast-rcon-to-ops=true
enable-rcon=false
rcon.password=
rcon.port=25575
```
の記述を見たことがあるかもしれないですね。
このあたりをいい感じにするとrconの接続がいい感じにできます。

## mcrconで接続してみる
まず、公式Githubリンクをここに書いておきます。
https://github.com/Tiiffi/mcrcon
困ったときは公式ドキュメントを見ろ、という格言もあるくらいですからね。(ない)
### mcrconを使用する準備
まずmcrconをインストールするところから。
```powershell
pip install mcrcon
```
いつものpipですね。
Linuxとかの場合は公式ドキュメントからインストール手順を確認してください。私に調べるほどの余力はない。
# python×mcrconでMinecraftサーバーを動かす準備
やりたいことをやる前に、とりあえずpythonとmcrconについて軽く勉強をしてみましょう。
少し触ればだいぶやりやすくなるぞ。
## mcrconを触ってみる
今回のゴールは「pythonでmcrconの接続及び処理を行う」ですが、とりあえずmcrcon単体で接続をしてみましょう。
まず、server.propertiesの以下の部分を書き換えます。
```properties
broadcast-rcon-to-ops=false
enable-rcon=true
rcon.password=<任意の文字列>
```
この3行を設定すれば、準備は万端です。さぁ、Minecraftのサーバーを起動しましょう。
起動すると、以下のような表示が出ます。出なかったら設定を間違えています。
```
[23:27:25] [Server thread/INFO]: Thread RCON Listener started
```
これが出たら、Powershellあたりから以下のコマンドを実行し、mcrconで接続をします。
```powershell
mcrcon localhost
```
(なんか環境によっては動いたり動かなかったりします 私はVSCodeのターミナルでだけ動きました なんで？)
パスワードの入力を求められるので、さっき設定した文字列を入力してください。
接続に成功すると、Minecraftのコンソールに以下の表示が出ます。
```
[23:28:03] [RCON Listener #1/INFO]: Thread RCON Client /127.0.0.1 started
```
これで接続成功です。mcrconでMinecraftのコマンド(listなど)を打つとちゃんと出力が返ってきます。
切断はCtrl+Cを使ってください。それ以外にあるかも...
**※localhost以外でrconの接続を行う場合はセキュリティに十分気をつけてください**
## pythonでmcrconを動かす
まずpythonでmcrconを接続・切断するコマンドは
```python
import mcrcon

mcr = MCRcon("localhost", "rconのパス") # MCRcon起動
mcr.connect() # 接続
### ここにmcrconの動作を記述 ###
mcr.disconnect() # 切断
```
です。ちなみにサーバーを落とすときには切断しないと長ったらしいエラーを吐かれます。
まず初めに、mcrconを用いてlistコマンドを実行して出力を取得しましょう。
mcrconの動作に以下の内容を書いて実行してください。
```python
res = mcr.commands("list")
print(res)
```
pyファイルを実行すると、listコマンドを実行した結果が返ってきます。
これで「mcrconで接続してコマンドを実行するファイル」が完成しました。
ただ、今の状態だと『実行時にコマンドが実行されるだけ』なので、継続的な処理は全くできません。
## pythonからMinecraftサーバーを起動する
pythonファイルに以下の記述を書いたら、pythonからサーバーを起動できます。
ついでに入出力もいい感じにできます。
```python
server = subprocess.Popen(['java','-Xms4G','-Xmx4G','-jar','spigot-1.18.2.jar','--nogui'],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,shell=True,text=True,encoding='shift-jis')

for i in server.stdout:
    print(i,end="")
```
Popenについては自身で調べていただくとして、軽く説明もしておきます。
* 入力：Popenで開いた際に標準入力を受け取る形になる
* 出力：server.stdoutに保存されるのでforで表示させる

(エンコード方式を指定する必要があったりする。私はあった。)
起動方法は以下の記事を参照しました。
[PythonでMinecraftサーバーの独自コマンドを作る](https://qiita.com/yt0f1/items/dc6357f6b3f2a37c5253)
1年以上更新されてないですが、仕様はほぼ同じです。
# 2つの技術をつなぎ合わせるぞ
pythonからサーバーの起動ができます、pythonでmcrconを使った接続ができます。
この2つの技術をつなぎ合わせて、以下の動作を実装してみましょう。
**「pythonでMinecraftサーバーを起動し、mcrconを自動接続/自動切断する」**
レベル感はだいぶ上がりますが、2つの技術を習得できれば比較的簡単です。
やるぞ！(オモコロ風)
## Minecraftサーバーとmcrconを同時に起動する
まず、Minecraftサーバーを起動しながらmcrconを起動する準備をします。
先ほど記述した『Minecraftサーバー起動+標準出力表示』と、『mcrconの動作』を並列で実行できるようにしましょう。
pythonの技術『threading』を使用します。
threadingに関するちょ～～～～分かりやすい説明は以下のサイトあたりに載ってます。
[【Python】 並列処理を理解しよう 　【threadingの使い方 01】](https://zenn.dev/nekoallergy/articles/py-advance-threading-01)
[Pythonのthreadingとmultiprocessingを完全理解](https://qiita.com/kaitolucifer/items/e4ace07bd8e112388c75)
要するに、「メインスレッドとサブスレッドを同時に処理させることができる」というわけです。
とりあえず、サーバー起動後にmcrconをサブスレッドで動作させてみましょう。
ただ、mcrconは**Minecraftサーバーがrconのリッスンを開始したあとに起動する必要がある**ので、まずは標準出力の構文解析をする必要があります。
これがまたダルいんだ................
## 標準出力の構文解析(超簡易的)
今回は使用用途が「Minecraftサーバーの出力を構文解析する」だけなので、超簡易的な構文解析機能を作りましょう。
標準出力は取得できるので、それを解析してrconの接続を開始するコードは以下のとおりです。
今回追加する機能は
1. 標準出力を解析し、サーバー出力を取得する
2. リッスン開始に合わせてmcrconを起動する
3. リッスン終了に合わせてmcrconを切断する

の3つです。
```python
concheck = False # RCON接続確認用
disconcheck = False # RCON切断確認用

for i in server.stdout:
  print(i,end="") # サーバーの出力を表示
  if len(i.split()) > 3: # サーバーの出力が3単語以上の場合
    # 構文解析開始
    temp = i
    time = re.search(r"\[\d{2}:\d{2}:\d{2}\]", temp).group() # 時間を取得
    temp = temp.replace("%s " % time, "") # テキストから時間を削除
    type = re.search(r"\[.*?\]", temp).group() # 種別を取得
    temp = temp.replace("%s: " % type, "") # テキストから種別を削除
    if concheck == False and temp == "Thread RCON Listener started\n": # RCONのListen開始
      concheck = True
      ### ここにmcrcon起動の処理を書く ###
    if disconcheck == False and temp == "Stopping server\n": # サーバーの停止
      disconcheck = True
      ### mcrconにループ処理を記述するなら切断あたりの記述が必要
      ### mcrcon切断の処理は書かない(disconcheckをTrueにするだけで切断できるようにする)
```
長いね。仕方ないね。構文解析って手作りするとすごいダルいんだね。
内部使用については、見て分かる程度の難易度なので深堀って説明はしません。
強いて言えば正規表現の扱いが面倒というくらい。一応参考サイトを張っておきます。
[Pythonの正規表現モジュールreの使い方（match, search, subなど）](https://note.nkmk.me/python-re-match-search-findall-etc/)
これでメインスレッドの記述はほぼ終わりました。
あとはmcrconの内部動作を定義するだけです。(これもこれでそれなりに面倒)
## mcrcon起動用のサブスレッドを作成する
サブスレッドとしてmcrconを起動するために、mcrcon用の外部関数を作成します。
```python
def rcon(): # RCON動作
  mcr = MCRcon("localhost") # MCRcon起動
  mcr.connect() # 接続
  ### ここにmcrconの初期動作を記述 ###
  while(not disconcheck): # メイン処理
    ### ここにmcrconのループ動作を記述 ###
  mcr.disconnect() # 切断
```
あとはこれをthreadingで起動するだけです。標準出力構文解析の中にmcrcon起動の記述を書き足したものが以下の形です。
```python
concheck = False # RCON接続確認用
disconcheck = False # RCON切断確認用

for i in server.stdout:
  print(i,end="") # サーバーの出力を表示
  if len(i.split()) > 3: # サーバーの出力が3単語以上の場合
    # 構文解析開始
    temp = i
    time = re.search(r"\[\d{2}:\d{2}:\d{2}\]", temp).group() # 時間を取得
    temp = temp.replace("%s " % time, "") # テキストから時間を削除
    type = re.search(r"\[.*?\]", temp).group() # 種別を取得
    temp = temp.replace("%s: " % type, "") # テキストから種別を削除
    if concheck == False and temp == "Thread RCON Listener started\n": # RCONのListen開始
      concheck = True
      rconth = threading.Thread(target=rcon,args=())
      rconth.start()
    if disconcheck == False and temp == "Stopping server\n": # サーバーの停止
      disconcheck = True
      ### mcrconにループ処理を記述するなら切断あたりの記述が必要
      ### mcrcon切断の処理は書かない(disconcheckをTrueにするだけで切断できるようにする)
```
これでmcrconの起動と並列処理ができました。
とりあえず今回はここまでできれば十分でしょう。
お疲れさまでした。
# まとめ
今回の内容は、
* pythonでmcrconを動作させる
* pythonでMinecraftサーバーを起動する
* Minecraftサーバーの出力を取得し、解析する
* Minecraftサーバーとmcrcon動作を並列動作させる

の3点でした。
このあたりを超有効活用すれば、ゲームっぽい動作をさせることができます。
dataコマンドでインベントリを取得して色々情報を得たり、scoreboardコマンドで色々と処理をさせてtellrawで表示したり...
Minecraftの標準出力をmcrconに渡す方法もあるのですが、面倒なので気が向きに向けば書きます。
ちなみにmcrconを使うと普通のコマンドだけだととても難しい動作を記述できます。
例えば、プレイヤーネームの取得(listコマンドの出力を解析する)とか、stringの扱い(正規表現や配列で自由な操作を行う)とかができます。

ちなみにpythonで書いた一連のファイルをpyinstaller等でexe化すれば、**起動するだけでサーバー起動～データパック的動作ができるexeファイル**が完成します。
夢はでっかいぞ。