MOD 開発で ありがちなエラー

特に注釈が無ければ、Winter, Modding Q&A から抜粋し、jik, Updated Module System Tutorial および Yoshiboy, Common Errors and How to Fix Them からの追加を加えたものです。

コンパイル時、何か起きたらそれが Warning(警告)なのか Error(エラー)なのか見分けて下さい。警告は、出ても(それなりに)動作する場合がありますが、いずれ(早く)修正すべきものです。一方 エラーは、もっと悪いものであって、それを修正しなければ先へ進めません。警告とエラーを見分けたら、コンパイラが出している説明を読んで下さい。標準のコンパイラにせよ何にせよ、多かれ少なかれ警告やエラーには説明メッセージが付きますされます。例えば改良版コンパイラの一つ WRECK では、通常の(TaleWorlds Entertainment 社による公式の).bat と比べて より多くの問題が明確になります [1]。何か問題が起きたら、最後に書き換えた箇所を確認して下さい。新しく加えた部隊かもしれないし、物品のエントリかもしれません。他の うまくいっている箇所と比べるなどして問題箇所を特定して下さい。

あなたの MOD システムが正常にコンパイルされた(エラーや警告が無くなった)からといって、新しく追加したスクリプトやトリガーなどが正常動作することを意味するわけではありません。エラーはコンパイル時にだけ起きるわけではないのです。

OPCODE エラーを読む

実行時、例えば新しく追加したスクリプトやトリガーなどの処理で、下図のように画面にエラーが赤色表示されることがあります。 [2]

(訳注: このメッセージはすぐに消えてしまいますが、[Q キー]-[メッセージ] の画面に残っています。ただし それも数十行までなので、後続のイベントのメッセージに消される前に、必要なら Ctrl Insert キーでどちらかの画面のスクリーン・ショット(画面コピー)を撮るとよいです。ただし、立て続けに撮ると、画像保存パスが移り込むので、公開の場で質問する際などは、セキュリティの観点から そのパスをトリミングするなり ぼかすなり して、見せないほうが安全です。)

このエラーメッセージを抜き出すと、次のように書かれています。

SCRIPT ERROR ON OPCODE 1677: Invalid Map Icon ID: 199; LINE NO: 25:
At script: create_kingdom_hero_party.
At script: create_kingdom_hero_party.
At script: create_kingdom_hero_party.
At script: create_kingdom_hero_party.

この問題を解決するには、次のように後ろから読んでいきます。

  • At script: create_kingdom_hero_party. → 通常はエラーが起きた場所。(module_scripts.py 内で名称が定義され、ID_scripts.py 内で ID が定義される。ここでは名称だけ表示されている。)
  • LINE NO: 25: → 当該スクリプト/トリガーのリスト [] 内の、タプル () の通し番号(0, 1, 2, ... の順に数えた番号)。ファイル全体の行番号でもないし、スクリプト内の行番号でもない。
  • Invalid Map Icon ID: 199; → 問題の核心。この例では、不当な ID。
  • SCRIPT ERROR ON OPCODE 1677: → 問題に関連したオペコード(つまり header_operations.py で定義された命令の番号)。この例では 1677 というオペコードなので party_set_banner_icon 命令。

(訳注: オペコードの数字と命令名の関係は header_operations.py (別途 拡張版もあり)で定義され、トリガー行の先頭にある値(負値など)とトリガー名の関係は header_triggers.py で定義されています。)

つまり まとめると、問題なのは module_scripts.py 内のスクリプト create_kingdom_hero_party の 26 個目(0 から数えると 25 番)にある party_set_banner_icon 命令のパラメータ「マップ・アイコン ID」に、誤った値 199 が渡ったことです。(このアイコン ID は、例えば「icon_camp」のように「icon_アイコン名」という形です。)そこでデバグ作業としては、party_set_banner_icon の手前の命令を調べ、なぜ正しい ID でなく 199 が渡ったか、理由を調べることになります。

訳注: 上記と全く同じ箇所で実行時エラーを起こして試そうとしても、ゲームをプレイして同じ状況を準備するのに手間がかかるでしょう。似た状況を作ってエラーだけ試したいなら、別ページに書いた Hello, world! の やり方を応用するなどして、下記のように自作スクリプトを書いて呼び出すとよいです。

(1) module_scripts.py の末尾にある角括弧閉じ「]」の手前に下記を追加。(字下げ数は自由)

  #----
  # test
  #----
  ("scr_test1",
    [
      (try_begin),
        (party_set_banner_icon, 0, 999),
      (try_end),
  ]),

(2) それを呼び出すために、キャンプ・メニューに選択肢と下記を追加。

(call_script, "script_scr_test1"),

(3) 実行すると、下記のように(期待通り)エラーが出るはずです。

SCRIPT ERROR ON OPCODE 1677: Invalid Map Icon ID: 999; LINE NO: 1:
At script: scr_test1.
At script: scr_test1.
At menu menu_camp item mno_resume_travelling consequece.

この 1677 は header_operations.py で定義された party_set_banner_icon 命令のオペコード、999 は今加えたコードが設定しようとした値です。LINE NO: 1 は、スクリプト scr_test1 内の 3 つの命令を 0 から数えた通し番号(リスト内のインデクス)で、それが 1 なので問題を起こした party_set_banner_icon 命令の位置(「0, 1, 2」の「1」)を示しています。もし try_begin と try_end の行を無くして party_set_banner_icon の行だけにすれば、LINE NO: 0 になるはずです。

ちなみに、実行時エラーなので、try ブロックがあろうと無かろうと とめられません。C++ 系とか Python のような例外発生を抑止する try とは異なり、この MOD 開発環境の try ブロックは判定命令が偽の時の全処理中止を抑止するだけです。

説明が繰り返しになりますが、エラーメッセージを注意深く読むことで問題を解決できます。OPCODE は、header_operations.py 内で各命令に割り当てられた番号です。だから 当該番号を探し、問題を起こしている命令を特定できます。例えば、OPCODE 1677 でなく OPCODE 1 で問題発生なら、call_script で問題が起きたことが判ります [3]。運良くあまり頻繁に使われない命令なら自分のソースを検索して見つけ易いですが、そうでないなら、LINE NO に表示された値で命令(のタプル)を数えて、場所を見つける必要があります。LINE NO と言っても行番号ではありません。コメントだけの行や空行は無関係だし、1 命令を複数行に分けて書いていてもタプル 1 個だし、1 行に 10 命令書いていたら その行だけでタプル 10 個です。

下記は、別の実行時エラーの例です。[4]

Unrecognized opcode 1073741855,; LINE NO: 30:
At Mission Template mst_lead_charge trigger no: 4 consequences
  • At Mission Template mst_lead_charge - module_mission_templates.py 内の lead_charge というミッション・テンプレートが問題に関係している。
  • trigger no: 4 - そのミッション・テンプレート中の(0 から数えて)4 番のトリガー。
  • consequences - そのトリガーの結果ブロック(条件ブロックでなく)。
  • LINE NO: 30: - 31 個目の命令(0 から数えて 30 番)。
  • Unrecognized opcode - 命令(のオペコード)を、エンジンが認識できない。
  • 1073741855 - 命令の番号(数が大きいので、eq のような単独の命令ではなく、this_or_next|eq のような、修飾子が「|」で付加された命令であることを示している。)
SCRIPT WARNING ON OPCODE ここに負の値;:
(訳注: スクリプトで警告。起きた箇所ではオペコードが負値。)

ありがちな原因: header_operations.py (や拡張版)で定義された命令の内、負値になり得るのは 修飾子 neg = 0x80000000 ぐらいで、それを「|」で付加した何らかの命令を実行中に警告/エラー。発生箇所のトリガー番号や命令のインデクスなども付近に表示されるので解析のヒントになる。[5]

訳注: 実行時の警告/エラーに負のオペコードが表示される状況を実際に試すとしたら、例えば下記のようにします。「どこに書けばいいか わかんないよ」というかたは、「モジュール開発システムを導入しよう」のページに注記した Hello, world! の例を読んで下さい。

(try_begin),
  (neg|3000, "@test string 1"),
  (display_message, "@dummy message."),
(try_end),

try_* で囲んだり、ダミー・メッセージの行を入れているのは、実行時エラーが意図通りに起きなかった場合に備えて、後続処理が(スキップされるなど)影響を受けないようにし、メッセージで確認できるようにするためです。

ここでは、定義されていないオペコード 3000 をわざと指定しています。オペコードのエラーである必要はなく、他のエラーを起こしてもよいのですが、前置きのコードが長くなるので簡単な例にしました。オペコード 8 なども未定義ですが試すとクラッシュするので この数値にしました。

