インデント(字下げ)
命令(operations)
モジュール・システムでは、ゲーム・エンジンに対して指示を出したり情報を授受したりするための様々な「命令」(operation)が定められています。各「命令」毎に、人が見て機能の見当が付くような名称(ニーモニックあるいは単に「命令」)と、ゲーム・エンジンが理解できる数値(オペコード、opcode)が割り当てられています。例えば 2 つの値が一致しているかどうかを調べる eq 命令なら、オペコードは 31、という具合です。命令は全部で 1,000 個余りあり、オペコードの値ともに、header_operations.py というファイルで定義されています。
(訳注: この operation を「命令」と訳す理由は、アセンブリ言語(CPU が直接理解する機械語/マシン語と可換な、最も低水準のプログラミング言語)と共通点が多いからです。例えば、実行時エラーに現れる OPCODE という語、ソース・コード内で reg# という形で使うレジスタ、命令の後ろにパラメータ(オペランド)を並べる書き方、高水準言語のような組み込み関数がほとんど用意されていない、といった点です。そして何より、MOD 開発者の書いたソース・コードが疑似的にコンパイル(あるいはアセンブルと呼んだほうが相応しそう)され、ゲーム・エンジンが理解できる数字の羅列に変換される点が、アセンブリ言語にそっくりです。なお、プレイヤーが F1 キーなどで兵に出す「命令」(order)と混同しないよう、文脈を読み分けて下さい。)
各命令にどのようなパラメータ(オペランド)を指定する必要があるか、説明が(「#」以降のコメントととして)付いているので、使い方のヒントになるでしょう。
(訳注: 以下、3 つの図は訳者が追加。)

(訳注: このヘッダ・ファイル内では命令が次の 3 種類に大別されています。ループや呼び出しなど「制御」をする Control opetaions、論理演算子やキー押下判定など「状態を見て条件分岐」する Condition operations、その他 全般の動作や格納域の読み書きなどをする Consequence operations です。また、これとは別に有志たちがコメントなどを補った header_operations.py expanded という拡張版も単体で配布されていて、そこでは Z00~Z26 という見出しで分類しています。)

(訳注: 更に 上の「拡張版」を基に、検索や並べ替えなどを可能にしたオフライン html 版を私(訳者)が作って 2024 年に公開しました。上の投稿への この返信 から辿るとサンプル web ページがあります。)

モジュール・システムを使って(MOD 開発者が)書くソース・コード中では、次のような書式(ルール)で命令を並べます。
- 命令毎に括弧でくくる。つまり「(命令)」という形。
- ただし、その括弧の直後にカンマ「,」を付ける。「(命令),」のように。
- 命令がパラメータを伴う場合は、その括弧内にカンマ区切りで必要な数だけ指定する。例えば「(命令, パラメータ1, パラメータ2),」という形。
この header_operations.py ファイルに書かれている各命令の数値(オペコード)は、Warband の実行エラーでは、OPCODE ##:... のように表示されます。例えば、エラー・メッセージに「OPCODE 1」と表示されたのなら、あなたは header_operations.py 内で「=」の右辺が「1」の行を探し、それが call_script 命令であることを知り、コード内のスクリプト(サブルーチン)を呼び出そうとした箇所で問題を起こしたことが判ります。 (訳注: 実行時エラーに関しては、もう少し詳しい説明が「MOD 開発で ありがちなエラー」のページにあります。)
各モジュール・ファイル mudule_*.py 内には、Python 言語のリスト(list)とタプル(tuple)で記述された「命令」や「データ」があります。リスト [] もタプル () も、他のプログラミング言語にあるような「配列」として機能するので、MOD システムの(疑似)コンパイラのような「Phthon スクリプト」は、リストやタプルを 0 から始まる通番でアクセスできます。そして、兵種(troop)、物品(item)、トリガー(trigger)、シーン(scene)、あるいは クイック文字列("@....")などが .txt ファイル群にコンパイルされた時点では、インデクスに変換され、いくつかに分かれたファイル間でインデックス値を使って情報を参照する形になっています。そして、ゲーム・エンジンも、(実行時エラーに備えて)命令やトリガーやクイック文字列などの全ての位置(インデクス)を知った上で実行します。だから MOD 開発者が あるミッション・テンプレート内に複数トリガーを書いてあっても、エラー・メッセージに「トリガー 0」と書かれていたら、問題を起こしたのは その内の最初のトリガーだと判るし、「トリガー 4」なら(1 から数えて)5 番目と判るので、そこを調べればよいのです。
(訳注: もし別ページで示した「Hello, world!」を試したのなら、実際にインデクスの状況がどうなっているか、追跡してみるとよいです。つまり、追加した display_message 命令が menu.txt 内のどこにあり、そこに指定したクイック文字列 がどう表現され、quick_strings.txt 内の当該行と どう関連付けられているかを調べるのです。そうやって本文の「インデクス」の意味を体感し、「MOD の構造」や「ゲーム・エンジンの都合」の理解が進めば、実際のデバグ作業に役立つはず。なお、その際、19 桁ほどもある 10 進数が現れます。これを一旦(4 バイトの)16 進数に変換してから一部を抜き出して 10 進に戻す必要があります。Windows に付属の「電卓」を使う場合、[表示]-[プログラマ] にするだけでなく、左下のラジオ・ボタンで「Qword」を選んでおきます。)
変数
ローカル変数 - ":変数名" - コロン付きで宣言するとローカル変数になります。これらは角括弧 [] で囲まれた現在のコード・ブロック(つまり単一リスト)内でのみ存在でき、スクリプトへのパラメータとして渡されない限り、他のコード・ブロックから参照できません (訳注: つまり、スコープが狭く、変数名が衝突して値を壊し合うような影響を気にせずに、ブロック内で使い捨てできることが最大の利点です。逆に、トリガーのページの例にある 2 段トリガーを使ったランダム遅延のように、リストをまたいだ先で参照させる目的には使えません。)。初めて読み出すと不定値をとり得るので、参照より先に(0 など適切な値に)初期化しておく必要があります [1]。 必ず引用符 " " で囲みます。 例: ":agent_id"
グローバル変数 - "$変数名" - $ 付きで宣言するとグローバル変数になります。これらは定義されるとコードのどの部分でも、どの module_* からでも参照できます。デフォルト値は 0 です。引用符 " " で囲みます。例: "$g_talk_troop"。 (訳注: $ の後ろに g_ を付けるのが慣例のようですが、冗長なので事故の元になり得そう。)
整数レジスタ - reg0 ~ reg65 - これらはエンジンから使われ、断片的な情報を一時的に保存します。スコープはグローバルで、存続期間中にいくつかのスクリプトによってアクセスされた後 それらを使う別のコードのために残される場合があります。header_common.py 内で レジスタ reg0 から reg65 が宣言されています(訳注: スコープについて、原文は "global in scope, but ... and are then left ..." で、「グローバルだけど残されることがある」のように矛盾? また、reg64 は欠番です。これら レジスタ reg~ と次項の文字変数 s~ は、MOD の翻訳版を作る場合に いくつかの csv ファイルの中でも使われます。例えば数値や発話キャラクタの性別などを保持しているので、各国語の語順、数詞、女性語などに合わせた訳文を作れます。) [3]。
文字列レジスタ - s0 ~ s67 - これらは数値でなく文字列を保持するためのレジスタです。代入されるものには、module_strings.py で宣言した通常の文字列と そこで宣言しない クイック文字列があります。後者は " " の中が @ 記号で始まります。header_common.py 内で s0 から s67 までの文字列が宣言されています (訳注: 本文に説明がありませんが、文字列レジスタもスコープはグローバルです。s64 は欠番です。クイック文字列はコンパイル出力では quick_strings.txt、翻訳版作成時には quick_strings.csv に相当します。命令列レジスタは整数レジスタと同様に翻訳版の csv 中でも使います(変更は そこでは できません)。)。
位置レジスタ - pos0 ~ pos64 - 上記 2 つの「レジスタ」とは異なり、位置レジスタには複数の情報が格納され、それは X、Y、Z 座標と X、Y、Z 軸回りの回転です (訳注: scale(拡縮)もあるはず。拡張版 header_operations.h の init_position 命令のコメントを参照のこと)。header_common.py 内で pos0 から pos64 までの位置レジスタが宣言されていて、pos64 は攻城塔 (siege tower, belfry) に使われる不特定範囲の位置の始点です。(訳注: これも本文に説明がありませんが、位置レジスタもスコープはグローバルです。位置や回転や拡縮の演算は固定小数点数を扱うので、事前に set_fixed_point_multiplier 命令で設定した値の影響を受けます。一見すると整数座標を扱う presentations モジュールでも この multiplier に要注意です。また、presentations では大抵は二次元座標だけを扱い、Z の初期化を忘れやすいので、常に init_position 命令で初期化してから使うのが安全。position_transform_position_to_parent 命令を使って表示位置に一定の「増分」を加えて 1 行ずつずらしていくような処理でも、表示位置はずなのでの単位と増分の単位が異なるはずなので、やはり要注意。)
定数 - 定数(の識別子)は module_constants.py で宣言し、そこで割り当てられた値を、ゲーム中を通じて維持します。module_constants.py を編集する以外に変更できません。スコープはグローバルで、任意の module_* ファイルおよび任意のコード・ブロックで使うことができます。定数(の識別子)は引用符でくくりません。 (訳注: 要するに Python 言語のレベルで変数として定義する定数値のことです。その .py ファイルの先頭にも書かれている通り、定数の追加は自由にできます。)
ローカル変数、グローバル変数、レジスタに単純な値を代入するなら、(assign, <変数名>, <値>) という命令を使います。この値はリテラル値だけでなく別の変数名を指定することもできます (訳注: 他にも各種スロットやエージェント ID などを読み出して変数に書く専用の命令がいくつもあります)。文字列レジスタと位置レジスタについては、それぞれに専用の命令が header_operations.py で定義されています。
レジスタと変数は、(トリガーやスクリプトの)条件ブロック、結果ブロック、命令ブロックなどでのみ使用できることに注意して下さい [4] (訳注: これは恐らく考慮もれで、実際には数値レジスタと文字列レジスタは翻訳した csv ファイル中で参照可能)。変数が保持できる数値の最大値は 253 です。ただしビット 54 (header_common.py 内の下記箇所) が符号ビットと仮定すればの話です:[5]
op_num_value_bits = 24 + 32
...
tag_register = 1
tag_variable = 2
tag_string = 3
...
opmask_register = tag_register << op_num_value_bits
(訳注: 最大数について、符号を考慮するとしても話が 2 ビットほど ずれているように見えます。原文脚注に示された投稿も含めて読むと、まずビット位置の表現から察するに、ビット 0 が右端にあるリトル・エンディアンでの表記を想定しているように見えます。上記コードでは、数値 1 を 24+32=56 ビット左シフトした位置、つまり ビット 56 を tag_register が使っていて、そこから左のビットも同様に使われていることを示しています。ユーザが使えるのは その右側だと言うのであれば、ビット 55~0 の計 56 ビットのはずです。ビット 55 が符号だとしても、残る計 55 ビット分が有効桁のはずで、-255 ~ 255-1 を表わすことができ、最大値は 255-1 近辺になりそう。)
初期化していない変数 [6]
モジュール・システム内の変数は、assign 命令などで値を設定しなくても、コンパイルや実行はできます。ただし、複数処理がメモリ・アドレスを共有しているので、初期化しないと場合(高速ループ、あるいは Linux や Mac といった OS など)によっては、不定値により予期せぬ動きを招く恐れがあります。
ゲーム・エンジンは、Linux および Mac のオペレーティング・システムでは未初期化の変数をオーバーフローさせます。どこかで数兆コインの赤字を含んだ予算報告書の話を聞いたことがあるなら、それがその理由です。だから ほとんどの場合、たとえ数行後に別のものが変数に格納される場合であっても、このモジュール・システムでは変数に値 -1 とか 0 を代入するようにします (訳注: 原文は受け身形で主語が不明瞭だが、恐らく「MOD 開発者の責任で、ローカル変数の全てをブロック先頭で初期化せよ」という意味。)。グローバル変数はローカル変数とは異なり、ゲームのセーブ先へ(スロットとして)保存されるので、(不定値でなく)常に何らかの定義された値(デフォルトの初期値を含む)を持ちます (訳注: こちらは構造上 物理的に不定値になり得ない、という意味)。
また、エンジンはループをうまく処理できません (訳注: 未初期化変数がどこかにあると、という意味らしい)。だから、化けた値が予測不可能なバグを引き起こすケースは珍しくありません。このような場合、最善の策はデフォルトに依存しないようにすることです (訳注: つまり何らか初期化せよということ)。 これは主に(メモリ内の同じ領域を共用する)ローカル変数での話です。
モジュール・システムの接頭子
モジュール・システム内(例: module_mission_templates.py)では、コード部分のラベルの手前の接頭子を使うことで、モジュール・システムの他のファイルに保存されているコードまたは情報を参照できます。 module_dialogs.py だけは直接参照されることがないので、接頭子がありません。
| module_animations: | "anim_" | module_pstfx: | "pfx_" | |
| module_factions: | "fac_" | module_presentations: | "prsnt_" | |
| module_info_pages: | "ip_" | module_quests: | "qst_" | |
| module_items: | "itm_" | module_scene_props: | "spr_" | |
| module_game_menus: | "menu_" | module_scenes: | "scn_" | |
| module_map_icons: | "icon_" | module_scripts: | "script_" | |
| module_meshes: | "mesh_" | module_skills: | "skl_" | |
| module_mission_templates: | "mst_" | module_sounds: | "snd_" | |
| module_music: | "track_" | module_strings: | "str_" | |
| module_particle_systems: | "psys_" | module_tableau_materials: | "tableau_" | |
| module_parties: | "p_" | module_troops: | "trp_" |
このような ID 定数を参照する際、引用符付きの箇所と無しの箇所があることに気づくでしょう。例えば、"trp_swadian_levy_1" と trp_swadian_levy_1 のようにです。引用符を付けると、参照時に ID_troops からインポートする必要がなくなります(モジュール・システム・ファイル間の相互関連 を参照)。module_*.py ファイルのうち、(module_scripts.py などのように)ID を探し、後で数値に変換するように設定されているものでは ID に引用符を付けて使います。一方、処理スクリプトが文字列を数値に変換しない(module_troops.py のような)他の特定のファイルでは、引用符なしです。引用符なしの変数名を使うことは、様々な不利益があります。現在のファイルに含まれるオブジェクトのタイプを参照できないとか、循環依存関係が存在するとか、エントリを削除した後で 2 度のモジュールのビルドが必要になったり、特定の状況下で正しい ID_*.py の作成が必要になったりするなどです [7]。
モジュール・スクリプト, パラメータ, 引数など
ほとんどの module_* ファイルでは、ほとんどのコードが自己完結しています。_skills、_troops、_items などのファイルは、ゲームの色々な要素を定義しているだけで、エージェントの動作やプレイヤーの選択を制御したりしません。module_game_menus には、様々なメニューとその選択肢などが含まれています。presentations には、もう少し込み入った表示や描画の位置などが含まれています。
一方、module_scripts.py には、各ソース・コードのあらゆる箇所から呼び出される共通的な処理(関数)を書きます。
呼び出しには (call_script, "script_スクリプト名", <オプションの引数群>) という命令を使います。引数を指定できるので、グローバル変数を使わずにスクリプトへ情報を渡すことができます。引数には、呼び出し側でローカル変数などに作った値を指定できます (訳注: つまり、スコープ外へ出る直前に自動的にコピーが作られて渡されます。)。呼ばれた側の module_scripts.py 内スクリプトでは、渡されたパラメータを読み出してローカル変数に写す命令を(開発者が)書いておく必要があります。そうすることで そのスクリプトのブロック全域で使用できるようになります。逆方向、つまりスクリプトから呼び出し元に情報を渡すことは同じやり方ではできません。通常はレジスタを介して値を返します。つまり、スクリプト側でいくつかのレジスタに値を設定し、呼び出し側に戻った後、呼び出し側が それら決まったレジスタをローカル変数などに写すように(開発者が)命令を書いておきます。呼び出し時に渡せる引数の最大個数は 15 個です。
以下は 呼び出す側と呼び出された側で値を授受する例です。後者を module_scripts.py に置きます。
//...statements...//
(assign, ":local_variable_1", 10),
(assign, ":local_variable_2", 5),
(call_script, "script_example_script", ":local_variable_1", ":local_variable_2"),
(assign, ":local_variable_3", reg0), #local_variable_3 = 15
//...further stuff, working with local_variable_3...//
# script_example_script
# Input: variable_1, variable_2
# Output: Sum in reg0
("example_script", [
(store_script_param, ":num1", 1), #now num1 is 10
(store_script_param, ":num2", 2), #now num2 is 5
(store_add, ":sum", ":num1", ":num2"),
(assign, reg0, ":sum"),
]),
- 脚注と出典:
- [1] (ループから) 繰り返し呼び出されるスクリプトがある場合、呼び出しが短時間で連続するので、このスクリプト内のローカル変数が呼び出しのたびにリセットされない。呼び出しごとに再設定されるように、スクリプトの開始時にそれらを初期化する必要がある。Caba`drin, Modding Q&A。
- [2] グローバル変数の値は保存されたゲームとともに保存され、ゲーム開始スクリプトで宣言されたグローバル変数は、ゲームをロード時に再宣言されない。Caba`drin, Modding Q&A。
- [3] 括弧内に数字を書く reg(#) という書き方は まだ機能するが古い。新しいバージョンのモジュール・システムからは reg# のように数字を直接続ける書き方になった。Native の MOD システムには、古い書き方がまだ残っている。kalarhan, Modding Q&A。
- [4] Yoshiboy, Modding Q&A。
- [5] dunde, Modding Q&A。
- [6] kalarhan, Modding Q&A、Modding Q&A および Modding Q&A。
- [7] Caba`drin, Modding Q&A と Vornne, Modding Q&A。
タプル(tuple)
タプルは、いくつかのデータ項目をカンマ「,」で区切って小括弧 ( ) でくくったものです。モジュール・システムの全ての「命令」や「構造」定義はタプルです。例えば module_scripts.py なら、下記のように書きます。
(訳注: Python 言語では、一度定義すると要素の書き換えや追加ができない(immutable な)タプルと、それらが可能な(mutable な)リストを使い分けることができます。しかし、この MOD 開発システムでは、その機能よりも、リスト内にタプル、そのタプル内にリスト、そのリスト内にタプル、という階層を(開発者が)識別しやすくする目的でタプルとリストを使い分けます。疑似コンパイラを構成する process_*.py (のオリジナル状態)ではタプルとリストを見分けていないようで、例えば命令を一ヶ所、タプルでなくリスト [] でくくっても、コンパイルはできるし、同じ .txt が出力されるようです。これを(どうしても)コンパイル・エラーにしたければ、process_*.py に isinstance() などを使った判定処理を書き加えればよいです。)
scripts = [
("script_name",
[
(operation, ":param1", ":param2"),
(another_operation, ":param1", ":param2", ":param3"),
]
),
("another_script_name",
[
(operation, ":param1", ":param2"),
(another_operation, ":param1", ":param2", ":param3"),
]
),
]
全体としては最外殻に 1 個のリスト(角括弧、"scripts=[]")があり、その中に 2 つのタプルが含まれていて、それぞれがスクリプトです。更に各スクリプトには、順に、文字列値 (スクリプト名) が一つと、「一連の命令群を含んだリスト」が 1 つが含まれています。そのリスト中の各命令はタプルで書かれ、その中の先頭の項目が命令名、残りはパラメータです。つまり逆にたどると、一番深い所に命令群(複数のタプル)があり、それらを要素として含むリスト、そのリストと文字列を要素とするタプル群、そして最外殻に それらを要素とする 1 個のリストがあります。簡単ですよね? 特に module_presentations.py のような もっと複雑なファイルと比べたら なおさら [8]。
兵種(Troop)とエージェント(Agent)
![]() |
![]() |
兵種(troop)は、module_troops.py 内のエントリを意味し、決められた名前で参照します。trp_player なら主人公(プレイヤー・キャラクタ)、trp_npc## は各コンパニオン、trp_knight_#_## は候伯 (訳注: Lord。各勢力に属し、部隊を引き連れてマップを動き回っている人たち。複数の場合は「諸侯」と訳される場合があります。)、trp_swadian_footman(スワディア歩兵)などです。名も無い一般兵などの集団(generic soldiers)は、集団テンプレート(party templatge)で扱い、その中に複数の兵種を含めます(次項)。
一方、エージェントは、シーン (戦闘、宿屋など) 内に配役された人物などを一時的な番号で表わしたものです。module_troops.py から作成された、主人公、コンパニオン、候伯が どこかのシーンのエージェントになれば、依然としてユニーク(重複なし)です。ユニークでない、名も無い一般兵などは、単一の集団テンプレート(party template)を基に複数のエージェントの実体を作り出せます。各エージェントはそのシーンに固有の ID を持っていますが、シーンが終了するとエージェントは存在しなくなり、そのエージェント ID は無意味になります。
(訳注: 現在のシーン内にいるエージェント全員(人としての「主人公」、物品としての「馬」などを含むが、シーン小道具などを含まない)について何か処理をしたければ、try_for_agents と try_end でループさせます。その中で agent_get_troop_id 命令や agent_get_item_id 命令を使うなどして人や馬を切り分けつつ兵種や馬種を取得できます。)
集落・施設(Party)と集団テンプレート(Party Template)
訳注:
「Party」という語は(同名のボタンもあるので)動き回る部隊を連想させるし、module_parties.py には攻撃性などの「性格」や「敵対相手」といった「部隊に関係しそうな項目」がありますが、多くの既製 MOD の実状では、集落や施設(街、城、村、悪漢の住みか、廃虚、橋など)のような静的な(動かないし性格も持たない)ものだけを module_parties.py に集める一方で、よく似た項目を持つ「集団テンプレート」(Party Templates)モジュールのほうに動的な各種集団(候伯以外の、「隊商」「強盗騎士」「身代金を待つ賊」「鹿の群れ」など)を集める、というように住み分けをさせています。
そのため、上の見出しは 原文(本文)の説明と異なり、 Party のほうを、実状に合わせて「集落・施設」と訳しています。ただし、Party ボタンや P キーで表示される隊員一覧のように、プレイヤー向けの視点では party が隊を意味する場合もあります。更にややこしいことに宴会も party です。だから翻訳版を作る時は、プレイヤーを混乱させないように、「パーティー」のようなカタカナを使わず、「隊」「宴会」などと訳し分けるのがお薦めです。

「テンプレート」という語は、昔の製図用具(決まった図形をいくつも描くための、長丸とか菱形の穴の開いた板)や、あるいは「モデル文」のように、同じものや似たものを多数 出現させることを目的としたものを意味します。このモジュール・システムの Party Template も同様で、例えば「隊商」や「鹿の群れ」というテンプレートを作っておき、そこに構成員(兵種)と「人数または頭数」の範囲(最小値と最大値)の初期値を書いておきます。トリガーなどを契機に「集団」として具現化させるたびに、ゲーム・エンジンがその範囲の乱数で「人数または頭数」を決めてくれます。このようにテンプレートで定義するものが戦闘集団とも人とも限らないので、訳も「集団テンプレート」としました。
下図は訳者が追加。

Party とは、ワールド・マップ上にある各々の実体(entity)です。プレイヤーの引き連れている部隊、盗賊とその隠れ家、NPC 候伯、または街/城/村などがそうです。
訳注:
上記「実体」について。parties モジュールのソースを見るとわかるように、部隊として定義されているものは むしろ「実体でないもの」(一時的や形式的なもの)ばかりだし、「性格」や「敵対相手」といった項目も 0 です。個々の候伯やコンパニオンを全員定義しているわけでもありません。一方「集落や施設」は列挙しています。またマップ上の木とか起伏は別のところで定義するので、ここでの「実体」に含みません。)様々なタイプの party が、最初の party スロット「slot_party_type」(または party スロット 0。下記スロットの説明を参照)のカテゴリに分類されます。タイプ毎に関連付けられた整数が含まれます。これらのタイプは module_constants.py で定義され、spt_* (slot_party_type) という接頭子で始まります。spt_* 値の例としては、spt_kingdom_hero_party(NPC 候伯の場合)や spt_castle(...城の場合)などがあります。
各 party には、コード内で参照される ID 番号があります。例えば、主人公を表わす party は「p_main_party」で、インデックス値(party ID)は常に 0 です。プレイヤーの party や全ての街、城、村など、特定の party は定数(静的 ID 番号)です。静的 ID を持つ party は module_parties.py で定義されます。モジュールをコンパイル後は、ID_parties.py というファイルを見れば それらの ID 番号を確認できます。
module_parties.py で定義されていない全ての party については、上記「エージェント/兵種」の区分と大体似た方法で、集団テンプレートに基づいてゲーム・エンジンによって動的に作成されます。これらの集団テンプレートは module_party_templates.py で定義され、名前には接頭子「pt_#」が付いています。 テンプレートには主に そのテンプレートに含まれる部隊のリストと、ゲーム内でそのテンプレートに各部隊タイプの数が与えられるかを決定するために使用される数値範囲が含まれます(エンジンはその範囲内の数値をランダムに選択します)。spawn_party スクリプトを呼ぶことで、集団テンプレートから新しい集団の実体を作ることができます。この新しい集団には ID 番号が割り当てられ、slot_party_type 値などで分類できます。集団テンプレートは既存の集団にも使えます。 このようにして、テンプレート(そこで定義された兵種)を別の隊に追加して、既存の隊を「強化」(訳注: 原文は reinforce)できます。
訳注: 上の 2 つの項では、この開発言語が扱う「対象」として troop や party などを紹介していますが、限定的だし、扱う対象は他にも たくさんあります。詳しくは個々のページを読んで下さい。
スロット(Slot)[9]
(訳注: 下記段落は原文では説明が前後して難解なので、ここでは補足しつつ説明順を変えて訳してあります。)
スロットは、各オブジェクト種別(隊、兵種、エージェント、チーム、勢力、シーン、物品、プレイヤー、シーン・プロップ、または集団テンプレートなど)の個々のオブジェクト毎に複数の変数を持たせ、それを名前でアクセスできるようにしたものです。名前と番号の対応が module_constants.py で定義されています (訳注: そのファイル冒頭のコメントにあるように、追記もできます。)。スロット名は「slot_オブジェクト種別_個々のスロット名」という形です。例えば物品なら「slot_item_個々のスロット名」というふうに、わかり易い名前でアクセスできます。オブジェクトごとに一意の情報を格納する必要があります。例えば物品の「基本価格」を格納するためのスロットは「slot_item_base_price = 53」と定義されていて、スロット名を指定すると、エンジンは 53 番のスロット変数を調べ、その物品の基本価格を見つけます (訳注: たいていは 0 から始まりますが連番ではなく、欠番もあります。)。個々の物品(「干し魚」や「木材」)ごとに slot_item_base_price でアクセスできるスロットがあるので、物品ごとに異なる基本価格を持つことができます。
(訳注: 物品以外のオブジェクトでも同様です。例えば、街メニューで「市場へ行く」から「武器屋へ向かう」を選ぶと、その街に専属の武器職人を調べるために module_game_menus.py の中で slot_town_weaponsmith というスロットが使われます。この場合はオブジェクトが「街」、スロットから取り出されるのは「兵種」です。その兵種は module_troops.py で指定し troops.txt に出力された town_XX_weaponsmith のような識別子を持っています。)
ここで、次のようなスロットを使用した命令を見てみます。
(item_get_slot, ":base_price", ":item_id", slot_item_base_price),
基本価格を取得したい商品を正確に指定する必要があることがわかります(訳注: item_id のこと)。また、変数は配列されているので、例えば全ての物品の基本価格を使って何か計算したければ、これを try_for_* ループの中に書いて、多くの「:item_id」を変えながら反復処理すればよいのです。
item_get_slot 命令で特定の物品の価格にアクセスし、item_set_slot で変更し、item_slot_eq などで等しいかどうかをテストできます。
C 系のプログラミング言語をご存じなら、スロットは次のように動作します。(訳注: C/C++ 言語系プログラマにとっては下記説明よりも、enum か #define で定義した定数で配列を扱う、というほうが明解かも。JavaScript なら連想配列に相当し、文字列で要素を検索できます。Python 言語に不慣れのかたは、tuple、list、dict の概略を理解しておくとよいです。)
slot_troop_spouse
spouse[troop1] = spousename
spouse[troop2] = spousename
spouse[trp_player] = spousename
兵種(troop)オブジェクトの配偶者を示すスロット slot_troop_spouse は module_constants.py ファイル内で 30 と定義されているので、エンジンは これを troop_variable_30[troop1] や troop_variable_30[trp_player] のように解釈します。
slot_item_base_price
baseprice[item1] = priceA
baseprice[item2] = priceB
baseprice[item3] = priceC
同様に、物品の基本価格なら 53 なので、エンジンはこれを item_variable_53[item1] や item_variable_53[item2] などと解釈します。
全てのスロットの値は、グローバル変数と同様で *_set_slot 命令で設定されるまで 0 です [10]。各スロットには、多数の固有の格納場所があります。 各スロット番号は 64 ビット整数を格納できますが、実際はゲーム・エンジンの他の部分(マルチ・プレイヤー・ネットワーク・メッセージなど)が 32 ビット整数のみを扱うので、安全のために 32 ビットで表わせる -2147483648 ~ 2147483647 の範囲にする必要があります。スロット番号(module_constants.py で定義されているもの)については、オブジェクトのタイプごとに 0 ~ 1048575 (つまり 220) を使えますが、メモリの無駄使いを避けるために、スロット番号を できるだけ近接させる必要があります。エンジンが連続番号の配列を割り当てるからです [11]。上限を超えるスロットを使おうとすると、割り当てられず、取得値は常に 0 になります [12]。
例えば、兵種スロットは候伯ごとに固有であり、候伯の性格を定義します。一方、エージェント・スロットは、どのエージェントが逃げるかを定義します。やはりエージェントごとに固有の動作であり、これにグローバル変数を使用してもうまくいきません。スロットの働きを理解すると、グローバルばかりでなく 状況によってはスロットを使うべき とわかります。これは代替策が無いからというだけでなく、そのほうが うまくいくからです。スロットを使う際の注意点が 3 つあります。
- これらは module_constants だけでなく、使用される任意のファイルで定義できます。ただし、このスロットが含まれる他のファイル群には、先頭で * 付きでインクルードされる定義を伴うファイルもあることに注意して下さい。または、全く定義されていないこともあります。その意味を確実に憶えていたら、(agent_set_slot, ":agent", <slot_name>, <value>)| の代わりに (agent_set_slot, ":agent", <slot_no>, <value>) を使えます(訳注: include でなく import のことを言っているとして、それを理解した上ならスロット名の代わりにスロット番号を指定できる、ということらしい。他ファイルを注意することと番号で指定可ということに脈絡が無く、文意が難解。)。実は このことはスロットだけでなく あらゆる定数にも当てはまります。
- 同じ番号を持つ複数のスロットを使えますが、それらが異なる「カテゴリ」でなければならないことに注意して下さい。つまり、兵種スロット 145 と隊スロット 145 があるのは問題ありませんが、番号 145 の兵種スロットを 2 つ使うのは避けて下さい。(訳注: 恐らく、「2 通りの名前を兵種スロット 145 番に割り当てて、異なる用途に使うのは混線のもとになり危険」という意味。)
- スロット名がコンパイル結果(配布物)のどこにも保存されないことを利用して、コンパイル済のコードを理解しにくくする目的で(意図的に)スロットを利用する場合があります。(訳注: 原文は やや漠然としているので省略して訳しました。難読目的の空きスロット利用というのは、例えば パズル要素の強い MOD を作っていて、ヒントとなる情報をユーザに類推させたくないような場合です。ユーザが txt ファイルを読み解いていくと、グローバル変数使用箇所やスクリプト呼び出し箇所から、その変数名やスクリプト名を辿ることができ、処理内容を類推できることがあります。でも module_constants.py で定義されていない、空いたスロットを独自 MOD で定義して使えば、txt のコード・ブロックの使用箇所からスロット名がわからないので、比較的 類推しにくいのです。そこまでやる必要性が本当に生じるかどうか(謎解きコンテストなど?)は さておき、たとえ実装する場合であっても、MOD 開発者は充分な説明コメントや整然としたコーディングなどにより、ソース・コードの可読性と保守性を確保すべきでしょう。)
例えば slto_kingdom_hero のように一見すると誤記のようなスロットを見かけます。slto が slot_troop_occupation の略で、これらスロットのための定数値の目印を付けている、ということに注意して下さい。つまりこれは要修正のスペルミスではなく、単に TaleWorlds Entertainment 社の命名規則にすぎません [13]。
実践:モジュール・データのリロード後にスロットが 0 にリセットされる, Somebody, Working with dialogs is frustrating(ゲーム中の「会話」に関する作業にはイライラさせられる)
その他:スロットに残影のような値があって対処できない (Mad Vader)。発言毎に値が ぶれることがある(There are also different comments with varying values) (_Sebastian_ と _Sebastian_) 他にも、もしまだ有効なら興味深いものの、恐らく古そうな情報も (Hellequin と Hellequin)。スロットを扱うゲーム・エンジンに関するコメント (cmpxchg8b)
固定小数点数
数学の演算ではいろいろな数字を扱いますが、モジュール・システムが扱えるのは「整数」だけです。浮動小数点数は、いわゆる「固定小数点数」でエミュレートします。モジュール・システムでの命令で固定小数点パラメータが現れる場合、実際には常にそれが単なる普通の整数であることに留意して下さい。つまり、浮動小数点数を「元の整数 / 割る数」という整数同士の割り算で表わす、という意味です。もちろん、浮動小数点数に分母の「割る数」をかければ「元の整数」に変換できます。かける値は set_fixed_point_multiplier という命令を使って指定し、これは固定小数点数を扱う全命令の精度に影響します。(訳注: この命令は header_operations.py で定義されていて、いくつかの module_*.py 内で使われています。指定値は 100 や 1000 のような 10 の倍数です。エンジンへの指示なので、当然ながら変更すると それ以降の全処理に影響しそう。退避~回復しようにも読み出す命令は見当たらないので、あちこちの非同期のトリガーで使われることを想定すると、固定小数点数を扱うあらゆる処理(ブロック)で毎回もれなく設定が必要であって、以前に設定した値が保持されていることを前提にするのは絶対にいけない、ということらしい。)
(訳注: 下記は原文ではパラメータ名が説明なく現れる上に脈絡も難解なので、大幅に補足しつつ説明順を変えて訳してあります。)例として、123 の平方根を求め、その結果が小数点以下 3 桁まで正確である必要があるとします。まず header_operations.py を開いて store_sqrt の行に書かれているパラメータの説明を読んで下さい。下記コードでは store_sqrt を使う前に、store_sqrt の第 2 パラメータ <value_fixed_point> )に渡す値を 1,000 倍しています。さらに、set_fixed_point_multiplier にも 1,000 を設定しているので、結果的に「123 * 1,000 * 1000」の平方根、つまり「123 の平方根」* 1,000 が求まります。なお、val_mul でかける値を 10 にして、set_fixed_point_multiplier のほうを 100000 にしても同じ結果が得られるし、val_mul の行を無くして set_fixed_point_multiplier に 1000 * 1000 を指定しても同じ結果を得られます。ただし、set_fixed_point_multiplier は演算命令に固定小数点の位置を知らせるので、固定小数点を扱う後続の演算に影響し続ける可能性があります。
(assign, ":value", 123),
(val_mul, ":value", 1000),
(set_fixed_point_multiplier, 1000),
(store_sqrt, ":result", ":value"),
電卓でこの平方根の値を計算すると 11.0905... ですが、この例では ":result" には その 1,000 倍の 11091 という値が入いります。この 11 と 091 の間に隠れた小数点がある、というのが この MOD 開発でいう「固定小数点数」で、小数以下が 3 桁だと決めているのが set_fixed_point_multiplier の指定です [14]。
- 備考と出典:
- [8] Lav, Modding Q&A.
- [9] Dalion 氏による補足情報。
- [10] これは、スレッド Modding Q&A で cmpxchg8b 氏が書いているように、グローバル変数はスロットだからです。
- [11] Vornne, Modding Q&A。
- [12] Dalion, Mount & Blade Modding Discord。
- [13] Somebody, Modding Q&A。
- [14] _Sebastian_, Modding Q&A。
まず、このゲーム・エンジンは あるブロック内の命令を順に処理し、その途中で判定命令や「失敗可能スクリプト」が 1 つでも偽になると、そのブロック内の残りの全命令を読まずに中止します。
この全処理中止を阻止し、コードの残りの処理を続行するには、try_begin、else_try、try_end などからなる制御文(try ブロックやループ)を使います。他の言語のような「前判定」ではなく、中判定なので注意が必要です。それらについては後述することにし、判定のための命令について先に説明します。
判定命令
(1) 汎用的な判定命令
(eq, 値1, 値2), 値1 = 値2 かどうか判定
(gt, 値1, 値2), 値1 > 値2 かどうか判定
(ge, 値1, 値2), 値1 ≧ 値2 かどうか判定
(lt, 値1, 値2), 値1 < 値2 かどうか判定
(le, 値1, 値2), 値1 ≦ 値2 かどうか判定
(neq, 値1, 値2), 値1 が 値2 不一致かどうか判定
(is_between, 値, 下限値, 上限値), 値が下限値以上かつ上限値未満かどうか判定
(2) 一定用途の(専用)判定命令
名前が「is_」で始まるか命令、名前の途中に「_is_」を含む命令、それと key_clicked 命令や map_free 命令 や party_can_join などのように名前に is を含まない判定命令があります。拡張版「header_operations.py expanded」の中を、大文字小文字の区別なしに checks という語で検索すると(汎用的な判定命令や判定用でない命令も含めて)おおかた見つかりますが、その方法で全てを洗い出せるかどうかはわかりません。
(3) 失敗可能スクリプトの結果による判定
call_script 命令で呼び出すスクリプト名は通常なら script_xxxx という形ですが、途中に _cf_ を加えた script_cf_xxxx という形にすると、「失敗可能スクリプト」呼び出しとなり、失敗時にそれを呼び出した call_script 命令が偽になります。それについては「スクリプト・モジュール」のページに説明があります。)
反転修飾子 neg
命令本来の「真かどうか」を判定する機能を反転して、「偽かどうか」を調べたい、つまり本来の真の時に処理を失敗させたい場合は、次のように neg 修飾子を指定します。
(neg|判定命令),
例えば、(neg|agent_is_alive, <agent_id>) とすれば、エージェント ID で指定されたエージェントが既に倒されている(存命でない)場合にのみ後続処理が実行されます。この neg は論理演算を逆にするのであって、neq(等しくない)とは異なることに注意して下さい!
全処理中止の制御(阻止)とループ
(訳注: 下記 訳者が header_operation.py を基に 2 命令を最後に追加。ここでの [] はリストでなく省略可の意味。半径の単位など詳細や省略。本文にも補足。)
(try_begin),
(try_end),
(else_try),
(try_for_range, 変数, 下限値, 上限値),
(try_for_range_backwards, 変数, 下限値, 上限値),
(try_for_parties, 変数),
(try_for_agents, 変数, [位置番号], [半径]),
(try_for_prop_instances, 変数, [物品 ID], [物品タイプ]),
(try_for_players, 変数, [サーバ・プレイヤー・スキップ]),
これらの内、try_for_* 命令は、try_end までの区間(try ブロック)をループ(反復)し、一定条件で終了(脱出)します。そこに指定した変数は、「ループ変数」として扱われ、ループするたびに 1 ずつ増減します。ループ全般について詳しくは後の項で説明しますが、*_backwards 命令を含め、ループ中のループ変数は「下限値」から「上限値 - 1」までの値をとる、という点が大事です。「上限値」や それ以上の値でループ内が実行されることは ありません(Python 言語の range() と同様です)。

訳注:
この図は、1 つのブロック内を模式的に示したものです。
判定命令 1 や 11 は、try ブロック中に無いので、偽になった時点で ブロック内の後続処理を全て中止します(赤線)。
ブロックの先頭の命令から順に実行していき、「判定命令 1」を無事に通過すると、その後ろの命令を実行しながら、最初の(最外殻の)try ブロックの入口、try_begin まで進み、その try ブロックの中を実行します。
判定命令 2、3、10 は、最外殻 try ブロック内にあって、「それより深い try ブロック」内ではないので、偽になった時点で最外殻 try ブロックを抜け、最外殻の try_end の後ろの命令から続行します(水色線)。
判定命令 1~3 まで無事に通過すると、内側(2 階層目)の try ブロックに入いります。そこでは、もし判定命令 4、5 とも無事に通過したら、同階層の最初の else_try の手前まで実行し、 E6 から抜けて、この(2 階層目の) try ブロックの try_end の後ろから続行します(青紫線)。しかし 判定命令 4、5 のどちらかが偽の場合は E4 か E5 から抜けて、同階層の「第 1 else ブロック」を実行します(緑線)。直近の else~ ではありません。たとえ判定命令 5 の後ろにもっと深い階層の try ブロックがあったとしても、その else~ ではありません。
上で E4 か E5 から第 1 else ブロックへ来た場合は、判定命令 6、7 を無事通過なら、(上の場合と同様)E9 から抜けて同階層の try_end の後ろから続行です(青紫線)。判定命令 6、7 のどちらかが偽の場合は、E7 か E8 から抜けて、同階層の次の else~ である「第 2 else ブロック」を実行します(緑線)。
つまり、他の言語の if~else~else や switch~case~case のような「前判定」ではなく、try や else~ の各ブロック内を順に実行しながら「中判定」をします。その途中で通る「判定命令以外の通常の命令」(図の「...」の区間)は、順次実行されます。
この例では「第 2 else ブロック」が最外殻 try_begin~try_end の最後の else~ なので、判定命令 8、9 とも真で通過なら、同階層の try_end まで実行を続けて 更にその後ろも続行するし、どちらかが偽なら E10 か E11 から抜けて同階層の try_end の後ろから続行します(青紫線)。
ここでは話を簡単にするため、AND も OR もループも無しで説明しました。本文で後述されるように、判定命令が連続すれば AND だし、this_or_next 修飾子を付けた判定命令が連続するなら 最後に修飾子の無い判定命令が 1 つ現れるまでが OR です。ループ系 try が加わった場合も原理は try ブロックと同じです。ループ処理内で「偽」が起きた時点で、そのループの階層の try_end へ抜けます。
何か試したくてウズウズしているかたは、別ページにある Hello, world! のやり方を応用して、上図から実際のコードを書き起こし、試してみるとよいです。ヒントとして、判定命令の箇所には 初め全て真になるよう「(eq, 1, 1),」を置き、「...」のところは 「(display_message, "@~" ),」で階層、ブロック、枝番などがわかるような、全箇所違う情報(面倒なら 1, 2, 3 ... という通し番号だけでも可)を表示し、通り道を完全に追跡可能にしておきます。後は 1 箇所ずつ「(eq, 0, 1),」で偽にするとか、判定命令 4 と 7 だけ偽にするなどして、都度コンパイル、実行して通り道を確認します。(そのコードは訳者の手元にありますが、あえてここに載せません。理解する過程の楽しさを奪っては いけないので。)
論理演算 AND と OR
上で見てきたように、ゲーム・エンジンは判定命令が偽(失敗)になると実行中のブロック(メニューやトリガーや try ブロック)の後続処理を全て中止するので、論理 AND を実現したければ、(それを利用して)複数の判定命令を単に順に並べていくだけです。
例えば、存命のエージェントで、馬でも敵でもない、という判定をする場合、コードは次のようになります。
(agent_is_alive,<agent_id>),
(neg|agent_is_human,<agent_id>),
(neg|agent_is_ally,<agent_id>),
2 つの条件の OR が成り立つかどうかを調べるには、先に実行する判定命令に「this_or_next|判定命令」のように修飾子を付けます。条件を 3 つ 4 つと連結する必要があるなら、修飾子を付ける行を増やします。n 個の条件を OR で連結するなら、始めの n-1 個の判定命令に修飾子を付け、最後の判定命令には付けません。例えば下記は 2 条件を連結する例で、ある変数が 1 または 5 なら後ろの行の処理を続けます。
(this_or_next|eq, ":test_variable", 1),
(eq, ":test_variable", 5),
(訳注: 「|」はコンパイラへの OR 指示で、this_or_next がゲーム・エンジンへの OR 指示です。エンジンは命令を解読時に this_or_next と 判定命令 が同時に指定されていることを(ビット範囲から)見分け、「その判定が偽でも次行の判定を続ける必要がある」ことを知ります。 )
try ブロック
判定命令は、一つでも偽になると 今いるブロック(トリガーやスクリプトやメニュー処理など)を終了してしまいます。しかし それをスクリプト呼び出しだけで迂回していては、可読性も生産性も悪くて不便です。そこで、今いるブロックがいきなり終了しないように、指定したある一部の区間のみで「判定と終了」を完結させ、判定結果の真偽(成否)にかかわらず その区間の後ろで合流して、処理が続行されるようにしたくなります。
これを行なうのが、try_begin と try_end で挟んだ「try ブロック」です。If-Then の形を作るには ふつう そうします。この try ブロック内に現れた *is_* などの判定命令が 1 つでも偽(失敗)になった時点で、try ブロック内の後続処理をスキップして、処理は try_end の後ろへ抜けます。それら判定命令がどれも真(成功)のあいだだけ try ブロック内の処理を続け、try_end まで行くと やはり後ろへ抜けます(ただし try_for_* と try_end の組み合わせの場合は抜けずにループします。それについては別項)。
If-Then だけでなく、else_try を使って「Else If」文を挿入できます。else_try の前の try で条件が失敗した時点で、ゲーム・エンジンは後続の else_try があるかどうか探します。例を挙げます。
#Code above, doing whatever
(try_begin),
(eq, 1, 1), #True, go to next line
(gt, 2, 5), #False, go to else try
#consequence operations here
(else_try),
(eq, 0, 1), #False, go to next else try
#consequence operations here
(else_try),
(eq, ":favorite_lance", ":jousting"), # Maybe?
(assign, ":prisoners", 100),
(try_end),
#Code continuing on, regardless of whether the favorite lance=jousting and prisoners was set to 100
(訳注: 2 箇所ある「#consequence operations here」というコメントのところに、実際には条件成立時のコード(1 行または複数行)を書きます。この例では 2 > 5 が不成立なので、後続の最初の else_try へ処理がスキップし、0 = 1 が不成立なので、後続の else_try へスキップします。だから 2 つの「#consequence...」の箇所に書いた成立時のコードはどちらも実行されません。ここでは説明のため前半の比較が定数同士なので、その経路は一定していますが、実際のコードでは変数などを参照するので、try_end の行まで様々な経路をたどります。例えば、gt の行で条件が成立すれば、最初の「#consequence...」の箇所を実行し、その後ろの else_try が見えた時点で try_end へ行って この try ブロックを終了します。最後の行のコメント箇所へは、try_begin から try_end の間で どのように条件が成立したかに関係なく合流し、それ以降が実行されます。もし、try ブロックが無いと、どこかで条件不成立が起きたとたん このブロック(何らかのメニューやトリガーなどを契機に発動したはずの、Python 言語のリスト [] 内にタプル () で書かれた一連の命令)の後続の一連のコードを全てスキップし、このブロックの処理が終了します。)
「AND の OR」か「OR の AND」か [14]
訳注:
原題は "Conjunctive and Disjunctive Normal Form"(連言標準形 CNF と選言標準形 DNF)。要するに、「A かつ B なら、処理 X を実行、それ以外なら Y を実行」のように命令を並べるか「{『A でない』または『B でない』}なら Y を、それ以外なら X を実行」とするか、といった、「論理的な AND と OR と否定を組み合わせてソース・コード(文字通り「ロジック」)を書く際の順番の話です。

「ド・モルガンの法則」が頭をよぎったかたは、「AND すること」が「OR と 3 つの否定」に可換であることや、「OR すること」が「AND と 3 つの否定」に可換であることをご存じでしょう。「そもそも、論理 AND、論理 OR に 不慣れだ」というかたは、「ベン図」で おさらいしてみて下さい。この MOD 開発環境に限らず他の言語にも言えますが、論理的に同じでも 作者の好みで(本文で後述されるように)「最深部で AND してから OR」で書いたり「最深部で OR してから AND」で書いたりします。初めのうちは思いついた あるいは気に入った書き方を選べばよいかもしれません。しかし、書きやすいコードと、読みやすいコードと、メンテし易いコードの、どれもが万国共通とは限らず、書き手、読み手、その経時変化、世の流行、論理思考の地域差などによって相対的に揺らぐ(普遍的でない)ことに注意して下さい。特に 2 人以上の寄りあいチームでコードを書く(可能性も含む)際には、事前にコーディング規則など 何か申し合わせをするほうが生産性や可読性や品質が上がるかも。
多くのプログラミング言語では数学と同じように「かっこ」を使って論理演算の優先順を指定できて直感的ですが、この MOD 開発環境では それができず、論理演算相当のことを書くには、try ブロックと this_or_next 修飾子と スクリプト呼び出しぐらいしか材料がありません。だから一人で開発するにせよ、複数名にせよ、 MOD のソース・コード中に丁寧にコメントを書き添えることが大事です。その際、コメントには「∩」とか上線のような記号は書きにくいし、英文だけで完全に論理的に書くのも難しいので、プログラミング言語やシェルでよく見かける論理演算記号や括弧 () を使うのがよいかも。
複数の判定を組み合わせようとすると、最終的に連言標準形(CNF)や選言標準形(DNF)と言われるような 複雑な形になることがあります。
OR してから AND
CNF は、論理和の論理積、つまり if (a or x) and (b or y) and (c or z) のようなつなぎかたを好む人向けです。これは、この開発環境では次のように簡単に表現できます。
(this_or_next|eq,"$a",1),(eq,"$x",1),
(this_or_next|eq,"$b",1),(eq,"$y",1),
(this_or_next|eq,"$c",1),(eq,"$z",1),
(訳注: 上記に 6 個のタプル、つまり命令があり、それぞれ仮に T1~T6 と呼ぶとします。T1 と T2 の間などの改行の有無は論理と無関係で、6 命令を 6 行で書いても同じであることに注意して下さい。T1 に付いている this_or_next 修飾子は、その命令が不成立(偽)でも次命令 T2 を実行します。成立(真)なら実行せずに T3 へ続行します。従って、T1 または T2 どちらかの条件が成立したら T3 以降を実行し、それ以外、つまり T1 と T2 が両方とも不成立なら以降の全命令を中止し、ブロックを終了します。T3 まで行くと、同様に T3 または T4 どちらかが成り立つかどうか判定していて、そこでも成立なら 3 行目の T5 以降を続行します。結果的に上記 if (a or x) and (b or y) and (c or z) のような判定をしています。もし、(A or B or C)のように 3 者を or するなら、次の例の最後付近のように 1 つ目と 2 つ目の命令に this_or_next を指定します。4 者以上の or も同様で、連続する判定命令のうち「最後」の判定命令以外に this_or_next です。
AND してから OR
一方、if (a and x) or (b and y) or (c and z) then のように判定命令をつなぐのは DNF の形になります。この開発環境では方法は 2 つあります。
(1) どちらかと言うと直観的な方法
この書き方を思いつくかどうかは、あなたの論理式の知識によって変わってくるかもしれません。次に示すように より長い処理時間と より多くのレジスタを必要とします。
(try_begin),
(eq,"$a",1),(eq,"$x",1),
(assign,reg(1),1),
(try_end),
(try_begin),
(eq,"$b",1),(eq,"$y",1),
(assign,reg(2),1),
(try_end),
(try_begin),
(eq,"$c",1),(eq,"$z",1),
(assign,reg(3),1),
(try_end),
(this_or_next|eq,reg(1),1),
(this_or_next|eq,reg(2),1),
(eq,reg(3),1),
#block continues with whatever you want
(訳注: 上記コードでは、if (a and x) or (b and y) or (c and z) then の内側の 3 つの AND のそれぞれが成立すると対応するフラグを立て、try_end 後に 3 つのフラグのいずれかが立っているかどうか、つまり 3 者の OR を判定しています。reg1~3 の初期化は省略されていますが、予め 3 つとも 0 などに初期化しておく必要があることに注意して下さい。なお、レジスタを 3 つ使わずに reg1 内の どこか 3 つのビットをそれぞれ立てて(つまり 1 か 2 か 4 を加えて)最後に 0 かどうかで見分ける方法もありますが、コードの可読性や保守性が少し悪くなります。理由は 1, 2, 4 のような数字(マジックナンバー)の唐突さと、不使用ビットが不明瞭な点、ビット数が増えた時の変数の最大ビット長の考慮(将来に亘ってつきまとう)などです。たとえ それらを定数として別途 定義したとしても、コードがあちこちに発散して、やはり読みにくいでしょう。)
(2) もっと短くて高速な方法
もう一つの方法は、ド・モルガンの法則を使って CNF の否定から DNF を求めることです。つまり、条件文を if ~( (~a or ~x) and (~b or ~y) and (~c or ~z) ) then のようにします。この形は、この開発環境では次のように書き表わせます。
(try_begin),
(this_or_next|neq, "$a",1),(neq, "$x",1),
(this_or_next|neq, "$b",1),(neq, "$y",1),
(this_or_next|neq, "$c",1),(neq, "$z",1),
(else_try),
(do stuff here),
(try_end),
これが前のコードと論理的に同等ということが、瞬時にはわかりにくいかもしれません。時間をかけて読み解いてみて下さい。
(訳注: ここまでは まだ「単純」な事例です。もっと多岐にわたる複雑な判定や処理になると、try ブロックを深く階層化するのが簡潔だったり、上記「直感的な方法」のようにフラグを使うほうが後続処理にとって都合がよかったりすることも あり得るでしょう。つまり、オールマイティな方法は無いので、都度 何通りかの方法を見つけ、それらの良し悪しを正しく評価し、選べるようにしておきましょう。冒頭の繰り返しになりますが、読み易いコードとメンテし易いコードが常に同じとは限らないし、経時変化します。)
for ループ
(訳注: 原文は説明が前後して不明瞭なので、補足しつつ説明順を変えて訳してあります。)
M&B モジュール・システムの for ループには いくつか種類があります。一番基本的なものは、「try_for_range, ":ループ変数", <下限値>, <上限値>」で、それに続くブロック(一つか複数の命令)が繰り返し実行されます。ブロックの終わりは、(Basic などの言語で Next 文を使うように)この MOD 開発環境では try_end です。
指定されたループ変数に まず 下限値が設定され その下のブロック、つまり try_end までを実行します。次に その変数を +1 してから同じブロックを最初から繰り返し、変数が上限値未満の間 続きます。つまり +1 して上限値になった時点でループを抜けるので、変数が下限値の状態でループ本体(ブロック)を実行することはありません。ループ変数をブロック内で使わないと、反復用変数の未使用について大量の警告が出ます。この警告を避けるには「:unused」または「:unused_変数名」のような名称の変数を使う必要があります [15]。また、「上限 - 1」に向かう反復をスキップするようにループ変数を変更することはできません。下記は通常の例で、反復用変数は ":i" です。
(try_for_range, ":i", 0, 10),
(store_add, ":count", ":i", 1),
(assign, reg0, ":count"),
(display_message, "@{reg0}"),
(try_end),
#This code will display a message on screen counting from 1 to 10.
(訳注: ブロック内では反復用のローカル変数 ":i" と数値 1 を加えて ":count" に代入し、reg0 に写してマップ画面左下の時限メッセージ(と Q キー のメッセージ履歴)として表示しています。":i" は 0 から 9 まで変化するので、結果的に 1 から 10 が表示されます。)
逆に変数をカウントダウンしながらループする「try_for_range_backwards, ":ループ変数", 下限値, 上限値」もあり、「上限値 - 1」から下限値まで下がっていきます。例えば、リストのインデックス付けを壊さずに何か(隊のメンバーなど)を削除するような場合に便利です。
下限と上限の両方が「単純な数値」である必要はなく、手前のコードで設定した変数を使うこともできます。その例については次項で説明します。
もっと特定用途に限定した try_for_* ループがいくつかあります(一覧は既に「全処理中止の制御(阻止)とループ」で上述)。例えば「try_for_agents, <エージェントID>」 と 「try_for_parties, <隊ID>」です。それぞれ、シーン内の全エージェントまたはゲーム内の全ての隊についてループします。ループ変数に指定したローカル変数には、エージェントや隊 の ID 番号が格納されます。 例えば、次のようになります。
(get_player_agent_no, ":player"),
(agent_get_team, ":playerteam", ":player"),
(try_for_agents, ":agent"), #Loops through all agents
(agent_is_alive, ":agent"), #Checks if current agent is alive
(agent_is_human, ":agent"), #If alive, checks if current agent is human (not a horse)
(agent_is_non_player, ":agent"), #If alive and human, checks that the current agent isn't the player
(agent_get_team, ":team", ":agent"), #If alive, human and non-player, gets the current agent's team.
(eq, ":team", ":playerteam"), #Checks that the current alive, human, non-player agent is on the player's team.
#Consequence operations go here to do stuff with this alive, human, non player, ally agent.
(try_end), #Go to next agent
(訳注: 上記コードでは、全エージェントについてループし、存命で人間で「プレイヤー以外」で「プレイヤー側のチーム」なら「#Consequence...」というコメントで示された箇所を実行します。ループするたびに ":agent" というループ変数に各エージェントの ID が(エンジンによって)代入されます。)
訳注: 汎用ループの上限値について補足。
「集落や施設(parties)モジュール」のページにも似た話題があり、少しダブりますが、ここではループ主体で書きます。
下記コードでは、全ての「街」についてループしています。実際のコードは module_simple_triggers.py 内あります。
(try_for_range, ":town", towns_begin, towns_end),
(party_get_slot, ":days_to_completion", ":town", slot_center_player_enterprise_days_until_complete),
(ge, ":days_to_completion", 1),
(val_sub, ":days_to_completion", 1),
(party_set_slot, ":town", slot_center_player_enterprise_days_until_complete, ":days_to_completion"),
(try_end),
このループ変数の上限下限に指定されている値は module_constants.py で次のように定義されています。
towns_begin = "p_town_1"
castles_begin = "p_castle_1"
towns_end = castles_begin
更に p_town_1 や castles_begin は、「集落や施設」(parties)モジュールから生成された ID_parties.py で定義されています。つまり、このループの上限値は、連続する「街の行の塊」の直後に「城の行の塊」があり、かつ その境い目で番号が連続していることを前提にしています。だからもし あなたの独自 MOD で街を増やすなど、その付近の行を変更する場合でも、街と城のあいだで番号が跳んだりしないようにする必要があることが おわかりでしょう(コード側の全ての当該ループを変えない限り)。そもそも、この「街の後に城」のような「暗黙の約束」を設けるやり方はバグの元になり得るし、デバグ効率を恒久的に落とします。
だから、同じ parties モジュール内の "spawn_points_end" でやっているように、街にもエンド・マークとなるダミーのレコードを作って、ループのパラメータから castle の文字を無くしたいところです。ただし、その街の 3D モデルとして微小サイズのものを渡せるかどうかとか、現実にダミーの街のレコードを置いてゲーム・エンジンに怒られないかとか、何がハード・コーディングされていて何を変えてはいけないのかとかについては、訳者は ほとんど試したり調べたりしてません。興味があればどうぞ。
ちなみに、このループ処理がやっていることは、各「街」について、主人公の事業開設完了までの日数を「街」毎のスロットから読んでローカル変数に入れ、それが 1 以上かどうか判定し、そうでないなら try_end までスキップし、そうなら続行して その日数を -1 してスロットに書き戻し、try_end へ合流しています。ge 命令を使うことで、結果的に事業開設の始まっている(開設完了までの日数が 1 以上の)「街」でだけスロットを操作しています。「街」毎のスロットを、街番号であるループ変数 ":town" で指定しています。
Warband をプレイしたことがあれば、お馴染みの処理ですね。では、この for ループがどんなタイミングで呼ばれるかを想像してみて下さい。そしてトリガー全般のことを ざっと理解したら、上の処理が呼ばれる実際のタイミングや、上のループ前後の処理がやっていることを、読み解いてみるとよいです。MOD 開発作業全体の理解が急速に進むかも。
ループ中断
特定のエージェントを見つけるために全エージェントをループしたり、特定の武器を見つけるためにアイテムの一群をループしたりすることがよくあります。探し物が見つかった時点で残りの探索を続行したくないこともあるでしょう。その場合、「ループを中断させる」命令を挿入したいところですが、M&B モジュール・システムには、そのような命令が無く、代わりに別の方法を使う必要があります。その方法は、ループの種類(try_for_*)によって異なります。
方法 1: ループの終端を変更する
(1) 昇順ループ try_for_range を終端変更で中断
上限値(ループ中にループ変数がとる最後の値 + 1)を下限値(ループ変数の初期値)と同値に変えることで、try_for_range ループを簡単に中断できます。 そうすることで、エンジンはループしようとしたときに、既に最終回に達し完了していると判断します。結果として、コードを再度実行せずにループが終了します。(訳注: ループ変数を変えるのではありません。)
これをするには、ループの上限値は変数で指定する必要があり、ループの開始前に上限値を設定しておきます。ループしながら探し物が見つかったら、その変数にループの下限値を代入し、ループのブロックの終端を通ることでループを脱出します。例えば下記のようにします。
#Within a larger (try_for_agents) loop
(assign, ":end", "itm_glaive"), #Set the loop's upper bound
(try_for_range, ":item", "itm_jousting_lance",":end"), #Loop through the lances in the game
(agent_has_item_equipped, ":agent", ":item"), #Check if the agent has the current lance equipped
(agent_set_wielded_item, ":agent", ":item"), #If the current lance is equipped, make the current agent wield it
(assign, ":end", "itm_jousting_lance"), #loop breaker - make the upper bound equal the lower bound - stop looking through the lances
(try_end),
#Continue on within the try_for_agents loop
(訳注: 上の例では下限値が "itm_jousting_lance"、上限値が "itm_glaive" で、どちらも ID_items.py で定義された数値です。ループの手前でローカル変数 ":end" に上限値を入れておき、ループを脱出したくなった時点で同変数に下限値を入れています。他言語の break や Exit For のように本当にループを終わらせるのとは異なり、try_end が現れるまで処理が続くので要注意です。ループを抜けたくなった場所から try_end までに 余計な(実行したくない)コードがある場合は後述「方法 2」のようなフラグや try ブロックを作って try_end までスキップさせる必要があります。いずれにせよ(スキップするにせよ意図的にスキップしないにせよ)ソース上にコメントを書いて明確にすべきです。
ちなみに、上の例のように同じ文字列 "itm_jousting_lance" を try_for_range の行とループ中断時の両方に書くのは可読性が悪くバグの元なので、別の変数で共通化したほうが美しいです。ただし、ゲーム速度やファイル容量などが その分わずかに増すので、高速処理が必須の場面では可読性とのバランスを考慮する必要がありそう。
なお、「ループ脱出文が無い」という この状況は、TaleWorlds 社の公式版 MOD 開発環境での話です。この web 文書の各所で紹介されている K700 氏の Warband Script Enhancer (WSE) を使うと (break_loop) という文でループを脱出できるようになる(らしい)など、開発環境を改善する手立てはあるようです。詳しくは Q&A のスレッド [WB] Warband Script Enhancer vX.X.X for 1.174 の Feb 6, 2015 付近と最新投稿を確認してみて下さい。2023 年秋現在ではメンテされているようです。)
(2) 降順ループ try_for_range_backwards を終端変更で中断
同じ方法が逆方向ループでも使えます。下限(ループ中にループ変数がとる最後の値)が「ループの終了」なので、下限を変数にしておき、ループを抜けたくなった時点で その変数を上限(ループ変数の初期値 + 1)と同じ値に変更し、try_end へ行くようにします。下記は例です。
#Code above...sets the variable ":troop" to some value
(assign, ":array_begin", 0), #Set the loop's lower bound
(try_for_range_backwards, ":i", ":array_begin", 10), #Loop from 9 to 0
(party_slot_eq, "p_main_party_backup", ":i", 0), #Check if the ith party slot for the player's party backup equals 0
(party_set_slot, "p_main_party_backup", ":i", ":troop"), #If it does, set that slot to the variable troop's value
(assign, ":array_begin", 10), #Loop breaker - make lower bound equal the upper bound - don't check other slots
(try_end),
(訳注: try_for_range_backwards に指定するパラメータは「上限 + 1、下限」ではなく「下限、上限 + 1」の順なので要注意です。また、ループ脱出のための変数設定後に try_end までスキップうんぬんについては、昇順ループと同様で、try ブロックやフラグで考慮が必要です。)
方法 2: 条件判定をする
try_for_agents や try_for_parties を中断
これらのループの場合は(上限や下限を指定しないので)「上限」を変更する方法は使えません。代わりに、フラグとなる変数を用意し、ループするブロックの先頭(など)で判定します。ループを抜けたくなった(条件になった)時にフラグの値を変えます。下記の例では ":break_loop" がフラグです。
#Code above...sets pos1 to some value
(assign, ":break_loop", 0), #Set up variable to break loop and a value to test
(try_for_agents, ":agent"), #Loop through all agents
(eq, ":break_loop", 0), #See if the loop-breaker is still true
(agent_is_alive, ":agent"), #If so, keep going; check if the current agent is alive
(agent_is_non_player, ":agent"), #If alive, check it isn't the player
(agent_is_human, ":agent"), #If alive and not the player, check that is is human
(agent_get_position, pos0, ":agent"), #If alive, human and non-player, get it's location and record to pos0
(get_distance_between_positions_in_meters, ":distance_to_target_pos", pos0, pos1), #See how far pos0 is from pos1 and record that in the local variable
(lt, ":distance_to_target_pos", 10), #Check if the alive, human non-player agent is within 10 meters of pos1
(assign, ":agent_at_target", ":agent"), #If so, record this current agent's ID to the local variable "agent_at_target"
(assign, ":break_loop", 1), #Agent Loop Breaker - change the test variable so the equality test fails on the next try
(try_end),
#The loop will break the first time an alive, human, non-player agent is found within 10 meters of the target position pos1
#Further code can now do stuff with the agent that was found
(訳注: 間違いかも。要試行! 上記は「真面目に」フラグを初期化、変更、判定する例です。もっと手っ取り早いのは、フラグなど使わず、ループを抜けたくなった箇所(この例なら try_end のすぐ手前)に「eq, 0, 1」のように偽になる命令を置くことです。目立つコメントさえ付けておけば可読性もずっと良いし、命令が少ない分だけ速度も少し速いはずです。たとえ「途中で抜けたか否か」を後続処理で知りたい場合であっても、上記のような「脱出フラグ」は大抵は不要で、例えば上記なら ":agent_at_target" を事前に -1 などに初期化しておけば済みます。)
ループ延長
特定の条件が成立時にループを中断できるのと同様に、特定の条件が成立時(または不成立時)にループを延長することもできます。下記は例です。
(store_add,":range_end",towns_begin,1), # ":range_end" = towns_begin+1
(try_for_range,":town",towns_begin,":range_end"), # try for towns_begin to towns_begin+1
(eq,":town",":found"), # if condition is true . . .
# more here
(else_try),
(neq,":town",":found"), # if condition is not true . . .
(val_add,":range_end",1), # ":range_end" = ":range_end"+1, i.e. continue the try_for_range
(try_end),
上の例では、条件が満たされなければ try_for_range の上限値に 1 を加え続け、結果的に その都度 ループ回数が 1 回増えます [16]。
(訳注: 上記テクニックが通用するなら、ゲーム・エンジンは上限値を(ループ前に一度だけ読むのではなく)ループを回る度に読み直している、ということになりそう。)
- 備考と出典:
- [14] fisheye 氏と Winter 氏による 3 つの表記のまとめ: Modding Q&A.
- [15] ":unused_ループ変数" について Masterancza/Garedyr 両氏への謝辞: Mount & Blade Modding Discord.
- [16] Winter, M&B Scripting Q&A.
本体の外側の宣言はタブにできない。Python のインデント規定と VC での自動インデント(kalarhan 氏の投稿)。
訳注:
補足すると、この MOD 開発システムの module_*.py では、リストの中に要素としてタプルやリストを並べてコードを書くので、それらタプルやリストの内部では Python の字下げ規定は適用されず、字下げの数やタブ有無は自由です。現に module_*.py の初期状態でも てんで ばらばら で、一定していません。一方で、その外側、module_*.py の最外殻や process_*.py などでは Python 言語の字下げ規定 Python 公式サイトの PEP-8 に従う必要があります。そこには "Use 4 spaces per indentation level."(字下げレベル毎に 4 個の空白を使うように)とか、"Python disallows mixing tabs and spaces for indentation."(字下げにタブと空白の混在は許されない)といったことが書かれています。
本文の VC は Viking Conquest の MOD 開発システム のことで、process_line_correction.py に字下げ自動修正機能が付いている、という話。「タブにできない」については、リスト [] やタプル () の外側の最外殻にあるレベルでのこと。

