PythonとQuarryで24時間無料の自作Minecraftサーバーに挑戦してみた(接続・チャット編) ver1.15
開発の経緯
皆さんは、「Minecraftサーバーを無料で24時間公開したい」と思ったことはありませんか? しかし、一般的なMinecraftサーバーは最低でも4GB程度のRAMを要求するなど、それなりのスペックが必要なため、無料のホスティングサービスで動かすのは困難です。
そこで今回は、Pythonのライブラリを使って、より軽量な自作サーバーの開発に挑戦してみました。
開発方法
サーバーの自作には、主にQuarryというPythonライブラリを使用します。
Quarryとは
Quarryは、Minecraftのネットワークプロトコルを扱うためのライブラリです。公式ドキュメントによると、主な特徴は以下の通りです。
- Minecraft バージョン 1.7 から 1.15.2 までをサポート
- Python 2.7 および 3.5+ をサポート
twisted
とcryptography
を基盤として構築- 独自のクライアント、サーバー、プロキシを実装するための基本クラスとフックを公開
- NBT、Anvil、チャンクセクション、コマンドグラフ、エンティティメタデータなど、多くの Minecraft データ型を実装
- プロトコルの設計(パケットヘッダー、モード、圧縮、暗号化、ログイン/セッションなど)を実装
- 「init」「status」「login」モードのすべてのパケットを実装
- 「play」モードのほとんどのパケットは実装されておらず、必要なパケットのフックと実装は開発者に委ねられています。
公式にはMinecraft 1.15.2までと記載されていますが、実際には1.19.2あたりまで動作が確認できています。最新版とまではいきませんが、十分に新しいバージョンと言えるでしょう。
開発
ステップ1: 接続するとメッセージを返して切断するだけのサーバー
まずは、プレイヤーが接続したら「Pong」というメッセージを返して切断するだけの、非常にシンプルなサーバーを実装します。
from twisted.internet import reactor # 非同期ネットワークフレームワーク
from quarry.net.server import ServerFactory, ServerProtocol
class MyProtocol(ServerProtocol):
def player_joined(self):
# プレイヤーが入ってきた時に行われる処理
super().player_joined()
self.close("Pong") # Pongというメッセージを表示して接続を切断する
class MyFactory(ServerFactory):
protocol = MyProtocol # 必須: 使用するプロトコルクラスを指定
motd = "説明だよ!" # サーバーリストに表示される説明文
max_players = 1 # 最大プレイヤー接続人数
online_mode = False # 必須: オンライン認証を無効化
def main():
# ファクトリのインスタンスを作成
factory = MyFactory()
# 0.0.0.0:25565 でリッスン開始
factory.listen("")
# イベントループを開始
reactor.run()
if __name__ == '__main__':
main()
このサーバーを起動し、Minecraftクライアントから localhost
に接続すると、サーバーリストに以下のように表示されます。
そして、サーバーに接続しようとするとすぐに切断され、クライアントには次のようなメッセージが表示されます。
解説
-
MyFactory
クラス-
motd
: サーバーリストに表示される説明文です。 -
max_players
: サーバーに同時接続できる最大人数です。 -
online_mode
:True
にするとMicrosoftアカウントでの認証を行いますが、今回はFalse
(オフラインモード)に設定し、認証プロセスを省略します。
-
-
player_joined
メソッド- プレイヤーがサーバーへの接続を完了した直後に呼び出される関数です。
-
close
メソッド- プレイヤーとの接続を切断し、引数に指定したメッセージを表示します。
ステップ2: チャットルームサーバーを作ってみる
次に、プレイヤーがログインしてチャットができるサーバーを実装します。公式ドキュメントのサンプルコードを参考に、自分なりに解説コメントを追記したものが以下のコードです。
from twisted.internet import reactor#非同期ネットワークフレームワーク
from quarry.net.server import ServerFactory,ServerProtocol#serverを作るのに必要
class Protocol(ServerProtocol):
def player_joined(self):#playerが入ってきた時に行われる処理
ServerProtocol.player_joined(self)
self.send_packet("join_game",
self.buff_type.pack("iBqiB",#join_game用のデータをiBqiB形式にする
0,#プレイヤーのエンティティID
3,#gamemodeの指定(3なのでスペクテイタ)
0,#ゲームモード(オーバーワールド)
0,#シード値
0,#サーバーの最大プレイヤー人数
),
self.buff_type.pack_string("flat"),#ワールドタイプ(フラット)
self.buff_type.pack_varint(1),#描画距離
self.buff_type.pack("??",False,True),#デバック情報を減らす設定
)
self.send_packet("player_position_and_look",
self.buff_type.pack("dddffB",#playerの位置と向きを指定するdataの送信
0.0,#x座標
100.0,#y座標
0.0,#z座標
0.0,#yaw
0.0,#pitch
0b00000),#flags: 座標の相対/絶対指定フラグ
self.buff_type.pack_varint(0),
# self.buff_type.pack("?", False)
)
#接続維持の「keep Alive」の送信
self.ticker.add_loop(20,self.update_keep_alive)
#チャットでプレイヤーが参加したことを知らせる
self.factory.send_chat(u"\u00a7e%s has joined" % self.display_name)
def player_left(self):#プレイヤーが退出した時に呼び出される
ServerProtocol.player_left(self)
self.factory.send_chat(u"\u00a7e%s has left." % self.display_name)#messageで退出を知らせる
def update_keep_alive(self):
if self.protocol_version <= 338:#1.12.1以前
self.buff_type.pack_varint(0)
else:#1.12.2以降の場合
self.buff_type.pack('Q', 0)
# self.send_packet("keep_alive",payload)
def packet_chat_message(self, buff):
p_text = buff.unpack_string()
self.factory.send_chat("<%s> %s" % (self.display_name, p_text))
class Factory(ServerFactory):
protocol = Protocol#必須
motd = "説明だよ!"#serverlistの説明文
max_players = 1#最大プレイヤー接続人数
online_mode = False#必須
def send_chat(self, message):
# self.playersリスト(接続中の全プレイヤーのプロトコルインスタンスが入っている)をループします。
for player in self.players:
# 各プレイヤーに「Chat Message」パケットを送信します。
# pack_chatでメッセージをJSON形式に変換し、その後ろにチャットの位置(0=チャットボックス)を示すデータを付けています。
player.send_packet("chat_message",player.buff_type.pack_chat(message) + player.buff_type.pack('B', 0) )
def main():
factory = Factory()
factory.listen("")
reactor.run()
main()
問題発生: バージョンによるエラー
しかし、このコードは特定のバージョンのMinecraftでしか正常に動作しません。
Minecraft 1.19.2の場合
ワールドに入れず、以下のようなエラー画面が表示されます。
Minecraft 1.15の場合
1.15で接続するには、player_position_and_look
パケットを送信する部分の self.buff_type.pack("?", False)
をコメントアウトする必要がありました。
原因の調査: 1.19.2で必要なNBTデータ
なぜ1.19.2では接続できないのか調査したところ、新しいバージョンのMinecraftでは、ワールドに参加するために NBT (Named Binary Tag) という非常に複雑なデータが必要になることが分かりました。
具体的には、以下の2つの情報が join_game
パケットに含まれている必要があるようです。
1. レジストリコーデック (Registry Codec)
これは、そのサーバーに存在する**「世界のルールブック」**のようなものです。クライアントはこの情報を受け取ることで、サーバーに存在するディメンション(ワールドの種類)やバイオーム(気候帯)の全カタログを事前に把握します。
このNBTには、ディメンションごとの詳細な設定(ベッドが爆発するか、天空光があるかなど)や、バイオームごとの環境設定(天候、気温、空や水の色など)が大量に含まれています。2. ディメンション (Dimension)
これは、レジストリコーデックで定義されたカタログの中から、プレイヤーが今まさにスポーンするディメンションの情報を具体的に指定するものです。
これらのデータを自前で用意するのは非常に大変なため、今回はひとまず、比較的シンプルなプロトコルで動作するバージョン1.15での実装を目標にすることにしました。
最後に
有志の方が開発してくれたQuarryライブラリを使用することで、Minecraftの複雑な通信プロトコルをすべて理解せずとも、基本的なサーバーを実装できることが分かりました。
一方で、Minecraftのバージョンアップに伴い、クライアントとの通信に必要なデータがより複雑になっていることも実感しました。
今後、今回断念した1.19.2以降のバージョンに対応するため、NBTデータの生成・送信にも挑戦してみたいと思います。また進展がありましたら、新しい記事を投稿してこの下にリンクを貼り付けます。
Discussion