3000 は 16 進表記では 0xBB8 で、ここでは neg と組み合わせている(ビット毎 OR している)ので、指定しているオペコードは 0x80000BB8 です。この行で実行時にエラー(Unrecognized opcode)を起こそうとしているので、エンジンは これを符号付き整数と見なすらしく、OPCODE として 10 進数 -2147480648 を表示します。

だから、本文の脚注の投稿にある -2147481946 のような桁の多い負数の OPCODE が表示された場合は次のように調べます。4 バイトの 16 進数に変換して 0x800006A6、ここから neg = 0x80000000 のビットを外して 0x6a6、もう一度 10 進に換えて 1702、header_operations.py から それを探し、agent_is_alive = 1702 が問題を起こしたオペコードです。

警告やエラーには他にファイルを特定する名称やトリガー名や そのトリガー内の命令を 0 から数えた番号が(LINE NO として)表示されるので、その付近にある その命令名(上の投稿の例では agent_is_alive)の位置を開発中の py ファイル内から探したり、出力の txt ファイルのオペコード(その例では 1702)を探します。

なお、修飾子 this_or_next のほうは 0x40000000 と定義されていて、4 バイト符号付き整数の符号を立てないので、OPCODE の負値表示に関与しにくいように見えます。

他に、命令に neg を伴わない場合でも、例えば トリガー・コード(10 進の負値)のような本来を命令(オペコード)として指定してはいけないものを誤って指定したような場合にも負のオペコードは表示されますが、その場合はエラーが Unrecognized opcode になるはずだし、桁が少ないので(せいぜい -80 のような 2 桁)、16 進数を持ち出すまでもなく、原因を追跡し易いはずです。

このコンパイル・エラーをどう解析するの?。(この内容が まだ有効かは不明。ちゃんとテストする必要あり)

rgl_log.txt を活用してゲームや独自コードのデバグをする。kalarhan, Modding VC: basic tutorials and Q&A thread

コンパイルが正常終了ならバグ無し、というわけではない。kalarhan, Modding Q&A

modding の実践テクニック。自分(たち)の変更箇所にマークを付けておく。EmielRegis (credit), Modding Q&A

コンパイル時によくあるエラー

何もかもが うまくいかない場合、スペルと構文を注意深く確認して下さい。カンマと括弧が どれも正しい位置にあることを確認します。公式モジュール・システムでのコンパイラ・エラーの原因としてよくあるのは、不正な構文です。コンパイルした時、ふつうは特定の行でエラーが指摘されます。テキスト・エディタによっては、行番号を表示できたり、括弧にカーソルを置いたりマウス・ホバーすることで括弧の対応が強調表示されたりし、作業が容易になります。

(訳注: 初期状態の build_module.bat だと、たった一つカンマを忘れただけで、膨大な行が出力され、とても読みにくいです。「モジュール開発システムを導入しよう」のページに注記したように、最初のエラー発生で止まるように変更するとよいです。)

よくあるエラーについて、下記に簡単にまとめます。

SyntaxError: invalid syntax
(訳注: 「シンタクス(書式)が不正」。)

ありがちな原因: 括弧 [] () やカンマ「,」や引用符「"」の過不足。

TypeError: 'tuple' object is not callable
(訳注: 「タプル・オブジェクトは呼び出しできない」。つまり、xxx(a,b,c) なら関数呼び出しだが、(...)(a,b,c) は不正。)

ありがちな原因: タプル同士の間のカンマが不足。

TypeError: list indices must be integers
(訳注: 「リストに指定する要素番号は整数なければならない」。つまり [...]["a"] や [...]("a")は不正。)

ありがちな原因: カンマ不足または括弧が余計。

ERROR: INPUT TOKEN NOT FOUND: <name>
NameError: global name 'cause_error' is not defined
(訳注: その名前の入力トークンが見つからない。グローバル名 '~' が未定義」)

ありがちな原因: 会話文を指定する際にパラメータが不足または ずれている。

