おはようございます.gurriumです.この記事ははてなエンジニアAdvent Calendarの3日目です。
インターネットの良いところは,普段の生活では知ることのない知識に出会えることだと思います.ということでこの記事ではWorld of Tanks(以下WoT)のMODを作ります.
WoTとは
WoTというのは戦車がメインの対戦ゲームです.云々. まあ細かいことはいいので公式を見てください.というか未だに何が面白いのか言語化できていないのです.退勤後すぐ始めるぐらいには面白いのですが…
クリスマスの時期は車両やスキン,クレジットなどが手に入る大規模なイベントが行われるので始めるにはいいタイミングです.公式YouTubeチャンネルではスーパープレーやRNG(Random Number Generatorの略,偶然撮れた珍プレーの意)をまとめた動画も公開されているのでそちらから入るのもいいと思います.
こちらはpart.1000を超えた今もアクティブに更新されているWoTのゆっくり実況シリーズです.実況を楽しむだけでなく,車両のスペックや立ち回りも参考になるいいシリーズです.
はじめに
ただ作るだけだと怒られそうなので,僕が新しい領域を開拓していくときに何を考え何をしているかも書いていきます.これなら技術記事と言えなくもないでしょう.
1.動機づけ
僕は,動機が無いと新しい領域に手を付けるのは難しい人なので,興味とかお金とか何かしら見つけます.今回はWoT熱とエントリーしたことによる締め切りです.
2.検索する
まずは「wot mod reference」のようにシンプルに検索します.運が良ければ知りたい情報がすべて見つかります.公式のMod portalやその運用開始のプレスリリースがヒットしますが欲しいものは見つかりません.
3.公式サイトを眺める
検索して見つからなくても公式サイトを眺めるとだいたい見つかるので,それっぽい文字列がないか探しましたが無さそうでした.こいうときはフッタのサイトマップ的なところを探すと見つかりやすいです.
4.検索ワードを変える
どうやらreferenceはなさそう & MODというのはあくまで非公式のもの ということで公式には公開していないのではと思い始めました.ただ,事実MODは開発されているので何かしら個人の記事があるだろうと思って「wot mod development」*1で検索しました.すると,いい感じの投稿とサイトがヒットしました.しかも一つは日本語ですね.ありがて〜〜〜 <3<3
5.古い情報を使う
古い情報でも部分的に使える/総当たりも使いよう
1つ目は動画付きの投稿で2016年のものでした.ディレクトリ構成は手元のものとは異なっていましたが,WoTのインストールフォルダ以下のres/scripts/
を触っていることがわかりました.手元のres/
以下にscripts/
はありませんでしたが,特に数も多くないのでres/
以下のすべてのディレクトリを眺めると,いかにもな感じのres/packages/scripts.pkg
というファイルが見つかりました.
知らない拡張子の調べ方
.pkg
という拡張子に見覚えはなかったので検索しましたが,こういうとき「open .ext」と検索すると怪しそうなソフトのLPが無限にヒットするのでおすすめしません.「wot .pkg」のように気持ち狭めに検索するとほしい結果が見つかる気がします.
「wot .pkg」だとPKG File Explorerというのがヒットしました.どうやらpkgファイルには3Dモデルも含まれている様子ですね.面白いですが知りたいことではなさそうなので別のキーワードを考えます.さっきの動画でres/scripts/
以下のファイルをdecompileするという話をしていたので「wot decompile .pkg」で検索すると,Starting client moddingという投稿がヒットしました.あたりっぽいですね..pkg
とdecompileの関連は薄かったので運が良かったです.
The byte code files are within res/packages/scripts.pkg file. It is just a zip-file with just a different extension. http://forum.worldoftanks.eu/index.php?/topic/719825-starting-client-modding/
これ後で気づいたんですけどfile*2を使えば一瞬でした.
❯ file scripts.pkg scripts.pkg: Zip archive data, at least v2.0 to extract, compression method=store
6.進められるところまで進める
ということで,unzip scripts.pkg
をするとpyc
ファイルが入ったscripts/
が展開されます.pyc
ファイルがコンパイルされたPythonのコードであるということはさっきの動画でわかっているので,decompilerを探します.「python decompiler」で調べるとuncompyle6*3がいいという回答が見つかりましたが,v1.14.1.4 *4のクライアントに入っているコードは3.9.9のもののようで,それに対応していないuncompyle6ではdecompileできませんでした. 違ってそう(後述
❯ uncompyle6 scripts/client/gui/app_loader/__init__.pyc I don't know about Python version '3.9.9' yet. Python versions 3.9 and greater are not supported. ...
次は「decompile python 3.9 pyc」で調べてこの回答を見つけました.pycdc*5のビルドにはcmake*6が必要でしたが入れていなかったのでHomebrew*7で入れました.使い方がわからなかったのでcmake -h
.
❯ cmake -h Usage cmake [options] <path-to-source> ...
あとはインストに従います.
❯ cmake . ❯ make ❯ make check
これで無事decompileできました.🎉🎉🎉
❯ ./pycdc ../wot/scripts/client/gui/app_loader/__init__.pyc # Source Generated with Decompyle++ # File: __init__.pyc (Python 2.7) from gui.app_loader import settings from gui.app_loader.decorators import app_getter from gui.app_loader.decorators import def_lobby from gui.app_loader.decorators import def_battle from gui.app_loader.decorators import sf_lobby from gui.app_loader.decorators import sf_battle __all__ = ('getAppLoaderConfig', 'decorators', 'settings', 'app_getter', 'def_lobby', 'def_battle', 'sf_lobby', 'sf_battle') def getAppLoaderConfig(manager): AppLoader = AppLoader import gui.app_loader.loader IAppLoader = IAppLoader import skeletons.gui.app_loader manager.addInstance(IAppLoader, AppLoader(), finalizer = 'fini')
7.コードを読む
それっぽいファイルを探すとscripts/client/gui/mods/__init__.pyc
というのが見つかりました.どうやらinit
メソッドとfini
メソッドが必要そうです.
# Source Generated with Decompyle++ # File: __init__.pyc (Python 2.7) def init(): global _mods _mods = _findValidMODs() forEach((lambda mod: _callModMethod(mod, 'init')), _mods.itervalues()) def fini(): forEach((lambda mod: _callModMethod(mod, 'fini')), _mods.itervalues()) _mods.clear() def _findValidMODs(path = None, package = None): result = { } if not path: pass path = __path__[0] if not package: pass package = __package__ modsFolder = ResMgr.openSection(path) ...
それと__path__
の中身が気になってgrepしていたんですが,ふと特殊な変数なのではという考えが降りてきた*8ので調べたところ当たりでした.
Packages support one more special attribute, __path__. This is initialized to be a list containing the name of the directory holding the package’s __init__.py before the code in that file is executed. https://docs.python.org/3/tutorial/modules.html#packages-in-multiple-directories The value must be iterable, but may be empty https://docs.python.org/3/reference/import.html#path__
ということで,__path__[0]
はscripts/client/gui/mods/
になっていそうです.そして4で見つけたサイトに関係しそうなことが書いてありました.
mod は
scripts/client/gui/mods
にmod_*.pyc
というファイル名で置かれます。 フォルダは仮想ファイルシステムでの相対位置を表すので、 実際にはres_mods/0.9.18.0/scripts/client/gui/mods
などのフォルダ、 もしくは、mods/0.9.18.0
に置かれた ZIP アーカイブファイルであるXXX.wotmod
のres/scripts/client/gui/mods
になります。https://wotmod.mtm-gaming.org/docs/mod_python.html#mod-%E3%81%AE%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E5%90%8D%E3%81%A8%E8%A8%AD%E7%BD%AE%E5%A0%B4%E6%89%80
ファイル名についてはこれですね.
# Source Generated with Decompyle++ # File: __init__.pyc (Python 2.7) def _isValidMOD(scriptName): if scriptName.startswith('mod_'): pass return scriptName.endswith(_MOD_NAME_POSTFIX)
8.MODを作る
4で見つけたサイトに結構書いてあったのでそれを見ればできる.解散!!とすると面白くないと思ってこのサイトを読まない縛りをしていたんですが,全然できなくて寝たさが勝ってきたので降参して巨人の肩に登ってこれを写経します.ゲーム開始時に通知センターにメッセージを表示するものです.wotmod.mtm-gaming.org
scripts/client/PlayerEvents.pyc
で定義されているonAccountBecomePlayer
を使います.
このサンプルでは、アカウントの処理が終了して有効なプレイヤーになるときに発生するイベント (onAccountBecomePlayer) に、 コールバック関数
__onAccountBecomePlayer
を追加登録しています。 https://wotmod.mtm-gaming.org/2017/07/01/notification_center.html
# Source Generated with Decompyle++ # File: PlayerEvents.pyc (Python 2.7) import Event class _PlayerEvents(object): def __init__(self): ... self.onAccountBecomePlayer = Event.Event() ...
grepとstrings
onAccountBecomePlayer
はscripts/client/Account.pyc
のonBecomePlayer()
で呼ばれていそうですが,それがどこから呼ばれているかは追えませんでした.grep -r onBecomePlayer scripts
して怪しかったのはscripts/client/Avatar.pyc
ですが,途中でdecompileが失敗して読めず,stringsを使ってonBecomePlayer
という文字列があるのを確認したところで力尽きました.(2時間ぐらいgrepとstringsを実行し続けていた…)
メッセージを表示するgui.SystemMessages
については引用元に説明があるので必要な人はそちらを参照してください.
Pythonのバージョン
適当な名前のPythonファイルを作ってコンパイルしてWorld_of_Tanks_ASIA/res_mods/1.14.1.4/scripts/client/gui/mods/
以下に置きます.
❯ python -m compileall mod_hoge.py ❯ ls __pycache__ mod_hoge.cpython-38.pyc
WoTを起動してもメッセージが出なくて泣きそうになりましたが,親切にも直下にpython.log
というファイルを作ってくれているのでそこを見るとコンパイルしたファイルがおかしそうということがわかりました.
2021-12-03 02:04:57.323: WARNING: [WARNING] (scripts/client/gui/mods/__init__.py, 72): There is problem while import gui mod ('gui.mods', 'mod_hoge.pyc') 2021-12-03 02:04:57.323: ERROR: [EXCEPTION] (scripts/client/gui/mods/__init__.py, 74): Traceback (most recent call last): File "scripts/client/gui/mods/__init__.py", line 67, in _findValidMODs File "scripts/common/Lib/importlib/__init__.py", line 37, in import_module ImportError: scripts/client/gui/mods/mod_hoge.pyc is not a valid Python compiled module file
まあPythonやしバージョンやろと思って2.7.18*9でコンパイルすると無事表示されました.3系とはコンパイルの方法が若干違うので注意です.
❯ python -m py_compile mod_hoge.py
終わりに
以上,WoTのMOD入門とそのために何を考え何をしていたかの記録です.インターネットに残る誰かの記録やツールに助けられて日々生活していることを改めて感じました.この記事もいつか誰かの助けになれば嬉しいです.
みなさんと戦場でお会いするのを楽しみにしています.それでは.