👋

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+ をサポート
  • twistedcryptography を基盤として構築
  • 独自のクライアント、サーバー、プロキシを実装するための基本クラスとフックを公開
  • 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 に接続すると、サーバーリストに以下のように表示されます。

server

そして、サーバーに接続しようとするとすぐに切断され、クライアントには次のようなメッセージが表示されます。

切断message

解説

  • MyFactoryクラス

    • motd: サーバーリストに表示される説明文です。
    • max_players: サーバーに同時接続できる最大人数です。
    • online_mode: TrueにするとMicrosoftアカウントでの認証を行いますが、今回はFalse(オフラインモード)に設定し、認証プロセスを省略します。
  • player_joinedメソッド

    • プレイヤーがサーバーへの接続を完了した直後に呼び出される関数です。
  • closeメソッド

    • プレイヤーとの接続を切断し、引数に指定したメッセージを表示します。

ステップ2: チャットルームサーバーを作ってみる

次に、プレイヤーがログインしてチャットができるサーバーを実装します。公式ドキュメントのサンプルコードを参考に、自分なりに解説コメントを追記したものが以下のコードです。

https://aistudio.google.com/app/prompts?state={"ids":["1MKDYF7iyhaM1eFcy00jrf0fLemCV1yJ9"],"action":"open","userId":"100793944339199882817","resourceKeys":{}}&usp=sharing

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の場合
ワールドに入れず、以下のようなエラー画面が表示されます。
スクリーンショット 2025-08-20 19.58.53.png

Minecraft 1.15の場合
1.15で接続するには、player_position_and_lookパケットを送信する部分の self.buff_type.pack("?", False) をコメントアウトする必要がありました。
スクリーンショット 2025-08-20 19.47.05.png

原因の調査: 1.19.2で必要なNBTデータ

なぜ1.19.2では接続できないのか調査したところ、新しいバージョンのMinecraftでは、ワールドに参加するために NBT (Named Binary Tag) という非常に複雑なデータが必要になることが分かりました。

具体的には、以下の2つの情報が join_game パケットに含まれている必要があるようです。

1. レジストリコーデック (Registry Codec)
これは、そのサーバーに存在する**「世界のルールブック」**のようなものです。クライアントはこの情報を受け取ることで、サーバーに存在するディメンション(ワールドの種類)やバイオーム(気候帯)の全カタログを事前に把握します。
このNBTには、ディメンションごとの詳細な設定(ベッドが爆発するか、天空光があるかなど)や、バイオームごとの環境設定(天候、気温、空や水の色など)が大量に含まれています。

2. ディメンション (Dimension)
これは、レジストリコーデックで定義されたカタログの中から、プレイヤーが今まさにスポーンするディメンションの情報を具体的に指定するものです。

これらのデータを自前で用意するのは非常に大変なため、今回はひとまず、比較的シンプルなプロトコルで動作するバージョン1.15での実装を目標にすることにしました。

https://github.com/codseowner/mincraft-server/tree/1.15

最後に

有志の方が開発してくれたQuarryライブラリを使用することで、Minecraftの複雑な通信プロトコルをすべて理解せずとも、基本的なサーバーを実装できることが分かりました。

一方で、Minecraftのバージョンアップに伴い、クライアントとの通信に必要なデータがより複雑になっていることも実感しました。

今後、今回断念した1.19.2以降のバージョンに対応するため、NBTデータの生成・送信にも挑戦してみたいと思います。また進展がありましたら、新しい記事を投稿してこの下にリンクを貼り付けます。

Discussion