(訳注: 原文は「You have a starting dialog-state -- example, [trp_guard,"guard_introduce_1", -- that isn't being led to by an ending dialog-state -- example, "close_window",[]],.」。まるで module_dialogs.py 内の会話文に限定のような説明だが、「開始部分と終端部分があるのに文を含むパラメータが無いのでパラメータが足りない、例えば ] が親のリストを閉じてしまっている」という例が正しいとして、「] が余計」というバグなら dialog に限らないはず。)
ERROR: INPUT TOKEN NOT FOUND: <empty>
NameError: global name 'cause_error' is not defined
(訳注: 「入力トークンが見つからない。グローバル名 '~' が未定義」)

ありがちな原因: 会話文の行が完結していない。

(訳注: 一つ前の例との違いは empty という箇所。原文は「You have an empty starting dialog-state -- example, [trp_guard,"guard_introduce_1",.」。例が正しいとすると、カンマの直後にピリオド(ドット)が指定された、ということらしい。これも会話文に限らず どこでも やってしまいそう。)
NameError: name '<something>' is not defined
(訳注: 「名称 '~' が未定義」)

ありがちな原因: trp_, itm_ or mnu_ のような接頭子が脱落、名前の未定義、スペルミス(つづりの誤り)。

IndexError: tuple index out of range
(訳注: 「タプルの要素数以上(要素数も含む)の番号(インデクス)を指定している」。)

ありがちな原因: 原因を特定しにくいものの、module_mission_templates.py を変更した場合、よくあるのはミッション・テンプレートの不用装備指定(alter flags)や AI フラグの未定義。

TypeError: int argument required
(訳注: 「整数型の引数が必要」。)

ありがちな原因: タプル内などに指定すべきものが整数であるべきなのに そうなっていない。どこで整数を使うべきかは、各モジュール・ファイル module_*.py 冒頭の説明を読むとわかる。例えばミッション・テンプレート・フラグのように (int) と書かれていれば整数。

(訳注: 上記は module_mission_templates.py の話。他にも例えば、module_game_menus.py の冒頭説明にはゲーム中の各メニューは「(menu-id, menu-flags, menu_text, mesh-name, [], []),」というタプルで書き表わし、その中の各項目に指定すべき型(Python のオブジェクト型)について「Game-menu flags (int)」とか「Game-menu id (string)」のように書かれているので、(誤記の可能性を想像しつつ)それに従う。)
IndexError: list index out of range
(訳注: 「リストの要素数以上(要素数も含む)の番号(インデクス)を指定している」)

ありがちな原因: リスト [] 内の項目にアクセスする箇所に問題がある。

(訳注: 例えば、3 個の要素からなるリストから読み出すならインデクスは 0~2 の範囲でなければいけないのに、3 以上や負値を指定するとこのエラーになりそう。)
Error: Unable to find object:<name>
ERROR: Illegal Identifier:<name>
(訳注: 「~という名のオブジェクトが見つからない。」「識別子(ID)~が不正。」)

ありがちな原因: 呼び出そうとしているオブジェクトが存在しない、または名称のスペルミス。変数に $: を付け忘れた可能性もある。

WARNING: Local variable never used:<name>
(訳注: 「ローカル変数 ~が使われていない」という警告。)

ありがちな原因: ローカル変数「:変数名」がスクリプト内で宣言だけされて全く使われていない。

ヘッダ・ファイルの import もれ

どこかの headers_*.py(または module_*.py )ファイルで定数を宣言していて、別のファイル内で その定数を使いたい場合、参照元が参照先を import して下さい。import しないと、それら定数の「値」をコードに直接書くことになるし、しかも定数の名前と数値のペアが宣言されている .py ファイルを何度も読み返しながら値を手作業で読み取ってくることになります (訳注: そんなことをしたら、コード中に わけのわからない数値(マジックナンバー)が溢れ、可読性が落ちるだけでなく、もし値の変更があったら、変更すべき箇所を探したり変更後の無害確認をするのに莫大な労力を要します))。この import をするには、参照元ファイルの先頭付近に例えば from module_constants import * のように「.py」を外した名前で行を追加します。元々どんな import があるかは、各 module_*.py ファイルの先頭を見ると判ります。この web 文書の「モジュール・システム・ファイル間の相互関連」のページに概要説明があります。また、別のページの「モジュール・システム接頭子」の項で説明されている接頭子を各 module_*.py ファイル で使えるようにするためにも、ほとんどのヘッダの import が必要です。

例えば特定の隊(troop)の属性(property)(訳注: スキル画面の左欄に表示される「体力、敏捷性、知性、魅力」) のどれかの値を module_scripts.py 内の記述(コード)で変更したいとします。隊のうち「主人公」、属性のうち「体力」を対象にしたいなら、(ID_*.py や header_*.py で宣言されている定数を利用して)下記ように書くことになるでしょう。

(troop_raise_attribute,"trp_player",ca_strength,-2),

しかし、もし header_troops.py import をせずに これをコンパイルすると、コンパイラは ca_strength が何を表わしているのか わからず、エラーを起こします。かたくなに import せず、数値(1=STR、2=AGI、3=INT、および 4=CHA)を直接書いてしまうと、前述のマジックナンバーだらけになってしまいます。

(troop_raise_attribute,"trp_player",1,-2),

だから この参照元のファイルの先頭付近に下記を加え、コンパイラに対し header_troops.py を参照することを伝えます。後ろの「.py」まで書く必要はありません。これで「ca_strength」が未定義というコンパイル・エラーは出なくなります。

from header_troops import *

その他 よくあるエラー

map_scene->manifold->faces[target_manifold_index].tag != rt_water (or rt_mountain)

ありがちな原因: ワールド・マップ上で、水(または山)の場所に隊をスポーン(発生)させようとしてる。全スポーン点とスポーン半径を確認する。

Get Object failed for *
(訳注: 「~のオブジェクトの取得に失敗。」)

ありがちな原因: オブジェクト取得失敗のエラーは、ゲーム内の BRF(Binary Resource File)(訳注: バイナリ・リソース・ファイルは、拡張子が .brf のバイナリ・ファイル群。Warband 本体の CommonRes フォルダに元々あるほか、各 MOD ごとに Resource フォルダに置く。この web 文書の「BRF バイナリ・リソースの扱い」のページにも説明あり。) や物品/シーン/シーン・プロップのいずれかの中で、存在しないテクスチャ/マテリアル/メッシュを参照すると発生する。語のスペル(つづり)を確認し、全てが正しく参照されていることを確認する。大文字小文字が区別されることに要注意。BRF のリソースを正しい順序でロードさせていることを確認する。

Buffer.has_more_tokens()
(訳注: 「バッファ・クラスの(インスタンスの)has_more_tokens メソッド(を呼び出そうとしてエラー? または そこを処理中にエラー?)」)

ありがちな原因: データ構造の予想サイズと実サイズ間の不一致を意味する。不正 BRF、破損した .txt ファイル、またはセーブ・ゲームの非互換性によって起き得る。また、データ構造の終わりを超えて読み取ったことが原因の可能性もある。例えば、25 個の文字列しかないのに、57 番目を要求したなど。識別子名にアンダスコア _ にすべきところで空白を使ってしまった場合も同様。このエラーは様々な要因が原因で起き得るので、原因特定作業は複雑。

rgl_between(skill_level,0,(skills[skill_no].max.level+1))

ありがちな原因: ゲーム・エンジンが、属性(体力、敏捷性、知性、魅力)にかかわらず、読み取ったスキルの数値が最大値を超えていないことを調べる。通常、ここ(エンジン内)でエラーが起きるのは、存在しない兵からスキルを読み取ろうとしたか、存在しないスキルを読み取ろうとしたか、またはその両方。例えば、knows_common のような事前定義されたスキル・セットを使って兵を作り、スキル・ポイントを追加し、そこでスキル値が最大値を超えるような時に起きるが、原因に気付きにくいかも。

(訳注: このエラーの前後には xxxx.h などのソース・ファイル名を伴うが、これは実行時エラーで、MS 社の Visual C++ をインストールした環境で報告される(らしい)。その名から rgl_log.txt に出るのかも。上記の原文には参考リンクがないが、例えば vako氏の投稿 Modding Q&A と その少し下の fisheye 氏の回答などに このエラー(クラッシュあり)記述あり。他の投稿も含め かなり古く、2010 年ごろを最後に、以降は当該エラーに関する投稿が見当たらない。だから最近の Warband でも起きるかは不明。)
rgl_between(current_sentence, 0, num_sentences)

ありがちな原因: 何も発言すべきことがない人物を相手に話した時に発生する。

(訳注: 上記も ひとつ前のエラーと同様、実行時にクラッシュするらしい。例えば次の投稿。Lemonfield, Modding Q&A。)


その他

  • ゲーム開始時に複数回レベルアップしてしまう。Lumos, Modding Q&AModding Q&A
  • ゲーム開始時のレベルの問題。Lumos, Modding Q&A
  • ゲーム開始時の奇妙なレベルアップ。Lumos, Modding Q&A
  • 「Hash Vector failed at index -100000」というエラーが出てクラッシュ、cmpxchg8b, Modding Q&A
  • 公式 MOD 開発システムでなく本体にハード・コーディングされたもの、The_dragon, Modding Q&A
  • 最終行でのエラー、jacobhinds, Modding Q&A

(訳注: py ファイルの変更やコンパイル結果のフォルダへの出力は、ゲームを起動した状態でもできますが、ゲームを再起動しないと、Modules 下の当該 MOD フォルダに置いたファイルの効き目は(ほぼ全て?)反映されないことに注意して下さい。デバグやテストの作業中は地道に このゲーム終了と起動を繰り返すことになります。

コンパイルと再起動を減らすテクニックの一つとして次のようなものもあります。会話中に右上区画に表示される「会話文」や その下の「プレイヤーの回答選択肢」などでは(languages フォルダ下の csv ファイルでローカル言語に翻訳できるぐらいなので)所々で数値変数(reg*)や文字変数(s*)を表示しています。だから、おあつらえ向きのところに既存メニューがあれば、ソースをいじらなくても、.txt ファイルや翻訳された .csv の当該箇所を見つけて他の reg* や s* を追記すれば「レジスタの表示だけ」ならできます。

更に言うと、後に残さない、3 分後には消すような一時的なテスト用のコードなどで、一ヶ所の reg3 を reg4 に訂正するとか、eq 命令を neg|eq に訂正する程度の変更なら、.txt 内の数字の羅列を直接いじるほうが早いこともあります。オペコードや .txt の内部形式に慣れてくると それも可能です。勿論 いずれにせよ、.txt ファイルを直接変更する場合は後でコンパイル結果に上書きされ得ることに注意して下さい。)

ASCII 以外の文字

  • ASCII でない文字、エラーの再現方法、Antonis, Mount & Blade Modding Discord
  • コンパイル後に全テキスト・ファイルを UTF-8 で保存し直しても、コンパイルの度に ANSI に戻ってしまうので手間。これを防ぐために冒頭に指示行を置いて .py ファイルを UTF-8 化。Swyter, Modding Q&A
  • (訳注: この UTF-8 化をすれば、.py ファイルに ローカル言語、例えば日本語でコメントを入れたり、会話文やエラー・メッセージなどをローカル言語に変える(ローカライズする)こともできるかもしれませんが、西ヨーロッパ言語の特殊文字くらいまでなら許容範囲としても、CJK(中国語、日本語、韓国語)などの環境では(特殊な場合を除いて)そうすべきではないでしょう。コメントについては、ローカル化してしまうと、英語がメインのフォーラムで(誰かが)コードを部分引用して質問するときに、コメントを外したり英訳したりが必要になるし、MOD を各国の何人かで開発したり拡張したりする可能性を狭め、結果的に MOD の認知度や寿命を縮めるかもしれません。自国優先的な感覚で無計画にコメントやソースを日本語化するのは、やめましょう。コンパイラ process_*.py 内で出す(書いてある)エラー・メッセージについても、地域言語化されてしまうと、他の言語を母語とする人は対処、質問、回答に困るでしょう。

    一方、会話文や UI については、Warband では コンパイルで出力される txt を直接 翻訳せずに languages フォルダ下に csv を置いてオーバライド(機能的に上書き)させるよう設計されています。その機能を積極的に利用しましょう。それについては「モジュール構造だから翻訳もし易い」のページにも説明があります。

  • Warband マルチ・プレイヤーで Alt コード文字を使いたければ、Alt キーで直接入力でなく、フォントをいじる。, Modding Q&A
  • (訳注: 上記の「Alt コード」とは、Windows ならメモ帳などで Alt キーを押しながら 4 桁ほどの数字をタイプして 1 文字入力する方法のこと。使用言語ごとにローカル言語に固有の文字を入力できる。上記投稿では それを使うとダメで、フォント・ファイル(画像)をうまく使うか 必要なら変更しろ、という意見。)

  • プロフィールに英語キーボードから北欧文字を入力。Alt コードを使うか、profiles.dat を notepad++ で開いてタイプし貼り付ける。ただし、質問者のところでは うまくいかないそう。SupaNinjaMan と MadocComadrin, Modding Q&A
  • 「Non-ASCII found, yet no encoding declared.」エラー。対処方法は上の項の .py ファイルの UTF-8 化 と同じで、冒頭に行追加。kalarhan, Modding Q&A

ワールド・マップで起きる問題

(この項は、ゆくゆくは別ページに分離するのが理に適っています。起き得る問題を相互参照する形になるかも。)

  • 川や浅瀬に接続しない部分に分離されると、ワールド・マップの挙動がおかしくなることがある。GetAssista, Modding Q&A と Lumos, Modding Q&A
  • 新しく地面テクスチャを定義できなくても、既存の地面テクスチャのフラグ、メッシュ、色などを変更できるようにすべき。Somebody, Modding Q&A。既存のものの置き換え。Dargor (credit?) の Modding Q&A
  • 訓練場。Ritter Dummbatz, Modding Q&A
  • 海の深さの効果について、ほか。Cozur, Modding Q&A
  • ワールド・マップの広さの限界についての議論 Modding Q&A
  • マップの描画を遅くする要因。Mad Vader, Modding Q&A
  • マップ上に傭兵キャンプや小さな居留地を作れるか。Lav, Modding Q&A
  • メッシュあたりの頂点数の上限。cmpxchg8b, Global Map Visualiser
  • set_show_messages の値を切り替えただけで s1 が化けた。Somebody, Modding Q&A
  • エンジンにハード・コーディングされたマップ上の物 map_*。Swyter, Modding Q&AModding Q&A
  • 用途ごとのマップ・エディタの使い分け。.bmp からの作成には thorgrims 、.obj メッシュのインポート/エクスポートには swyter's cartographer 、隊の動きには bloodpass editor 。jacobhinds (Kentucky 『 HEIGUI 』 James), Modding Q&A
  • マップの木は、木のマテリアルを使うようハード・コーディングされている。jacobhinds, Modding Q&A
  • 地形テクスチャをうまくブレンドさせる要件。Z 軸の変化が大きいと積雪効果が不良。メッシュが格子状に近い理由。jacobhinds, Modding Q&A
  • 深海のマテリアル。沼についての議論。jacobhinds, Lav, La Grandmaster, Seek n Destroy, Dusk Voyager, Modding Q&A
  • 浅瀬のテクスチャのマテリアル。Michadr, produno (credit), Modding Q&A
  • 内海の潮流についての議論。Modding Q&A and Modding Q&A。dstn や Swyter も書いていて、シェーダが起こしている。
  • 深海では歩けるが、terrain_types ヘッダー・ファイルに含まれていないので、隊がそこにいるかどうかを判定できない。jacobhinds, Modding Q&A
  • シェーダをいじって、海の見映えを改良。La Grandmaster, Modding Q&A
  • マップの森を増やすと実行速度に影響するかどうか。cmpxchg8b, Kentucky 『 HEIGUI 』 James, Modding Q&A
  • Thorgrim マップ・エディタの問題。MadVader, Modding Q&A
  • ハード・コーディングされた地形。The Bowman (credit?), Modding Q&A
  • ランダムな森の問題。The Bowman, Modding Q&A
  • マップ・エディタで作成したものを MOD システムに持ち込む方法。NPC99, Modding Q&A
  • script_game_get_party_speed_modifier は経路探索スキルに影響されるか。kalarhan, Modding Q&A
  • マップで時々クラッシュする。NPC99, Modding Q&A など。
  • マップの山岳地帯でのカメラ制御。NPC99, Modding Q&A
  • マップの日付の近くに時刻表示を追加。kalarhan, Modding Q&A
  • Native のマップ・テクスチャに新しいテクスチャを追加可能か。NPC99, Modding Q&A
  • 非アクティブな (クリックできない) オブジェクトをマップ上に配置する場合の数の上限。NPC99, Modding Q&A
  • マップでの季節の変化。Docm30, Modding Q&A
  • 新勢力の領主が島に置かれるとそこから離れられないように見える。乗船下船させるように変更する方法。NPC99, Modding Q&A
  • ground_specs ファイルの変更が反映されない。NPC99, Modding Q&A
  • マップの特定の地形タイプから海岸効果を削除しようとして、map_plains のシェーダを map_mountains で使われているものと同じものに変更してみたが、効果が無い。NPC99, Modding Q&A
  • マップの水の輝きと平坦地形。Leprechaun (credit), Useful Techniques
  • マップを陸塊に分けようとして無限ループ。浅瀬の位置に基づいてマップを別々の陸地に分割しようとしている。経路探索のためには、どの浅瀬が陸地 A から陸地 B に通じているかを知る必要がある。cmpxchg8b, [WB] Warband Script Enhancer v3.2.0
  • マップのメッシュあたり頂点数の上限。cmpxchg8b, Global Map Visualiser

 txt 読解と変更

[訳者からの免責事項] この枠内は、原著者の意図とは関係ありません。私(訳者)は、Warband 製造元である TaleWorlds Entertainment 社(TW 社)その他 権利者の不利益になるような侵害行為(例: 商用コードの無断開示など)の助長を意図していません。その点をご理解の上、ここに書いた情報をご利用の場合は、自己責任でどうぞ。私(訳者)の誤りに起因するか否かにかかわらず、利用したかたが何らか不利益を被っても、私は責任を負いません。

読解と変更の概要

.txt ファイルの読解や変更が、なぜ必要なのでしょう。誰にどんな得が? どこかでチート(ずるい方法)のテクニックを披露して優越感に浸る? いえいえ、もっとずっと有意義なことがあります。

一つは、ソース・コードを見ることができないような一般ユーザや翻訳者でさえ いつでもデバグ作業に参加できるので、お気に入りの既製 MODの発展に貢献できることです。また、ソースにアクセスできる開発者にとっても、開発やテストの効率を上げる選択肢が広がります。これら利点による品質の早期向上(つまりバグが早く収束する)を期待できます。

もう一つの意義は、Warband MOD 開発を学ぶ人たちにとって、既製 MOD をお手本にし、コードの読み書きの実践量を増やすことで、構造やプログラミング手法の理解にかかる時間を短縮できたり、バグ管理(プロジェクト管理)やチーム編成など方法のヒントを得たりできることです。

簡単な読解例

MOD 開発システムが出力した .txt には Warband エンジンが理解できる「数字の羅列」が多く含まれ、一見わかりにくいですが実は かなり可逆的で、元のソース・コードに書かれた命令群(operations)などを ほぼ復元(復号)できます。(「ほぼ」と書いたのは、ローカル変数名やスロット名など一部が復元不能だからです。)

この web 文書の「そもそも...」のページとややダブりますが、もう少し掘り下げます。

例えば この図はスクリプト・モジュールがコンパイルされて出力された scripts.txt の一部です。npc_get_troop_wage というスクリプト名の次行を見ると、先頭の半角空白一つに続く、14 が そのスクリプトの命令数を示しています。最初の命令のオペコード 21 を header_operations.py 拡張版で調べると それが store_script_param_1 命令だと判ります。続く 1 はその命令に指定されたオペランド数、その後ろの長い数字が実際のオペランドです。

この長い数字 1224979098644774912 を「電卓」で 16 進数にすると 0x1100000000000000 で、先頭バイトの 11 はローカル変数を示す接頭子です(早見表を別途示しました)。最下位の何バイトかが示すのが番号で、今は 0 なので、このブロックで最初に使われたローカル変数 0 番を意味します。つまり、最初の命令は store_script_param_1, ":L0" という形だと判ります。ローカル変数の名前までは txt から読み取れません。

その後ろが 2 番目の命令で、オペコード が 2133、2 個のオペランドを伴い、それらがローカル変数 1 番と数値 0 であることが読み取れます。つまり assign, ":L1", 0 です。続く 3 番目の命令は 4 つまり try_begin で、常にオペランドを持たないので、後ろは 0 です。この要領で行末まで辿って行くと、14 個の命令が読み取れるはずです。

もしソース・コードが非公開の既製 MOD なら、概略はここまでです。しかし上図は MOD システムが出力した(つまり Native と同じ)scripts.txt なので、もちろんソース・コードと照合できます。module_scripts.py を開いて当該スクリプトを見ると、次のような 14 個の命令が書かれていて、読み解いた結果とも一致します。

  ("npc_get_troop_wage",
    [
      (store_script_param_1, ":troop_id"),
      (assign,":wage", 0),
      (try_begin),

      ...

      (assign, reg0, ":wage"),
  ]),

ということは、裏を返せば、ソース・コードの無い既製 MOD であっても、命令の「読解」「追加」「変更」「削除」ができますよね? 例えば display_message 命令を加えたければ行頭の数字を +1 して命令と命令の境い目にその命令(オペコードとオペランド数とオペランド)を置けばいいし、テスト用にスクリプトを 1 個増やしたければ、ファイル先頭付近にあるスクリプト数を +1 してエンジンに知らせつつ、ファイルの末尾などに他と同じ書式でスクリプトを書き加えればよいのです。他のモジュールでも、条件ブロックと結果ブロックがあったり、トリガーごとに分かれていたりするなど、多少の書式の違いはあるものの基本は同じです。

しかし このような読解を、全て手作業でやっていたのでは時間がかかります。もっと効率よくやらないと、.txt を扱う意味は薄れてしまいます。効率を上げるための環境整備や、ツール自作のためのヒントなどについては、少し後のほうの項で説明するとして、次項では先に、一般ユーザを含めた人たちの出番について紹介します。


txt を読解する目的や意義

ある「既製 MOD」があって、そのソース・コードにアクセスできないようなテスター、バグ追跡者、翻訳者、一般ユーザが、その MOD の動作バグやテキスト・バグに気付いたとします。もし「症状」と「再現方法」だけを開発者に報告したのでは、開発者はソース・コード内の原因箇所を自分で探したり、修正前と後の再現確認に時間を費やさねばなりません。でも、報告者たちが txt ファイルの復号方法を知っていれば、バグの原因箇所、対策コード案、加速試験手順などを開発者に示すことが可能になり、その MOD の品質がより早く良くなることに貢献できます。そうすれば その MOD の愛好者が増え、関連コミュニティが盛り上がり、開発者の士気も上がり、MOD の寿命も延びるかもしれません。

ソース・コードを持っている開発者自身も、txt ファイルの「数字」の規則性を理解していれば、ソース・コード検索と txt 内検索を使い分けて効率を上げる選択ができます。なぜ txt のほうが容易に検索できるような場合があるのでしょうか。理由を次項以降で説明します(開発者以外のかたも ぜひ一読下さい)。

もう一つの意義は、Warband MOD 開発の学習者にとって、理解を早める助けになることです。開発にとても興味があるが、進行中の既存プロジェクトに途中参加する勇気は無く、自ら大規模な MOD 開発プロジェクトを立ち上げるほどの充分な手腕も人脈も、公開ノウハウも持ち合わせない、という人は大勢いるでしょう。そうかと言って、MOD 開発システムの(Native の)ソースを眺めたり、目標も無く小変更を繰り返したりするだけでは、技術的な理解が進みにくく、やりたいことのアイディアも広がっていかないでしょう。その打開策となるのが、既に出回っている、他の人たちの作った立派な MOD の .txt を読むことです。バグ報告(フィードバック)や改善コード案提示などで品質向上に貢献しつつ、自分の理解やデバグ能力や、バグを作り込みにくいコーディング方法が自然と身に付いていくはずです。


練習課題: 加速試験の準備

あなたは ある既製 MOD を楽しくプレイしていたところ、どこかのシーンで動作の不具合(バグ)を見つけた。早速 原因箇所を調べて対策コード案を開発者に報告しよう、と考えた。その MOD のソース・コードは非公開で、あなたはアクセスできない。

その MOD では、村のシーンで村長(長老)に対し「力になれること」を尋ねた時にランダムに依頼される仕事が 20 種類以上用意されていて、そのうちの特定の一つを引き受けた時のミッション・テンプレートの処理中に何らか問題が起きるのだ。

しかし その特定の仕事はランダムなので一度や二度では現れないし、村長と立て続けに話しても仕事をくれなくなってしまうので、所望の仕事が来るまで「再ロード→村の中心へ行く→村長のところまで行って話す」という不毛な作業を、数え切れないほど繰り返すことになる。そんなことをしていては、「再現」させるにも、あるいは実験的なコード変更後の「確認」をするにも、膨大な時間がかかってしまうだろう。しかも、そうやって あなたが突き止めた原因箇所や対策コード案を、開発者に示したとしても、開発者は 同じように長時間かけて幸運な乱数を待たねばならないはずだ。

このようなバグ調査や対策後確認などを加速するためには、コードを書き替え、与えられる仕事が一定になるよう、一時的に村長の選択を強制するとよい。そして開発者にも その加速方法を知らせれば、開発者は乱数待ちの二の舞を踏まずに済む。

では、村長クエストの場合、相手からの依頼を こちらの意図する一つに固定(強制)するには、どの .txt ファイルの、どの行の、どのブロックを変更したらよいか? ソース・コードを見ずに、Native の .txt ファイルを読み解いて その場所を示せ。

(方法はいくつかあるが、この強制は一過性のものなので、容易に元に戻せるよう、変更量の少ない方法が望ましい。)


txt 検索が有利な場合

ソース・コードからの検索だと、改行が何千何万もあるせいで エディタで開いた時にブロックのどこを見ているかが判りにくいことが多く、更にコメント(説明文用、長期の無効化用、一時的な無効化用など)が含まれていたり、字下げ(インデント)が不統一だったたりして、それらが検索を困難にすることがよくあります。例えば、エディタで troop_get_slot 命令を次々と探していたら いつのまにかブロックの境界をまたいでいて、それに気づかずに誤った箇所を変更してしまったり、変更が抜けたりしがちです。かと言って、境界やコメントにばかり神経を使うと、本題を忘れたり、作業時間をロスしたりします。

一方 txt ファイルからの検索なら、ブロックの境界がより明瞭です。例えばスクリプト・モジュールなら改行がブロック境界です。たとえ条件ブロックと結果ブロックがあるモジュールであっても、慣れるとその境界はすぐに見つかります。そこには空白が 2 個あったり、文字列がはさまれていたりするからです。

また、特定のバグを作り込んだバージョンを知りたくなり、バージョン間で比較するような時も、コメントなどを大幅に変更しているはずのソース・コード同士よりも、txt を比べたほうが容易な場合があります。

txt ファイル内を検索するにしても、正規表現を全く使わないのでは、かえって効率が落ちるでしょう。上の例で挙げた troop_get_slot 命令は header_operations.py (あるいは拡張版)に従って 520 というオペコードに変換されています。これをテキスト・エディタや既製ツールなどを使って探す場合、他の 1520 のような命令と区別するために、520 の前後に半角空白を付けるとよいでしょう(この場合は正規表現「¥<」「¥>」を使うまでもありません。もちろんテキスト・エディタ Vim のように「*」というワンキーだけで「¥<」「¥>」を付けて検索してくれるなら使わない手はありません)。しかし、それだけでは代入値、比較値、スロット番号などにも検索ヒットしてしまいます。この命令は常に 3 個の引数を伴うはずなので、前後に空白を付けた「 520 3 」を検索すると、(完全とは言い切れないものの)目的通りの対象に かなり絞り込めるはずです。更に絞り込む必要がある場合は、正規表現で手前や後続の数字に条件を付けるなどして検索します。

txt 検索も万能ではない

お察しの通り、オペコードが 1 桁とか 2 桁の命令を txt 内から検索する場合は、絞りにくくなります。特にオペコード 1 の call_script 命令には不定個の引数を指定できるので、他の数値と完全に区別するのは かなり困難です。しかし事前にその MOD の全 txt ファイルから、両端に空白を指定した正規表現「 1 [6-9] 」と「 1 [1-9][0-9] 」を検索して全く見つからなければ、その MOD では call_script の引数が 5 個を超えることは絶対に無い、と判ります。そうすれば正規表現で「 1 [0-5] 」のように引数が 5 個以下の call_script 命令を検索することで、かなり絞り込めるはずです。目視検索の場合、慣れてくると前後を眺めて それが call_script 命令かどうか見当がつきます(目視でなく、完全な検索ツールを作る場合は、ブロック内の全命令について、少なくとも命令長を順に読解していく必要があります)。

また、真偽を反転する neg を指定した判定命令も、元のオペコードを探しにくいはずです。neg は header_operations.py に 16 進で 0x80000000 と定義されていて、10 進で 2147483648 です(この変換は Windows に付属の「電卓」を「プログラマ」「Qword」モードにすると できます。ただし 16 進 16 桁を扱う時は符号の挙動に注意)。これが元のオペコードとビット毎論理和されますが、全オペコードは 16 進の 3 桁程度に収まるので、(符号無しという前提があれば)実質的にはビット毎論理和の代わりに加算でも同じです。だから例えばオペコード 33 の is_between 命令を反転した is_between|neg のオペコード「2147483648 | 33」は「2147483648 + 33」と同じであり、2147483681 です。つまり、txt ファイルから is_between 命令と is_between|neg 命令に相当するオペコードを両方とも見つけたければ、33 と 2147483681 の両方を探す必要があります。

もしテキスト・エディタや既製ツールを使って txt 目視検索をしていて、(限られた時間と気力では)対象だけに絞り切れず「ハズレ」にもヒットしてしまう場合、仕方なくハズレを見分けながら探すことになりますが、ある程度は経験でカバーできる場合があります。

ここまでで「こりゃ手におえん。やめだ。」と思ったかたも、正規表現や環境整備に関する項を ざっと拾い読みしてみて下さい。もやもやした点と点がつながって一気に視界が開け、いつのまにか既製 MOD の品質向上の一翼を担うテスターとして活躍しているかも?!


環境整備

txt ファイルの「検索」にも「読解」にも言えることですが、環境によって作業効率が大きく変わります。極端な話、例えば Windows に付属の「メモ帳」とか「コマンド プロンプト」のような貧弱で低機能の環境で検索や読解をしよとしても効率が上がらず、疲れて長続きしないはずです。あるいは Visual Studio のエディタを使いこんでいるかたも あるかもしれませんが、この MOD 開発システムが出力した txt ファイルを扱うのに最適な機能が揃っているようにも見えません。(注: 障害などにより他に選択の余地が無いようなかたの都合を、排除する意図はありません。他の記述でも同様です。)

txt の読解や改善コード案作成といった作業では、数えきれないような検索とコマンド再利用が必要になります。どんなに使い慣れたテキスト・エディタや既製ツールも、最善の選択肢でない可能性があります。それらを一旦 頭から捨てて、この作業に特化した最適の環境を思い描き、それを探してみることをお勧めします。ポイントとなるのは、本来やりたいこと(目的)への思考を妨げず、いかに少ないキー入力回数で結果を得られる環境か、です。

また、検索や読解のためのツールを自作する場合でも、例えば C/C++ 系のような冗長コードの多い言語で書くと維持が煩雑になるし、一般向けにバイナリを配布しようとしても信用されない場合があるでしょう。それに比べると、Unix ライクな環境で動くシェル・スクリプトなら冗長コードが少ないしテキスト・ファイルなので、核心の創作に専念でき、信用も得易いでしょう。ただし、Windows からほとんど切り離された仮想環境だと、OS 間を行き来するのが面倒です。そうでなく、Windows のファイルにアクセスしやすいものがお薦めです。


お薦めの環境

それぞれのかたに「好み」や、長く長く使い込んだ「経緯」などもあるでしょうから、以下は参考まで

上述の「キー入力数が少なくて済む」「冗長で不透明なコンパイル言語を不使用」「Unix ライクな仮想環境のうち Windows のファイルを容易に扱えるもの」という要件をよく満たすような環境として、私(訳者)がお薦めなのは、次のような組み合わせです。

[訳者のお薦め] Cygiwin(など)+ bash シェル + vi コマンドライン・モード。そしてテキスト・エディタも Vim など vi 系に揃えると、思考がシームレスに働いて効率が上がります。いずれも無料です。

Cygwin の代替として、mingw-w64 や Git BASH という選択肢もあるかもしれませんが、それぞれ得意分野(gcc による開発とか Git とのやりとり)に関連した機能を持っているので、Cygwin より かさばるかも(不確か)。

bash を常に vi コマンドライン・モードにするには、自分の home にある .bash_profile に set -o vi を書き加えておきます。vi コマンドライン・モードや Vim では、コマンドの多くが 1~2 文字程度の非常に少ないキー入力数で指定できます。例えばコマンド・モード開始が「Esc」、ファイル内やコマンド履歴の検索開始が「/」、カーソル位置の語を検索が「*」、次検索が「n」、カーソルやコマンド履歴の上下左右移動が「h」「j」「k」「l」、次の語へのカーソル移動が「w」、語の置換が「cw」、入力開始が「i」「I」「a」「A」、繰り返しが「.」などなど。詳しくはヘルプや公開されている各公式ドキュメントを参照下さい。

(注: ここで「キー入力」を手などの運動によるキーボード入力に限定する意図はありません。他機器や音声などによる入力など、都合や状況に応じて読み替えて下さい。他の項でも同様です。)

正規表現(検索)

正規表現(regular expression, regex, regexp)は、いくつかの文字に「メタ文字」として特殊な意味を持たせた検索の仕組みです。既存の様々なテキスト・エディタ、比較ツール(WinMerge, ...)、シェル環境下のテキスト処理コマンド類(sed, awk, ed, ...)、言語エンジン(Python, JavaScript, ...)などに備わっています。ただし仕様は、それらの間で微妙に異なるので、思い込みで使い間違えないよう要注意です。

ここでは、それらの違いやバージョンを含めて網羅的には書ききれないので、詳しい仕様については、各公式サイト、関連書籍、一般の比較サイトなどで確認して下さい。

Warband MOD の .txt ファイルの「検索」や「置換」をするのに、正規表現を知っているといないとでは「雲泥の差」どころか「可能と不可能の差」が出ます。「txt 検索」と「正規表現」が揃って初めて両輪が回るので、不慣れなかたは どんどん実践して身につけて下さい。検索対象(見つけたい文字列)を見つけるための うまい正規表現を見つけるたびに、難解なパズルを解いた時のような爽快感を得られるはず。

そして .txt だけでなく、全ソース検索、ソース内検索、翻訳作業のスクリプト化(による変更行管理)、翻訳結果の検証、シェル下で使う自作ツールの開発などでも、正規表現は威力を発揮します。


練習課題: 正規表現による検索

troops モジュールで定義された兵種には、各レコードが示す兵種毎に「兵種スロット」が備わっている。スロットの実体は、複数の数値を要素として格納できる配列であり、0 から始まる番号で その要素(格納場所)を指定する。ただし、どの番号の要素に何を格納すべきかは、Warband エンジンが(ほぼ)定めている。

兵種スロットを読み書きしたり値と比較したりする命令が用意されていて、例えば 読み出すための命令は troop_get_slot であり、header_operations.py 拡張版(または MOD 開発システムのオリジナル)で そのオペコードが 520 と定義されている。この命令は常に「格納先」「兵種 ID」「スロット番号」という計 3 個のオペランドを伴う。

今、Native(または既製 MOD)の .txt ファイルから、troop_get_slot 命令で何らかの兵種(兵種は不問)の兵種スロット 4 番を読み出している箇所を見つけたいとする。

  1. テキスト・エディタなどを使って、試しに(正規表現でない)半角空白で挟んだだけの「 4 」を Native(または既製 MOD)の .txt ファイルから検索し、troop_get_slot 命令と無関係の無数の箇所にもヒットしてしまう(見つけてしまう)ことを確認せよ。
  2. 同様に半角空白で挟んだ「 520 」を検索し、やはり troop_get_slot 命令以外にヒットしてしまうことを確認せよ。
  3. 適切な正規表現を作り、troop_get_slot 命令で兵種不問の兵種スロット 4 番を読み出している箇所に絞って見つけられることを、テキスト・エディタやシェル上で確認せよ。
  4. 応用として、オペランドを 1 個または 3 個とり得るの try_for_agents 命令(オペコード 12)を探すための正規表現を書き、確認せよ。

注: ここでは多少の不完全さは許容される。オペランド数が一定でオペコードが 3 桁以上ある troop_get_slot のような命令では さほど長くない正規表現で「ほぼ完全に」絞り込めるはずだが、オペランドが不定個の命令やオペコードが 2 桁以下の命令では不完全の度合いが増す。完全を期そうとするとブロックの先頭から全命令のオペランド数を追跡する必要があるが、多くの場合は そうせずに「次を検索」するなどして目的外にヒットしたものを読み飛ばしながら目視で探すほうが効率がよい。また、その MOD で「オペランド値のとり得る範囲」や「オペランドに指定される定数や変数の状況)などを事前に(別の正規表現で調べて)知っておけば、それに反するものを除外するよう正規表現に書き加えて検索することで、絞り込みやすくなる。

Vim や vi モード・コマンドラインでの正規表現の例

テキスト・エディタ Vim および bash の vi モード(コマンド入力)で使えるコマンド例をいくつか示します。Vim の場合の詳しい仕様は「:h regex」で表示できます。先頭の「/」は Vim や vi モードのコマンドラインでの検索開始コマンドなので、他のツールでは相応のものに読み替えて下さい。


「close_window」の後ろに任意の一文字と 2 桁ちょうどの数値が続く箇所(末尾に空白を指定している)。

/close_window.[0-9][0-9] 

「{」の後ろに「:」でも「}」でもない文字が 0 個以上続いてから「}」が現れる箇所。

/{[^:}]*}

