Cocos2d-x3.2でも実行中のゲームの画像を書き換える

Cocos2d-xには実行中のゲームのリソースを書き換えるHotUpdatingという機能があり、Cocos Code IDEでも[利用されています](http://www.cocos2d-x.org/wiki/How_to_Debug_Cocos2d-x_Lua_Game_Using_Code_IDE#Code-Hot-Updating)。3.1でこれらの機能をつかっていたのですが、

* [[Cocos2d-x]ゲームを起動したまま画像やレベルを編集して素早くプレビューする](http://qiita.com/blankblank/items/19eb40e9d75e1999a879)
* [cocos2d-xを使った開発で効率よく多職種連携したい話](http://www.slideshare.net/NaoyukiTotani/cocos2dx-37492696)

3.2からデータを転送するフォーマットが一部Protocol Buffersを利用したものに置き換えられ、以前のフォーマットを期待して書かれていたツール等が動かなくなったため、3.2でも動くようにします。

## Protocol Buffers
[Wikipedia曰く](http://ja.wikipedia.org/wiki/Protocol_Buffers)
> Protocol Buffers(プロトコルバッファー)はインタフェース定義言語(IDL)で構造を定義する通信や永続化での利用を目的としたシリアライズフォーマットであり、Googleにより開発されている。

だそうです。HotUpdatingではファイルサイズや更新時間など、ファイルのメタデータをProtocol Buffersで転送しています。以下は定義の一部です。

```cpp:templates/lua-template-runtime/frameworks/runtime-src/Classes/runtime/Protos.pb.h
class FileSendProtos : public ::google::protobuf::MessageLite {

...

  // required uint64 content_size = 4;
  inline bool has_content_size() const;
  inline void clear_content_size();
  static const int kContentSizeFieldNumber = 4;
  inline ::google::protobuf::uint64 content_size() const;
  inline void set_content_size(::google::protobuf::uint64 value);

...

  // optional uint64 modified_time = 6;
  inline bool has_modified_time() const;
  inline void clear_modified_time();
  static const int kModifiedTimeFieldNumber = 6;
  inline ::google::protobuf::uint64 modified_time() const;
  inline void set_modified_time(::google::protobuf::uint64 value);

...

}
```

本来言語に依存しない*.protoファイルがあるはずですが、現状ではリポジトリには含まれていないようなので、このコードをそのまま使ってエンコードの処理をC++で書きます。

## 送信するためのデータを作る
ファイルの情報を受け取って送信するデータを出力するコードは次のようになります。

```cpp:encode.cpp
#include<iostream>
#include "Protos.pb.h"

using namespace std;

int main(int argc, char* argv[])
{
    char* fileName = argv[1];
    int contentSize = atoi(argv[2]);
    int modifiedTime = atoi(argv[3]);

    runtime::FileSendProtos fileProto;
    fileProto.set_file_name(fileName);
    fileProto.set_package_seq(1);
    fileProto.set_package_sum(1);
    fileProto.set_content_size(contentSize);
    fileProto.set_compress_type(runtime::FileSendProtos_CompressType_CompressType_MIN);
    fileProto.set_modified_time(modifiedTime);
    string requestString;
    fileProto.SerializeToString(&requestString);

    char dataBuf[1024] = {0};
    struct RequestStruct 
    {
        char startFlag[12];
        unsigned short protoNum;
        unsigned short protoBufLen;
    };
    RequestStruct requestData;
    strcpy(requestData.startFlag, "RuntimeSend:");
    requestData.protoNum = 1; // FileServer::PROTONUM::FILEPROTO
    requestData.protoBufLen = (unsigned short)requestString.size();
    memcpy(dataBuf, &requestData, sizeof(requestData));
    memcpy(dataBuf + sizeof(requestData), requestString.c_str(), requestString.size());
    cout.write(dataBuf, sizeof(requestData) + requestString.size());
    return 0;
}
```
`runtime::FileSendProtos`がProtocol Buffersで記述されている箇所です。必要な情報をセットしてシリアライズします。`package_seq`と`package_sum`はデータを分割して送信する場合に順序と総数を指定します。requiredなフィールドなので、1回で送る場合も明示的に1を指定します。`FileSendProtos`には他にもoptionalなバリデーション用のフィールドがいくつかありますが、Runtimeの中では特に使われていないようだったので省略できます。

Runtimeは他にもメタデータ自体の長さなども期待しているので、それらも合わせて結合したデータを出力します。`SerializeToString`以降がその処理です。

## ローカルファイルの更新にフックして転送する
先に作ったツールを組み合わせると以下のようになります。

```py
import os
import traceback
from socket import *
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
import subprocess

CMD_PORT = 6010
FILE_PORT = 6020
BUFFER = 1024
HOST = '0.0.0.0' # change if other device
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) + '/..'
RESOURCE_DIR = BASE_DIR + '/res'

cmdSocket = None
fileSocket = None

class ResourceHandler(FileSystemEventHandler):
    def on_created(self, event):
        print('created: ' + event.src_path)
        on_resource(event, event.src_path)
    def on_modified(self, event):
        print('modified: ' + event.src_path)
        on_resource(event, event.src_path)
    def on_moved(self, event):
        print('moved: ' + event.src_path)
        on_resource(event, event.dest_path)

def on_resource(event, path):
    if event.is_directory or ('tmp' in path):
        return
    if upload(path):
        cmdSocket.send('sendrequest {"cmd":"reload","modulefiles":["src/main.lua"]}\n')

def upload(src_path):
    print("upload: " + src_path)
    success = False
    if not os.path.exists(src_path):
        print("not found: " + src_path)
        return
    filename = os.path.relpath(src_path, BASE_DIR)
    mtime = os.path.getmtime(src_path)
    size = os.path.getsize(src_path)
    metaData = None
    try:
        metaData = subprocess.check_output(["./encode", filename, str(size), str(mtime)])
    except Exception, e:
        traceback.print_exc()
        return False

    try:
        fileSocket.send(metaData)
        fp = open(src_path, 'rb')
        data = fp.read(BUFFER)
        while (data):
            fileSocket.send(data)
            data = fp.read(BUFFER)
        fp.close()
        success = True
    except Exception, e:
        traceback.print_exc()
    return success

if __name__ == "__main__":
    HOST = raw_input("type IP shown your device (like 127.0.0.1) >>> ")
    obs = Observer()
    obs.schedule(ResourceHandler(), RESOURCE_DIR, recursive=True)
    obs.start()
    cmdSocket = socket(AF_INET, SOCK_STREAM)
    cmdSocket.connect((HOST, CMD_PORT))
    cmdSocket.send('debugmsg on\n')
    fileSocket = socket(AF_INET, SOCK_STREAM)
    fileSocket.connect((HOST, FILE_PORT))
    try:
        while True:
            print cmdSocket.recv(BUFFER)
    except KeyboardInterrupt:
        obs.stop()
    obs.join()
    cmdSocket.send('exit\n')
    cmdSocket.close()
    fileSocket.close()
```