はじめに
この記事では,discord.pyをつかったdiscord botからminecraft serverの起動,停止操作を行うまでの流れを説明します.
また,
- WSL
- Ubuntu 22.04.1
- java 18
- Python 3.10.6
-
discord.py
- python 3.8以降が必要
- minecraft server
を利用しています.
minecraft serverの準備
まずは,minecraft serverを準備します.serverはpapermcを利用します.
具体的な準備の流れは記事を書いたので読んでくださるとありがたいです.
簡単に説明すると,
- ディレクトリを用意する.
- サーバーを起動する際にいるディレクトリにワールドデータなどが展開されるからです.
- サーバーを起動
- papermcのRunning The Serverのところに起動の仕方が書いてあります
- eula.txtの
eula=false
をeula=true
にする.- 利用規約への同意です
- 再びサーバーを起動
- サーバーの停止
という感じです.
これでminecraft serverの準備はできました.
serverの起動と停止
discord botはpythonを利用するため,pythonからminecraft serverを起動,停止できるようにします.起動の際にはsubprocessを利用します.subprocessについての記事も書いたので読んでくださると喜びます.
serverの起動と停止はクラスにまとめます.これによりdiscord botからの起動と停止が容易になり,コードの修正がやりやすくなると思います.
コードは以下のようになりました.
server操作のコード
import os
import subprocess
class MCServer:
def __init__(self) -> None:
self.process: subprocess.Popen = None
def start(self):
if self.process is not None:
print("maybe already started")
return
jar_file_name = "paper-1.19.2-305.jar"
jar_directory = "../mcserver/"
# コマンドを実行するときの作業ディレクトリに
# ワールドデータなどが展開されるため
# 作業ディレクトリを移動
os.chdir(jar_directory)
memory_giga = 2
start_command = f"java -Xms{memory_giga}G -Xmx{memory_giga}G -jar {jar_file_name} --nogui"
start_command_list = start_command.split()
# サーバーの起動
print("server starting...")
self.process = subprocess.Popen(
start_command_list,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# このpythonファイルのディレクトリに戻す
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# subprocessの出力をリアルタイムに取得
while (self.process.poll() is None):
stdout_byte = self.process.stdout.readline().strip()
print(stdout_byte.decode())
if (b"Done" in stdout_byte):
break
# ループを抜けた理由が,subprocessが終了したとき
if self.process.poll() is not None:
print("startup error. kill process")
self.process.kill()
self.__process_communicate()
self.process = None
print("server started!")
def stop(self):
if self.process is None:
print("maybe server is not starting")
return
if self.process.poll() is not None:
print("already server is stopping")
return
print("server is stopping...")
self.__process_communicate(b"stop", timeout=30)
self.process = None
def __process_communicate(self, input: bytes = b"", timeout=5):
def printOutsErrs(outs: bytes, errs: bytes):
print("output")
print(outs.rstrip().decode())
print("\nerror")
print(errs.rstrip().decode())
try:
outs, errs = self.process.communicate(input, timeout)
printOutsErrs(outs, errs)
except (subprocess.TimeoutExpired, Exception) as e:
print(e)
self.process.kill()
outs, errs = self.process.communicate()
printOutsErrs(outs, errs)
注意点としては,
- minecraft serverを起動する際,現在の作業ディレクトリ内にワールドデータやeula.txtが生成されるため,ワールドデータなどを保存するディレクトリに作業ディレクトリを移動する.
- メモリ使用量の初期値と最大値を一致させるため変数に入れている
- コマンドにオプションを付ける場合,リストにする必要がある.
- リアルタイム出力において,起動処理の終了を
Done
と表示されたタイミングとしている- おそらく,マルチスレッドで出力と入力が並行して待機しているため
- 出力がない状態を
b''
やb' '
として試してみたが無限ループになった
- 出力は,空白文字によるインデントによって見やすくなっている可能性があるため,末尾の空白だけ除去
rstrip()
している. - subprocessの起動に
None
であるかの確認をしているため,subprocessが終了後にNone
と更新しないとsubprocessの再起動ができない -
Popen.communicate()
はタイムアウトする時間を設定できるが,その場合exceptionが発生する.
といったところです.
上手くいっているか試してみましょう.
from MCServer import MCServer
import time
server = MCServer()
server.start()
time.sleep(5)
server.stop()
実行すると,
$ python3 main.py
server starting...
Starting org.bukkit.craftbukkit.Main
System Info: Java 18 (OpenJDK 64-Bit Server VM 18.0.2-ea+9-Ubuntu-222.04) Host: Linux 5.15.74.2-microsoft-standard-WSL2 (amd64)
Loading libraries, please wait...
2022-12-09 01:59:10,605 ServerMain WARN Advanced terminal features are not available in this environment
[01:59:13 INFO]: Building unoptimized datafixer
...
[01:59:19 INFO]: Done (1.787s)! For help, type "help"
server started!
server is stopping...
output
[01:59:19 INFO]: Timings Reset
...
[01:59:29 INFO]: Closing Server
error
問題なく出力されていますね.error
って出力しているのでerror
が発生したのかと思いましたが勘違いでした.中身がないなら出力しないようにした方がよさそうですね.
ともかく,これでminecraft serverの起動と停止をpythonで実行できるようになりました.
discord botの準備
続いてdiscord botを準備します.discord botを作成する流れの記事もあります.環境変数の設定を忘れずに.
簡単に説明すると,
- bot用アカウントを登録
- tokenを取得
-
.env
ファイルで環境変数にtokenを設定 - discord.pyのインストール
という感じです.
discord botからserverの起動と停止
では最後にdiscord botからserverの起動と停止をできるようにしましょう.
コマンドからMCServer
クラスのstart
とstop
メソッドを呼べば大丈夫です.
discord bot本体,コマンドの登録
from MCServer import MCServer
from discord import Intents, Client
from discord.app_commands import CommandTree
from dotenv import load_dotenv
load_dotenv()
class MCClient(Client):
def __init__(self, intents: Intents) -> None:
super().__init__(intents=intents)
self.tree = CommandTree(self)
self.server = MCServer()
async def setup_hook(self) -> None:
await self.tree.sync()
async def on_ready(self):
print(f"login: {self.user.name} [{self.user.id}]")
from discord import app_commands, Interaction
from MCServer import MCServer
class MinecraftCmd(app_commands.Group):
def __init__(self, server):
super().__init__(name="minecraft")
self.server: MCServer = server
@app_commands.command()
async def start(self, interaction: Interaction):
await interaction.response.send_message('minecraft server is starting...')
started = self.server.start()
if started:
await interaction.followup.send(f"{interaction.user.mention}! server start successfully!")
else:
await interaction.followup.send(f"{interaction.user.mention}, sorry. server cannot start")
@app_commands.command()
async def stop(self, interaction: Interaction):
await interaction.response.send_message('minecraft server is stopping...')
stopped = self.server.stop()
if stopped:
await interaction.followup.send(f"{interaction.user.mention}! server stop successfully!")
else:
await interaction.followup.send(f"{interaction.user.mention}, sorry. server cannot stop")
実行には以下のようなコードを用意しました
import os
from discord import Intents
from discordbot import MCClient
from dotenv import load_dotenv
load_dotenv()
intents = Intents.default()
client = MCClient(intents=intents)
client.run(os.getenv("TOKEN"))
私の環境だけかもしれませんが,相変わらずコマンドが反映されるまでそこそこな時間が必要みたいです.
気長に待っていると反映されたので実行してみましょう
無事コマンドからminecraft serverを実行できました.
以上となります.discord.pyをつかったdiscord botからminecraft serverの起動,停止操作を行うまでの流れを説明しましました.
お疲れ様でした.