「star」の後ろに任意の文字が 0 個以上続いてから「pre」が現れる箇所。

/star.*pre

(conversation.txt 用)話者に anyone つまり 0xfff(のみ)が指定されている行(末尾に空白を指定している)。(行頭の「dlga」の後ろに空白以外の文字が 1 個以上続き、さらに空白 1 個をはさんで「4095」と空白がある行)

/^dlga[^ ]\+ 4095 

(conversation.txt 用)話者に少なくとも plyr つまり 0x10000 = 65536 が指定されている可能性が高い行(末尾に空白を指定している)。ただし、multi_line = 0x80000 のような より大きな数のフラグが併用されていると検出できない。(行頭の「dlga」の後ろに空白以外の文字が 1 個以上続き、さらに空白 1 個をはさんで、「6」で始まると 5 桁の数がある行)

/^dlga[^ ]\+ 6[0-9][0-9][0-9][0-9] 

(Vim で troops.txt を編集時)ID の行だけ残し、他行を削除する。

:%g!/^trp_/d

(翻訳者が csv ファイル内を検索時にも便利。)全角文字が含まれていない行。

/^[\x01-\x7E]*$

txt のレコード構造と命令オペランド内レコード番号

MOD 開発システムの各モジュールに(Python 言語のリストやタプルの要素として)書いた「レコード群」が、疑似コンパイルされて .txt ファイルとして出力される時、各 .txt ファイルの先頭に 0~2 行程度のヘッダが追加されます。そこにはレコードの数やバージョンが表示されます。

