LoginSignup
10
6

More than 5 years have passed since last update.

RaspBerry Jam Modのpython側の処理の流れを少しだけ書き出してみる

Posted at

はじめに

当記事ではJava(mod)側の処理の流れは書かれていません。
Java側の処理の流れを知りたいと言う事であれば他記事を探すか、APIServer.java辺りから掘っていけば色々分かるかもしれません。

Raspberry Jam Modとはなんぞ?

先人の方々が詳しく紹介して下さっています。
Google先生に聞いてみると色々な記事が出てくるのですが、Qiitaでは検索した所3件しか出てきませんでした。(悲しい)

簡単に言うと、pythonでminecraftを操作できるようにしようぜと言うことです。
なにそれすごい

処理の概要

python側からminecraftが操作できると聞くと裏で凄い黒魔術でもうごめいてるんじゃないかとか思ったりするかもしれませんが、
実際にはpython側からJava(mod)側へ実行する内容のメッセージをソケット通信で送り、受け取ったJava(mod)側がメッセージに応じた処理を行っているだけです。

大雑把に書くと、Raspberry Jam Modは以下のような手順で処理を実行しています。

  1. python側で実行したい関数を呼び出す
  2. ConnectionのメソッドであるsendRaceiveが呼び出され、メッセージがJava(mod)側に送られる
  3. Java(mod)側はメッセージを受け取り、メッセージに応じた処理を実行し結果をpython側に返す
  4. python側はメッセージを受け取り、関数の呼び出し元に返す

では、実際に処理の流れをソースコードから追ってみたいと思います。

呼び出し→実行までの流れ

まずは実際に実行したい関数を呼び出したところから。
今回はspawnEntityを例に追ってみます。

main.py
from mcpipy.mcpi.minecraft import Minecraft

mc = Minecraft.create()
mc.spawnEntity("Pig",0,1,0)

spawnEntityは以下のようになっています。

minecraft.py
    def spawnEntity(self, *args):
        """Spawn entity (type,x,y,z,tags) and get its id => id:int"""
        return int(self.conn.sendReceive("world.spawnEntity", args))

conn.sendRaceiveはJava(mod)側にメッセージを送信し、戻り値としてJava(mod)側から受け取った実行結果を返すメソッドです。
また、connはConnectionクラスのオブジェクトであり、以下のように初期化されています。

minecraft.py
        if connection:
            self.conn = connection
        else:
            self.conn = Connection()

connが何者か分かった所で、sendRaceiveについて見て行きます。
上記したsendRaceiveはconnection.pyの中にConnectionのメソッドとして定義されています。

connection.py
    def sendReceive(self, *data):
        """Sends and receive data"""
        self.send(*data)
        return self.receive()

sendRaceive内ではsendとreceiveを順番に呼び出し、sendでメッセージの送信
receiveでメッセージの受信処理を行っています。

これらのメソッドの中では、socketを用いてメッセージの送受信を行っています。
と言うことで先にそちらを見て行きましょう。
socketはJava(mod)側とのメッセージのやり取りに使用されるソケット通信用のオブジェクトです。
pythonのsocketについてあまり詳しくないよという方は以下の記事を参考にしてみてください。

send関数内で使われているsocketは、Connectionのコンストラクタで以下のように初期化されています。

connection.py
    def __init__(self, address=None, port=None):
        self.windows = (platform.system() == "Windows" or platform.system().startswith("CYGWIN_NT"))
        if address==None:
            try:
                 address = os.environ['MINECRAFT_API_HOST']
            except KeyError:
                 address = "localhost"
        if port==None:
            try:
                 port = int(os.environ['MINECRAFT_API_PORT'])
            except KeyError:
                 port = 4711
        if sys.version_info[0] >= 3:
            self.send = self.send_python3
            self.send_flat = self.send_flat_python3
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((address, port))
    #以下略
    

ざっくり説明すると、Java(mod)側で開かれているsocketserverのIPアドレスとポート番号を指定してsocketを生成し、Java(mod)側との通信経路を確保しています。
これを用いる事によって、Java(mod)側とpython側とのやり取りが行えるようになります。

ではsendの処理を見てみます。
なおsendやsendRaceiveには末尾に「flat」や「python3」が付いたメソッドもありますが、送信までの流れは同じなので特に分けません。
(_python3はpython3用の各対応関数、_flatは文字列オンリーの場合にのみ使用?)

sendは以下のようになっています。

connection.py
    def send(self, f, *data):
        """Sends data. Note that a trailing newline '\n' is added here"""
        s = "%s(%s)\n"%(f, flatten_parameters_to_string(data))
        #print "s:"+s+":"
        self.drain()
        self.lastSent = s
        self.socket.sendall(s)

sendの処理の流れは以下の通りです。

  1. メッセージの形式に送信データを加工
  2. drain()を呼び出し受信バッファに残っているデータを全て流す
  3. lastSentに送信するメッセージを格納
  4. socketを利用してメッセージを送信

ちなみにここでlastSentにメッセージをわざわざ格納した理由ですが、これは後にエラー出力等の場面で利用するためかと思われます。

次にreceiveを呼び出し、Java側から送られてきた返答メッセージを受け取ります。

connection.py
    def receive(self):
        """Receives data. Note that the trailing newline '\n' is trimmed"""
        s = self.readFile.readline().rstrip("\n")
        if s == Connection.RequestFailed:
            raise RequestError("%s failed"%self.lastSent.strip())
        return s

receiveの流れは以下の通りです。

  1. readFileからの読み出しで受信したメッセージを読み取る
  2. もしメッセージがリクエスト失敗を表すものだった場合に例外を発生させる。
  3. 何事も無ければ受け取ったメッセージを呼び出し元に返す この中には処理に応じたメッセージ(位置取得の処理を行ったのであればその位置)などが格納されてる

以上が処理の内容となります。

まとめ

上記で紹介した関数を用いて処理の流れを書くと以下の通りです。

  1. 任意の実行したい関数を実行
  2. 実行内容に応じた引数をsendRaceiveに渡し呼び出す
  3. sendRaceive内でsendが呼び出される
  4. send内でJava(mod)側にメッセージを送信
  5. sendRaceiveからreceiveが呼び出される
  6. Java(mod)側からのメッセージを受信
  7. receive呼び出しで受け取ったメッセージを呼び出し元に返す
  8. 終了

また今回は面倒くさくて触れませんでしたが、メッセージを受け取ったJava(mod)側は、APIServerクラスでメッセージ受け取りの為の処理を行い、
メッセージを受け取ったらAPIHandlerクラスに投げつけてメッセージの解析と処理の実行をさせると言った流れで処理しているようです。
APIServerはRaspberryJamModクラス内のonServerStarting(恐らくワールド開始時に呼ばれるイベント?)の中で実体化されています。

詳しく知りたい方は、RaspberryJamMod.java,APIHandler.java,APIServer.java辺りを見てみれば分かるかもしれません。

感想

最初に触った時にはきっと想像もできない黒魔術で動いているんだろうなぁなどと考えていたのですが、どうやらそこまで難しい作りではなさそうで安心しました。
特に固有のライブラリを使わなければ実装できないと言う訳でも無さそうなのでソケット通信が標準でサポートされている言語であればそこそこ簡単に他言語版も作れそうです(調べた感じだとRuby版が作られている?)

誤字、脱字、日本語不自由部分、技術的な事等々何でもマサカリ大歓迎です。

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6