スクリプト群は、スクリプトを要素とするリスト。その中の各スクリプトには次のような 2 つの項目がある。
(1) スクリプト ID。参照時は接頭子「script_」を付ける。
(2) 命令ブロック。正しい「命令」の羅列をリストで指定。header_operations.py を参照のこと。
訳注:
意義(長所)や目的
スクリプトを使う最大の目的は、繰り返し現れる(重複した)コードを「サブルーチン」として共通化し、複数箇所から呼び出すことにより、保守性や可読性を上げることです。また、たとえ一度しか現れないコードであっても、他機能との境界が明瞭な(クローズした)区間をスクリプトに分離して呼び出すことで、呼び出し側のコードの可読性を向上することもできます。

なぜ「スクリプト」という名なのか
この script という呼び方を、Warband 製造元が なぜ使ったかは(訳者には)わかりません。一般の OS 上で使うスクリプト・ファイルのように、何かひとまとまりの(ドラマの「脚本, script」のように境界が明瞭な)処理をテキスト・ファイルに書いておいて、コマンド・ラインからファイル名で呼び出したり、コマンドに渡したりするのを連想するからかもしれません。一般の高級言語のような「関数, functions」や「プロシジャ, 手続き, precedures」、あるいは単に「サブルーチン, sybroutines」という名でも良さそうに思えます。製造元の公用語が英語ではないことと関連しているかもしれませんが、憶測の域を出ません。
スクリプト以外のサブルーチン置き場
一般の高級言語の多くは、複数のソース・ファイルを「対等」に扱うことが可能で、互いに他のソース・ファイルにあるサブルーチンを(参照することを事前に明示するなどすれば、また その程度や そうすべきかどうかは別として)呼び出し可能です。
一方この MOD 開発システムでは、scripts モジュール以外で(開発者が呼び出したい箇所で随時 呼び出せるような)サブルーチンを置ける場所は、いくつかあるものの限定的だし、制約があります。
例えば、jump_to_menu 命令で開始するメニューで何も表示せずに処理を書いておくとか、start_presentation 命令のプレゼンテーションで同様のことをするとか、3D シーン内のドアなどに設定した passage の示す番号(0 から)に応じて特定メニューの選択肢 mno_xxxx を実行する、などです。また、「呼び出し」の解釈を広げるなら、単純トリガーやミッション・テンプレート発動中の ti_on_presentation_run トリガーでグローバルな変数やスロットの状態を定期的に読み出して処理し分ける、という間接的なものも、ある種の呼び出しと言えるかも。
つまり まとめると「サブルーチンは通常は 全て scripts モジュールに置け」ということのようです。
スクリプトの呼び出し命令、引数(ひきすう)、スコープ
スクリプトを呼び出す命令はただ一つ、call_script です。この時、16 個までの引数を渡すことができます。呼ばれたスクリプト(つまりサブルーチン)側では、store_script_param_1 命令や store_script_param 命令などを使って その値を取り出します。
各種変数のスコープ(どこから参照可能か)は、他のコード・ブロックの場合と同様です。例えばローカル変数に設定した値はスクリプトのコード・ブロックの終了とともに捨てられるので、呼び出し側への悪影響を意識せずに使えます。そのことがスクリプトによるサブルーチン化(あるいは部品化)を意義あるものにしています。他ブロックと同様に、ローカル変数に何も値を設定せずに参照すると不特定のゴミになるので、必ず設定を先にします。その他の数値レジスタ、文字列レジスタ、位置レジスタ、グローバル変数、各種スロット値などは、スコープがグローバルなので、スクリプト内で何か値を設定し、呼び出し元へ返った時に、値が保持されます。
階層、再帰呼び出し
スクリプト内から他のスクリプトを(孫、ひ孫、...と階層的に)呼ぶことができます。例えば mod 開発システム v1.171 では、複数のスクリプトが move_headquarters_flags というスクリプトを呼び出し、その中から更に(2 箇所で) calculate_flag_move_time というスクリプトを呼び出しています。
スクリプトが自分自身を(再帰的に)呼び出すこともできます。例えば mod 開発システム v1.171 では、スクリプト clear_party_group の中から自分自身である clear_party_group を呼んでいます。このような再帰呼び出し(recursive call)が当てはまる典型は、一部の既製 MOD に実装されている「兵種ツリー」(兵種の昇格経路を図示する)画面のように、枝分かれした木構造の可能性(ノード)を根元から末端まで同じルールを繰り返して検索(探査)したい場合です。当然ながら無限ループでデッドロックしてしまわないよう、厳重な考慮とテストが必要です。
スクリプトの終了
スクリプトの途中や最後で(強制的に)呼び出し元へ戻るための return 命令のようなものは ありません。この MOD 開発システムの あらゆるコード・ブロック(通例では [ ] でくくった Python リストの中に、処理コードを要素として並べたもの)は、「判定命令」の結果が偽になった時点、または失敗可能スクリプト(名前が cf_ で始まる)が偽を返した時点で、そのブロックを終了します。スクリプトでも同じで、その機構をそのまま利用するし、それ以外に方法がありません。この終了動作は、MOD 開発者が意図しようとしまいと起きるので、スクリプトに限らずコーディング時やテスト時には常に要注意です。
判定命令や失敗可能スクリプトについては、この下の本文で後述されるほか、「判定命令」の項にも説明があります。
レジスタ退避/回復命令は無い
読者の皆さんがもし ワンチップ・マイコンや x86 といった CPU アーキテクチャの命令体系(つまりアセンブリ言語)の概要と、それをアセンブル(高級言語で言うところのコンパイル)して 16 進数の機械語(マシン語)にする、ということを ちらっとでもご存じであれば、この MOD 開発用言語のソースコードと .txt 出力された数字の羅列を見て、すぐに類似点に気づくことでしょう。 というのも、この mod 開発システムでも似たような書式でソースコードにニーモニック(命令名)とオペランド(パラメータ)を書いておき、「疑似コンパイル」すると、オペコード(opcode)とパラメータが 10 進数で .txt に出力されるからです。
例えば、「assign, reg1, 3」という命令は(Warband のあらゆる MOD の どのバージョンでも)「2133 2 72057594037927937 3」に変換されて .txt へ出力されます *1。(ゲーム・エンジンが そのような数字の羅列しか理解できない、という点も CPU アーキテクチャを連想させます。少し異なるのは、MOD のほうの数字はテキストで、CPU はバイナリ値だという点です。)
世間の CPU アーキテクチャには サブルーチンの呼び出しと戻りの際にレジスタを(スタック つまり後入れ先出しの領域に)退避したり回復したりするための push 用の命令と pop 用の命令が、必ずと言っていいほど備わっています。しかし、この mod 開発システムには push/pop に相当する命令がありません。そして数値レジ、文字列レジ、位置レジともグローバルなので、サブルーチン内(スクリプト以外にも広義にはプレゼンテーションや単純トリガーなどを含めて)でレジスタを別の値に変更すると、呼び出し元へ戻った時もその変更が反映され、呼び出し前とは別の値になっています。
だから MOD 開発者は、保持すべきレジスタを誤って壊して呼び出し元に悪影響を与える、ということを皆無にしなければなりません。また、スクリプトによる直接の呼び出し階層だけでなく、上述のトリガーのように非同期で実行されるコードを意識した「スレッドセーフ, thread-safe」のような考慮も必要です。レジスタ類をスマートに退避回復する方法は無く、大別すると下記のいずれかに限定されるはずです。
- ゲームに登場させないダミーの「兵種」を用意し、そのスロットをスタック領域に見立て、数値レジスタの退避用領域として使う *2。呼ばれた直後にレジスタを退避、帰る直前に回復。文字列レジスタには使えない。位置レジスタも位置以外の要素に要注意。(再帰呼び出しやトリガーからの呼び出しが無い限り、スタック・ポインタをシミュレートする必要までは恐らく生じにくそう。なぜなら、この場合のスロットは「配列」に相当し、退避領域の先頭が明瞭なので。)
- 退避回復用のグローバル変数を用意しておく。呼ばれた直後にレジスタを退避、帰る直前に回復。文字列レジや位置レジについては上記と同様。(一見すると手軽な手法だが、配列のようにループ処理*3 できないので、つまらないコードが長くなり可読性が落ちる。また再帰呼び出しやトリガーからの呼び出しがある場合も この方法は使えないはず。)
- ソースコード規約を決め、スクリプトやプレゼンテーションやトリガーを含めた全モジュールで排他的にレジスタ類を使い分ける。特に文字列レジについては この方法しか無いはず。
- 上記の複合。
*1 この例で reg1 に相当する 10 進数の桁数が長いのは、64 ビットの情報を 10 進数で表わしているからです。これを 16 進数に変換するために Windows に付属の「電卓」の「プログラマ」モードを使う場合は、Qword を選んだ状態にする必要があります。このような数値レジスタの場合は問題ないですが、この電卓は 16 進数を 10 進数に変換した時に符号付き 10 進数として扱うので、有効桁は 63 ビットしか無いことに要注意です。)
*2 スロットは このように配列として使えますが、数値(各種 ID や各レジスタを指す数などを含む)しか扱えません。位置レジの値(固定小数点数)も退避可能かもしれませんが、回転値や拡縮値など、要素が多いことに要注意。また、訳者の知る限り、文字列レジスタの値(可変長)を退避できる先は文字列レジスタだけです。それには str_store_string_reg 命令でレジ間のコピーをするか、または str_store_string 命令などで扱う文字列に {s1} のような文字列を含めておいて文字列レジスタの値をそこへ展開させる、ぐらいしかないはずです。また、(ユーザに特殊な環境の導入を強いない限り)文字列の中から区切り記号を検索したり、文字列を分割する(高級言語に備わっている関数のような)方法も無いはずで、「カンマ区切りの文字列を作り、そこから切り出す」というような、テクニックも使えないはずです。
*3 もし数値レジスタの番号をループさせたい場合は、レジスタを表わす上述の巨大な 10 進数をローカル変数に入れて +1 させるなど。ただし、10 進数でなく 16 進数で指定したほうが可読性が少し上がるはず。
サブルーチン化の短所
[オーバヘッド]
短所の一つは、オーバヘッド、つまり 呼出し命令、引数受け取り命令、レジスタなどの退避回復処理といった本来不要の処理が必要になることにより、ゲームの実行が若干 遅くなることです。ただし大抵の場合、その「遅れ」は非常に軽微です。そして直感的にもわかるように、長いスクリプトほど相対的にオーバヘッドが目立たなくなります。だから、よほどの事情が無い限り、上述の保守性や可読性といった長所のほうを優先すべきに見えます。そうしなかったために MOD の品質がなかなか上がらず(バグが収束せず)ユーザの不平不満を買い続けるほうが、明らかに危険です。「バグ再現、調査、対策」という、開発作業のオーバヘッドの種を(わざわざ自分が気づきにくい所へ)次々と埋めていくよりも、サブルーチン化のオーバヘッドなど考えずに長所を選ぶほうが賢明(のはず)です。
[ソースコードの分散]
もう一つの短所は、ソースコードを分けることによる弊害です。ファイルをまたぐことで、コメントや変数や命令を検索したり、グローバル情報の変化などを追跡したりする作業が、少し煩雑になります。
しかし この分散の問題も、手持ちのテキスト・エディタや既製ツールで、正規表現による検索を使いこなせば、かなりの程度は補えるはずです。そして、ソースコードから検索するだけでなく、疑似コンパイルして出力された .txt ファイルを検索のほうが高効率の場合も多く、両者を瞬時に見分け、適宜 組み合わせるべきです。これら正規表現や txt 検索、それらを活かす Unix ライクな環境などについて、この web 文書の「モジュール開発システムを導入しよう」の訳注や、「MOD 開発で ありがちなエラー」の訳注でも説明しています。
エンジンによる固定的な参照
名前が "game_" で始まる既存のスクリプトがいくつかあります。(このページの本文でも簡単に説明されている通り)エンジンは それらが必ず存在する前提で、随時に呼び出します。だから、それらの名前を変えたり削除したりしては いけません。また、それらのスクリプトの戻り値としてエンジンが文字列を要求する場合に、MOD は set_result_string 命令の実行が必要になることがあります。このようなエンジンによるハード・コーディングについては、header_operations.py 拡張版のコメントも参照して下さい。
ゲーム・エンジン・スクリプト
ハード・コーディングされたスクリプト。 MadocComadrin, Modding Q&A
詳細に踏み込まず、内容の簡単な説明とチュートリアルとコード例のセクションへのリンクのみ。
game_get_troop_wage : 隊員への週給
game_get_join_cost : 雇用時の費用
game_get_upgrade_cost : 昇格時の費用
など。
game_get_skill_modifier_for_troop のいくつかの特殊な挙動。 cmpxchg8b, Modding Q&A と [WB] Warband Script Enhancer v3.2.0}
失敗可能スクリプト cf_x
名前が cf_(意味は can-fail)で始まるスクリプトが module_scripts.py 内にいくつかあることにお気付きかも (訳注: 他のモジュールから呼び出す場合は 接頭子 script_ を その手前に付けるので、script_cf_xxxx のような名前になります)。あるいは、モジュールをコンパイルする際に次のような警告が出たことがあるかも。
WARNING: Script can fail at operation #x. Use cf_ at the beginning of its name: some_script_name
(訳注: 訳すと、「警告: オペコード x の命令の実行に失敗する可能性あり。スクリプト名「xxxxx」の手前に cf_ を付けて下さい。」)
この MOD 開発環境では、結果判定のある一部命令が実行された時、結果が true なら実行中のブロック内の処理を続行し、false ならブロックを終了します(ここでブロックとは、try ブロック、条件ブロック、結果ブロックなどです)。cf_ 付きスクリプトは それと同じことを call_script 命令にさせます。つまり、cf_ 付きスクリプトを call_script 命令で呼び出し、その処理が途中で失敗した場合に、call_script が false を返すようになります。 [1]。
最上位レベルのスコープ
| 第 2 レベルのスコープ
|
(try_begin)
(call_script, "cf_something"), # true なら次行を続行、false なら else_try へ。
(assign, ":true", 1),
(display_message, "@ololol"),
(else_try),
(assign, ":true", 0),
(try_end),
try ブロック内ではなく、スクリプトの最上位スコープ内でスクリプトを失敗させる条件付き命令があるので、この警告は推奨事項と見なすことができます。だから、その命令によってスクリプトが失敗した場合、call_script 命令がこのスクリプトを呼び出すスコープも失敗します。 [2]。cf_ 付きスクリプト呼び出しは、複雑な条件判定を行なう独自の条件判定用関数を作るのに便利です。 [3]
(訳注: 上記前半は原文の主旨が不可解。脚注 2 の説明も参照のこと。前半の原文は「You can read the warning as a recommendation, since you have a conditional operation that would cause the script to fail within the top level scope of the script rather than inside a try block. Therefore, if that operation causes the script to fail, the scope where the call_script operation calls this script would also fail.」)
下記はより複雑な条件判定の例です。cf_ 付きのスクリプト呼び出しは this_or_next 命令の一環として使えます。
例 1:
(this_or_next|eq, 1, 0),
(call_script, "script_cf_eq_1_1"),
例 2:
(this_or_next|call_script, "script_cf_eq_1_1"),
(eq, reg1, 0),
例 1 は、cf_ 付きスクリプト呼び出しを後ろに置いているので、うまく機能します。しかし、例 2 は意図通りにならず、失敗するか、通常の call_script 命令と同様に続行されます[4]。
(訳注: つまり、cf_ 付きスクリプトを呼び出す call_script 命令に this_or_next を直接併用してはいけない、ということらしく、理由説明は見当たらず。this_or_next 自体の使い方については、この web 文書のトップページ前半 導入部「開発言語の書式」の、「論理演算 AND と OR」と『「AND の OR」か「OR の AND」か』の項に説明があり。)
前述の警告メッセージの話に戻ります。これに対処する方法として、今のところ 2 つの方法が示されています。一つは、スクリプト名の前に cf_ を加えて「失敗し得る」ようにすることです。これは、単に警告メッセージを非表示にしたいのではなく、警告メッセージの影響と、このスクリプトが あなたの作業にどのような影響を与えるかを理解するためです。もう一つの選択肢もほぼ同じです。スクリプトが失敗し得る(can-fail)ようにしたくない場合は、スクリプト・コード全体を try_begin と try_end で挟むのです。[5]
その他 ちょっとした情報
ゲーム・エンジンが呼び出すスクリプトは上から下に読み取られるので、同名のスクリプトが多数あっても最初のスクリプトのみが呼び出されます。スクリプト ID (数値)はコンパイル時に作成されます。コンパイラは、最初のスクリプト(ID 0)から開始して全スクリプトについてループし、探しているスクリプト(「script_abcd」)の ID と各スクリプトの文字列 ID と比較します。1 つ見つかると、そのスクリプトの ID が返されるので、その検索は打ち切られ、2 番目または 3 番目のスクリプトが返されることはありません。[6]
たぶん有益な方法(要検証)。 RecursiveHarmony (credit), Script file unknown float variable
- 脚注と出典:
- [1] kalarhan, Modding Q&A.
- [2] このことは、最上位レベルのスコープで この cf_ 付きでスクリプトを呼び出すようなスクリプト(またはトリガー)も失敗し得ることを意味します。
- [3] Caba`drin, Modding Q&A, と Somebody, Modding Q&A.
- [4] Vornne, Modding Q&A と Modding Q&A.
- [5] The_dragon, Modding Q&A.
- [6] The_dragon, Modding Q&A.