一例として、この図は strings モジュールのコンパイル結果、strings.txt の冒頭付近です(左端の行番号はエディタが表示したもので、ファイルには含まれていません)。このモジュールではヘッダが 2 行あり、その下にレコード 0 があります。このように、あらゆるレコード番号は 0 から始まるインデクスとして扱われます。このモジュールでは、行頭にある str_no_string のような連続した文字列が ID です。レコード番号が使われる場所は、txt 内のコード・ブロックにある命令のオペランド(の下のほうの何バイトか))などです。

(ID と レコード番号の対応は、コンパイルで生成された ID_strings.py でも定義されています。ただしエンジンは参照しないので、手でどこかの txt 内のレコードを実験的に増やす場合に、ID_xxx.py まで追従させる必要はありません。)

レコード同士の区切りはモジュールごとに異なり、「1 行で 1 レコード」というのもあれば、複数行のものもあります。

例えば、どこかの処理で display_message 命令のパラメータに(MOD で登録済みの)文字列を引用符付きで「"str_yes"」と指定したとします。

(display_message, "str_yes"),

この登録済み文字列のレコード番号は(上図で示すように)2 番なので、この display_message 命令がコンパイルされた時に、その 2 に対し、登録済み文字列であることを示す 8 バイト長の接頭子 0x0300000000000000 が ビット毎論理和(実質は加算)され、0x0300000000000002 つまり 10 進数の 216172782113783810 がオペランドとして(命令を書いたモジュールに対応した)txt ファイルに出力されます。

