ぜのぜ

しりとりしようぜのぜのぜのぜ

WoTのMODを作りたい

おはようございます.gurriumです.この記事ははてなエンジニアAdvent Calendarの3日目です。

インターネットの良いところは,普段の生活では知ることのない知識に出会えることだと思います.ということでこの記事ではWorld of Tanks(以下WoT)のMODを作ります.

WoTとは

WoTというのは戦車がメインの対戦ゲームです.云々. まあ細かいことはいいので公式を見てください.というか未だに何が面白いのか言語化できていないのです.退勤後すぐ始めるぐらいには面白いのですが…

クリスマスの時期は車両やスキン,クレジットなどが手に入る大規模なイベントが行われるので始めるにはいいタイミングです.公式YouTubeチャンネルではスーパープレーRNG(Random Number Generatorの略,偶然撮れた珍プレーの意)をまとめた動画も公開されているのでそちらから入るのもいいと思います.

worldoftanks.asia

こちらはpart.1000を超えた今もアクティブに更新されているWoTのゆっくり実況シリーズです.実況を楽しむだけでなく,車両のスペックや立ち回りも参考になるいいシリーズです.

www.youtube.com

はじめに

ただ作るだけだと怒られそうなので,僕が新しい領域を開拓していくときに何を考え何をしているかも書いていきます.これなら技術記事と言えなくもないでしょう.

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/modsmod_*.pycというファイル名で置かれます。 フォルダは仮想ファイルシステムでの相対位置を表すので、 実際にはres_mods/0.9.18.0/scripts/client/gui/modsなどのフォルダ、 もしくは、 mods/0.9.18.0に置かれた ZIP アーカイブファイルであるXXX.wotmodres/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

onAccountBecomePlayerscripts/client/Account.pyconBecomePlayer()で呼ばれていそうですが,それがどこから呼ばれているかは追えませんでした.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

Hello Worldが表示されている画像
人生で一番時間がかかったHello World

終わりに

以上,WoTのMOD入門とそのために何を考え何をしていたかの記録です.インターネットに残る誰かの記録やツールに助けられて日々生活していることを改めて感じました.この記事もいつか誰かの助けになれば嬉しいです.

みなさんと戦場でお会いするのを楽しみにしています.それでは.

*1:この辺は難しいと思う.過程を言語化できない

*2:https://man7.org/linux/man-pages/man1/file.1.html

*3:https://github.com/rocky/python-uncompyle6/

*4:version.xmlより

*5:https://github.com/zrax/pycdc

*6:https://cmake.org/

*7:https://brew.sh/

*8:この辺は(ry

*9:怒られたときは3.8.12