1106 1 216172782113783810

説明のため前後の命令などを省略しています。1106 が display_message 命令のオペコードで、その後ろの 1 はオペランドの個数です。txt からソース・コードへ戻すことができる(可逆である)ことがお分かりでしょう。

Warband エンジンは この 1106 という命令が現れた時に、その後ろに 1 個のパラメータが指定されていることを読み取ることができます。そして その数字が 8 バイト長の情報であり得ることを知っていて、その先頭バイト(最上位バイト)が 0x03 であることから、それが登録済み「文字列」であることを知るのです。後は最下位の数バイトからインデクス「2」を取り出し、strings.txt の 0 番レコードから数えて 2 番を参照するのです。

数値レジスタも、上記のようなレコードのインデクスの場合と同様で、「接頭子+レジスタ番号」の値が txt に出力されます。一方、文字列レジスタと位置レジスタは(上表に無いことからも判るように)コンパイル時に接頭子が付けられず、s0, s1, s2, ... は 0, 1, 2, ... だし、pos0, pos1, pos2, ... も 0, 1, 2, ... です。なお、display_message 命令や他の命令のオペランドに数値レジスタ(reg1 など)を指定した場合、そのレジスタの内容がどう扱われるか、については、実際に試すなどして確認してみて下さい。

登録済み「文字列」と「数値レジスタ」の他にも、各レコード参照用の前置子があります。別掲の「オペランド早見表」にまとめました。


練習課題: 実行順の追跡

  1. Native などの conversation.txt を見つけてコピーを作り、オリジナルのほうを conversation_org.txt などに改名、コピーしてできたほうを conversation.txt に改名せよ。
  2. 村のシーンで、村民の一人に話しかけた時の相手のセリフ(訳例「こんにちは、...」など)を日本語ファイル languages¥ja¥dialogs.csv から見つけよ。
  3. その行と同じ ID を持つ行を conversation.txt から見つけよ。
  4. その行内の、条件ブロック、会話文、結果ブロックを見つけよ。
  5. 後述される「txt のレコード構造と命令オペランド内レコード番号」の説明を読んだ上で、別途示した「オペランド早見表」を使って、quick_strings.txt 内のクイック文字列 ID、qstr_Use_Blunt_Weapons (訳例「鈍器を使え」)と qstr_Use_Any_Weapon (訳例「全ての武器を使え)の 10 進数表記をそれぞれ求めよ。
  6. 上記 conversation.txt の行内を次のように書き換えよ。条件ブロックの命令数を +1 し、ブロックの先頭に display_message 命令を加え、上記 前者のクイック文字列を表示するよう改変せよ。
  7. 同様に結果ブロックの命令数を +1 し、ブロックの先頭に後者のクイック文字列を表示する display_message 命令を加えよ。
  8. 実際に Warband でプレイし、村民に話しかけた瞬間に、画面左下に「鈍器を使え」「全ての武器を使え」に相当するメッセージが表示されることを確認せよ。(メッセージが消えた後でも、会話を終了し、Ctrl L キーで出る画面で確認できる。)
重要: dialogs モジュールには、会話相手とプレイヤー選択肢、それぞれのテキストに「条件ブロック」と「結果ブロック」を指定できますが、会話相手のテキストのほうは、両ブロックが実行された後に表示される、ということに要注意です。上記の例では この実行順序を確認することができます。(ただし、結果ブロックの先頭に「偽」になる命令(例えば 31 2 0 1)を置いてみると判るように、結果ブロックの成否が会話開始に影響しないことにも注意して下さい。)

練習課題: バグ対策と確認

Native には下記のような、練習に打ってつけの既存バグがある。ソース・コードを見ずに .txt だけを使い、バグの再現を加速するコードを加え、対策、確認の作業をせよ。

このバグの(本来の)再現手順は次のようなもの。まず、独立勢力として城か街を 2 つ奪取する。宮廷(court)は最初に取ったほうの城か街にあるはず(大臣が立っている)。その城か街の守備兵を(0 人などに)減らして無防備にする。他の安全な場所で待ち、敵にその無防備のほうを攻撃させる。そこが陥落すると、その旨が表示される。ほどなく(ゲーム内の数時間後に)、宮廷が強制的に移転された旨が表示される。しかしそのメッセージ作成処理に誤りがあり、移転先の名前が空だったり、移転前と同じだったり、というように不特定の文字が表示される。

くどいようだが、ソース・コードを見てはいけない。課題は下記の通り。

  1. 試験を加速するため、.txt を改変してキャンプ・メニューかレポート・メニューに選択肢を加え、マップ上で随時 隊員数を 400 人ほどに増やせるようにしておく。同じく持ち金も増やせるようにしておく。(チート・モードは不使用。下記に注記。)
  2. それを使って新規ゲームから「ゲーム内 5 日」程度までに城か街を 2 つ奪取し、「症状が出る直前の状況」を意図的に作ってセーブし、そこから容易に症状が再現できるようにしておく。
  3. .txt ファイルを読み、display_message 命令を適宜加えるなどして、原因箇所を特定する。
  4. .txt ファイルを適切に書き換えて、問題に対処する(つまりバグを仮対策する)。
  5. 正しく対策できたかどうか、セーブ・ポイントからプレイして確認する。
  6. 余力があるなら、変更した一連の命令が含まれるコード・ブロック全体を手で逆アセンブル(逆コンパイル)し、try ブロックなどの位置や階層、あるいは処理全体が正しいことを目視確認する。

開発やデバグの心得

上の例などで指示が「新規ゲームから」なのは、起こしにくい状況も「加速試験」により短時間で再現させる練習になるからです。

再現させずに対策だけするなどは論外。再現させないことには、問題の症状が完全に安定して起きるのか、それとも非同期に発火したトリガーなどの影響を受けるのかが わからず、対策が不完全になったり、対策したつもりが悪化させただけになったり、あるいは どこが問題点(バグ)なのかを誤解したままになることも。

充分なテストをしないなど、もちろん言語道断。対策した部分の確認だけでなく、従来動作に悪影響が無いかどうかの無害確認などは省略してはいけません。コーディングにどんなに細心の注意を払っても、テストしなければ、悪化や、新規バグ作り込みを必ず招きます。そして そんなことを繰り返していたら、テストやバグ管理の技術も、問題に気づく能力も向上しません。もしテストしていない自分の提案コードを既製 MOD の開発者たちに見せた後、自分や誰かが誤りに気付いたら、自分の恥をさらすだけでなく、相手に大混乱を与えます。

図は、私(訳者)が とある既製 MOD のバグを見つけ、開発者に「提案コード」とともに示した「組み合わせテスト」結果(部分)です。あらゆる MOD 開発作業や デバグ作業で ここまでする必要は無いかもしれませんが、このようにテストの過程や結果を共有することで、追試験や これを足掛かりにした更なる検討が容易になります。

チート・モードについて

この欄の「練習」では試験を加速する目的で「増兵」や「増額」の機能追加を促しましたが、それを Warband 起動時 小画面の「チート・モード」で代替してはいけません。そのモードでは MOD に書いてある様々な処理が「横道に逸れる」ので、予期せぬ区間でレジスタが壊されたり、非同期な事象同士のぶつかるタイミングが変わったり、といった擾乱が入いり込みます。それにより通常モードでの症状が隠されたり不安定になったりし、「直したつもりが直ってない」とか「無害のつもりが、新たに副作用やバグを埋め込んだ」ということが起きる確率を増やしてしまいます。

だから、デバグ作業全般に言えることとして、(チート・モード時の処理自体を対象としたデバグを除き)チート・モード使うのは避けるべきです。

練習課題: モジュール毎の違い

以下、Native の .txt を使ってもよいし、既製 MOD の txt でもよい。ここでは構造を概観するだけであり、命令を詳しく調べる必要は無い。

  1. menus.txt をテキスト・エディタで開き、次のことを概観せよ。menu_ で始まる行がメニュー本体の ID を示していること。その行末の数値が(そのメニューの)選択肢の数を示すこと。その行に続く、行頭が半角空白で mno_ が 1 つ以上含まれる行が選択肢の記述であること。選択肢同士の区切りが「2 個の半角空白」と「.」と「2 個の半角空白」の続いたものであり、終端には「半角空白 2 個」と「.」と「半角空白 1 個」があること。残る区間が「条件ブロック」「テキスト」「結果ブロック」であり、半角空白 2 個で区切られていること。両ブロックでは先頭の数値が命令の数を示していること。(実は このファイルに限らないが)テキストには半角空白が含まれておらず、実行時に Warband エンジンがアンダースコア「_」を空白に置き換えるらしい、ということ。
  2. mission_templates.txt をテキスト・エディタで開き、次のことを概観せよ。ID の行と説明テキストの行に続く行の先頭に数値があり、その数が示す行数だけ何かが続いていること。その後ろに単体の数値があり、その数が示す行数だけトリガーが続いていること。トリガー行のうち、負数で始まるものがあり、その値が(MOD 開発システムの)header_triggers.py で定義されていること。先頭が負数かどうかにかかわらず、先頭から(1 から数えて)4 番目の数値が、その後ろの命令の数を示していること。
  3. presentations.txt をテキスト・エディタで開き、次のことを概観せよ。ID の行の行末に書かれた数値が、それに続くトリガー行の数を表わしていること。各トリガー行の先頭の負数が(MOD 開発システムの)header_triggers.py で定義されていること。その負数に続く数値が、その後ろの命令の数を示していること。
  4. parties.txt をテキスト・エディタで開き、次のことを概観せよ。ID が、先頭が空白の行の(1 から数えて)4 項目目にあること。行の後半に座標らしきものがあること。全ての街、城、村の名前を定義するレコードが含まれていること。

他のモジュールも、読み方のコツに大差は無いはず。他の課題中で ざっと調べた scripts.txt や dialogs.txt の理解と合わせて、余力と興味があれば全ての .txt ファイルを眺めておいてもよいかも。もし(MOD 開発システムの)ソース・コードと(出力の)txt の完全な対応関係を知りたければ、process_*.py を読み解くほうが早いかも。ただし それは必要に迫られてからでも遅くないはず。

実験前後のバックアップ

実験的に .txt ファイルを変更する前に、元のファイルをバックアップ(退避)しておくとよいでしょう。後で復旧するためだけでなく、比較したくなることがあるからです。

Native のデフォルト状態から実験を始めたければ、Warband の Mudules にある Native をフォルダ丸ごとコピーしてもいいし、あるいは そうせずに Native 下の txt ファイルを変更する前に個々に複製し、オリジナルのほうを xxxxx_org.txt などに改名しておいてから作業する方法もあるでしょう。また、既に MOD 開発システムで大規模な作り込みをした後なら、ソース・コードの変更やコンパイルを一旦 封印して、コンパイル結果の .txt を使って実験することもできるでしょう。いずれにせよ、実験のため書き換えた .txt を残したければ、復旧したりコンパイルで上書きする前に、実験結果のほうのファイル名に簡単な説明や日付を付けるなどしておくとよいでしょう。

なお、txt だけを扱うなら、Python(の言語エンジン)も MOD 開発システム一式も不要で、header_operations.py 拡張版だけ入手すれば実験はできるでしょう。

セーブ・スロットとスクショ

Warband をプレイする時のセーブ枠(セーブ・スロット)は 9 個しかないので、プレイヤーによっては無駄なセーブを避け、ゲーム内日数で何十日も間隔をあけることもあるでしょう。しかし、MOD のデバグや再現テストに専念している時には、いつでも(ゲーム内)数日前とか数時間前の状態に戻せるよう、とにかく こまめにセーブしたくなることがあります。セーブ・スロットがたちまち満杯になるので、セーブ・データ *.sav が格納されるフォルダを その都度 丸ごと圧縮するなどし、そのファイル名に説明や日付を付けて安全な場所へ退避するとよいです。

Warband のセーブ・フォルダは、自分の Windows ユーザの「ドキュメント」フォルダを探すと見つかります。

調査の都合で「新規ゲーム」から始めたくても、セーブ・スロットが満杯だと新規ゲームを選択できません。その場合、セーブ用ディレクトリにある .sav ファイルを 1 個以上 改名することで退避するか、または上述のようにフォルダを丸ごとバックアップしてから .sav ファイルを削除するなどするとよいでしょう。

Warband のスクリーン・ショット(画面コピー, スクショ)は Ctrl Insert キーで撮ることができ、やはり「ドキュメント」下を探すと画像の格納場所が見つかります。デバグ用に加えた display_message 命令が画面左下に出すメッセージを(消える前に)記録するのに便利です。メッセージが消えた後でも、Ctrl L キー(または [q]-[メッセージ])で開く画面に数十行だけ残ります。もちろんメッセージの記録だけでなく、開発者への状況説明にも重宝します。

画像はファイル・サイズが比較的大きいので、開発者のフォーラムなどへ画像を報告する際は、(エチケットとして)相手のストレージ容量を決して浪費しないよう最大限の工夫しましょう。さもないと、MOD の寿命を縮める遠因になりかねません。「どうせ開発者だってゲーマーだろ。楽しくやろうぜ」みたいな身勝手な思い込みで相手に混乱や費用や時間を強いるのは、絶対にやめましょう。画像なら例えば、自分で用意したフリーのファイル置き場に置いた上で、その URL を示したり 相手サイトの BB コード([img]~[/img] など)で表示するのが一番いいでしょう。どうしても相手のサイトへ画像を添付する場合でも、見映えなど二の次にして、解像度を極限まで落としましょう。画角も、撮ったままでなく、不要範囲を切り捨てましょう。また画像内に相手 MOD のバージョンを記入したほうが、後で その問題が対策済みか否かを皆が判断でき、お互いに安心できます。

スクショは、撮る度に Windows ユーザ名を含んだ格納先フルパスが表示され、短い間隔で撮ると それが映り込みます。そのような画像を他人に見せたり公開するのはセキュリティ上 危険です。これは他のツール類のスクショにも言えることです。写り込んだ部分をカットするとか黒塗りにするなどの処置を施すべきです。

オペランド早見表

意味0 番の 16 進表記0 番の 10 進表記
数値レジスタ010000000000000072057594037927936
グローバル変数0200000000000000144115188075855872
文字列0300000000000000216172782113783808
物品0400000000000000288230376151711744
兵種0500000000000000360287970189639680
勢力0600000000000000432345564227567616
クエスト0700000000000000504403158265495552
集団テンプレート0800000000000000576460752303423488
集落・施設0900000000000000648518346341351424
シーン0a00000000000000720575940379279360
ミッション・テンプレート0b00000000000000792633534417207296
メニュー0c00000000000000864691128455135232
スクリプト0d00000000000000936748722493063168
粒子システム0e000000000000001008806316530991104
シーン小物0f000000000000001080863910568919040
音声・効果音10000000000000001152921504606846976
ローカル変数11000000000000001224979098644774912
マップ・アイコン12000000000000001297036692682702848
スキル13000000000000001369094286720630784
メッシュ14000000000000001441151880758558720
プレゼンテーション15000000000000001513209474796486656
クイック文字列16000000000000001585267068834414592
楽曲17000000000000001657324662872342528
部分画(紋章)18000000000000001729382256910270464
アニメーション19000000000000001801439850948198400
  • 脚注と出典:
  • [1] kalarhan, Modding Q&A.
  • [2] この例と説明は、Dalion 氏の説明で解決される TheCaitularity の問題から引用。Modding Q&A.
  • [3] Caba`drin, Modding Q&A.
  • [4] この時、質問者 Bustah 氏のところで起きていた問題は kalarhan 氏による説明で解決したらしい。Modding Q&A(訳注: でも 1073741855 は本文にある通り this_or_next|eq なので、それが「認識できない」事態になる理由を聞きたい。)
  • [5] kalarhan, Modding Q&A.