Companion Dogs/Mod制作
機能マニュアルを見る犬種マニュアルを見る
Discord に参加
機械翻訳(未校正)。このページはポルトガル語から機械翻訳したもので、校正はされていません。用語や数値が正しくない場合があります。内容に迷ったときは、英語版かポルトガル語の原文を確認してください。英語版を読む

Companion Dogs: Mod制作マニュアル

自分のアドオンの作り方。新しい犬種、あるいは種族まるごとを、Companion Dogs に噛み合う別の Workshop Mod として作ります。

このマニュアルは Mod を書く人向けです。遊ぶだけなら、他の二冊を読んでください。犬が何をするかは 機能、犬種は 犬種 です。

Lua が読めること、動物のリグ付き・アニメーション付きモデルを持っていることを前提にします。アート側のパイプラインはここでは扱いません。モデルファイルが満たすべき条件は第 2 節に挙げます。

このマニュアルが名前を挙げているものはすべて公開された契約であり、Mod の外から呼ばれるためにあります。ここに名前がないものは内部のもので、どのリリースでも予告なく変わり得ます。

1. アドオンとは何か

アドオンは、require=CompanionDogs を宣言し、ロード時に関数をひとつ呼ぶだけの普通の Workshop Mod です。追加するのは犬種ひとつ(体を共有するなら複数)だけで、それ以外は何もしません。

ベースが持っているもの:

  • アニメーションのステートマシン、animset、スケルトン
  • 追従、パス探索、戦闘、狩り、家畜の世話、見張り、欲求、メンテナンス
  • インターフェース一式。犬のウィンドウ、ラジアル、コンテキストメニュー、犬舎、マップの目印
  • 手なずけ、絆、繁殖、子犬、雑種
  • サンドボックス設定と、マルチプレイの同期

あなたのアドオンが持つもの:

  • あなたの動物をカラーメロと違うものにする数値
  • 体のモデル、テクスチャ、肖像、そして moodle があればそのアイコン
  • 世界のどこで見つかるか
  • 名前と説明文を、同梱するすべての言語で

同じ振る舞いの二色でも、ベースから見れば犬種は二つです(名前が二つ、犬舎の項目も二つ)。CD: Cats が五つの毛色でやっているように、共有テーブルひとつから書き起こします。

2. 始める前に必要なもの

動物には media/models_X/Skinned/ の下に自分用のスキン付き .glb が要り、21 本の Rac_* アニメーションクリップが全部そろっている必要があります。クリップはモデルファイル間で継承されません。15 本しかないモデルは、残り 6 本のどれかを求められた瞬間に固まる動物になります。モデルはさらにボーン 60 本以下で、トップノードが単位行列でなければなりません。そうでないとエンジンが毎フレーム例外を投げ、動物は黒い染みとして描画されます。任意のクリップがもう 2 本あります。Rac_WalkLimpFront と Rac_WalkLimpBack(足を引きずる歩行サイクル)で、犬種定義で limpAnim = true にしたときだけ意味を持ちます。

モデルがまだないなら、CD.applyDogModel をベースの体のどれかに向けておけば、それ以外は全部書いて試せます。動物の見た目はカラーメロですが、このマニュアルにあるシステムはすべて動きます。

体のテクスチャと、犬舎ウィンドウ用の肖像も要ります。

コードを書き始める前に、あなたの動物が犬種なのか種族なのかを決めてください。犬種は数値の違う犬です。種族は、いくつかの仕事をそもそも持たない動物です。この選択で書く項目が変わります。第 6 節で扱います。

3. アドオンのファイル構成

これは公開済みで最小のアドオン、CD: Pug の、要点だけを抜いたものです:

CompanionDogsPug/
  42/
    mod.info
    icon.png
    poster.png
    media/
      models_X/Skinned/Pug_Body.glb
      scripts/models_pug.txt
      textures/Body/Pug.png
      textures/CDPortrait_pug.png
      lua/shared/CompanionDogsPug_Breed.lua
      lua/shared/Definitions/animal/PugDefinitions.lua
      lua/shared/Definitions/animal/CompanionDogsPug_Parts.lua
      lua/client/CompanionDogsPug_Moodle.lua
      lua/shared/Translate/EN/IG_UI.json
      lua/shared/Translate/PTBR/IG_UI.json
  common/

そして mod.info:

id=CompanionDogsPug
name=CD: Pug
description=Pugs for Companion Dogs. REQUIRES the Companion Dogs mod (0.6.8 or newer).
poster=poster.png
icon=icon.png
require=CompanionDogs
modversion=0.1.0
pzversion=42.19.0
versionMin=42.18.0

大事なのは require=CompanionDogs の行です。これがあると、ベースの Lua がすべて、あなたのどのファイルよりも先に走ることが保証されます。shared、client、server のどの段階でもです。これがないとあなたのファイルが先にロードされ、CD. の呼び出しはすべて nil への添字になります。

versionMin では「Companion Dogs 0.6.8 以上が必要」と書けません。あの項目はゲーム本体のビルドしか見ませんし、依存 Mod のバージョンを書く項目は mod.info にありません。ベースの下限は次の節のガードで強制し、説明文で言葉にしてプレイヤーへ伝えます。そこに書いておかないと、プレイヤーに出る症状は「動物がいつまでも現れない」だけになります。

4. バージョンガード

アドオンのすべての Lua ファイルの先頭に、あなたのアドオンが必要とする番号を入れて置きます:

local CD = CompanionDogs
if not (CD and CD.registerBreed and (CD.API_VERSION or 0) >= 3) then return end

ベースがない、または古すぎる場合、ファイルは 2 行目で戻り、アドオンは何もしません。犬種も、スポーンも、moodle も、ログの 1 行も出ません。

ガードはファイルごとで、各ファイルは自分が使うものでガードします。PugDefinitions.lua はモデルのヘルパーを使うので、それでガードします:

if not (CompanionDogs and CompanionDogs.applyDogModel and CompanionDogs.DOG_SOUNDS) then return end

グローバルを作ってはいけません。アドオンの中の CompanionDogs = CompanionDogs or {} は、ベースがないという状態を半分だけできたテーブルに変えてしまい、以降のガードが通ってはいけない場面で全部通ります。

5. 犬種を登録する

shared のファイルで、呼び出しはひとつだけです:

CD.registerBreed({
    key = "pug",
    engineBreed = "pug",
    typePrefix = "pug",
    nameKey = "IGUI_PD_Breed_pug",
    litter = { 1, 2 },
    xpMult = { scent = 1.5, combat = 1, obedience = 2.0, hunt = 1.2, herding = 1.0 },
    combatPower = 0.15,
    lethalityCurve = { min = 0.40, max = 1.3 },
    canKill = false,
    canKnockdown = false,
    combatStressMult = 0.9,
    panicThreshold = 0.70,
    bagMult = 0.6,
    sentinelMult = 1.35,
    barkNoiseMult = 1.5,
    alertModeLocked = true,
    loyaltyDecayMult = 0,
    geneRange = {
        strength       = { 0.00, 0.10 },
        aggressiveness = { 0.05, 0.20 },
        resistance     = { 0.00, 0.15 },
        stress         = { 0.55, 0.85 },
    },
    spawns = {
        { id = "pughouse", class = "house", suffix = "|pg", breed = "pug",
          chance = function() return CD.strayChancePerHouse() / CD.PUG_HOUSE_RARITY end },
    },
})

この呼び出しが犬種を挿入し、その三つの動物タイプを登録し、あなたの動物が交雑に参加できるように繁殖の順序を組み直し、スポーンの手順を登録します。

定義が不正なとき、呼び出しは例外を投げずに nil を返すので、ロードはそのまま進みます。却下ごとに、名前入りの行がログに出ます。

必須の項目

項目 中身
key 犬種の識別子。すべての Mod をまたいで一意。リリース後に変えてはいけません。
typePrefix 三つの動物タイプの接頭辞: <prefix>pup、<prefix>female、<prefix>male。体のメッシュを選びます。
nameKey 表示名の翻訳キー。
xpMult スキルごとの習得速度: scent、combat、obedience、hunt、herding。1.0 が標準。
combatPower 打撃の重さ。カラーメロは 0.20、闘犬種は 1.0 超。
lethalityCurve { min, max }。戦闘 0 から戦闘 10 までダメージがどう伸びるか。
geneRange 四つの遺伝子の生まれつきの範囲: strength、aggressiveness、resistance、stress。それぞれ 0 から 1 の中の { low, high }。

任意の項目

項目 既定値 効果
engineBreed key と同じ エンジンに渡す犬種名。テクスチャを選びます。犬種ごとに一意。
descKey IGUI_PD_BreedDesc_<key> 説明文のキー。複数の犬種でひとつのキーを共有できます。
litter ベースの値 一度の出産の子の数 { min, max }。
canKill true false ならゾンビを削りはするが、とどめは決して刺しません。
canKnockdown false true でゾンビを転ばせられます。
combatStressMult 1 戦闘一回で受けるストレスの量。
panicThreshold ベースの値 戦うのをやめるストレスの水準。panicImmune = true なら決してやめません。
bagMult 1 サドルバッグ容量の倍率。
puppySize 0.6 子の見た目の絶対スケール。小型犬種は設定が必須で、しないと子が成体の大きさで生まれます。
sentinelMult 1 最終的な見張りの半径にかかる倍率。
barkNoiseMult 1 警戒吠えの半径と音量にかかる倍率。サンドボックスの騒音設定を切ると、値が何であれどの犬種の吠えもゾンビを引きません。
loyaltyDecayMult 1 一日あたりの忠誠の減りにかかる倍率。0 なら絆は決して薄れません。
alertModeLocked false 動物が常に全開の警戒のまま。飼い主はどこでも静かモードや無音モードにできません。
huntFetchLevel 6 獲物を飼い主のところまで運ぶようになる狩りレベル。
huntDeliverTimeoutMin 5 届けるのを諦めるまで粘るゲーム内の分数。
distract 切 { <kind> = { chance = 0..1, ... } }。モードもレベルも要らずに、犬種が自分から何かを追いかけます。
idleAnimMs ベースの値 待機アニメーションの窓の長さ、ミリ秒。犬より短いクリップのときは設定してください。しないとループが再開して仕草が途中で切れます。
restAnim true false なら待機と警護で伏せずに立ったままになります。
restPoses 伏せ 動物が落ち着くたびに引く休憩ポーズのリスト。任意で出入りの遷移と待機バリエーションを付けられます。自前のクリップとアニメーションノードが要ります。
limpAnim false true で、傷ついた動物が歩くとき足を引きずります。任意のクリップ Rac_WalkLimpFront と Rac_WalkLimpBack(Rac_Walk と同じルートモーションを持つ 1.0 秒の歩行サイクル)がモデルにあるときだけ設定してください。クリップがないと動物はその場で固まったまま歩きます。
bandSkin 切 { base, front, back, cutFront, cutBack }、体のテクスチャ名5つ。傷ついた脚には、傷から出血している間は切り傷のバリエーション、包帯を巻いている間は包帯のバリエーションが付きます。4つのバリエーションは _dogrig/forge/_paw_band.py で作ります。なくても動物は傷を負いますが、跡が表示されないだけです。
voices 犬の音 { bark, growl, idle, wildbark, pet, whine, eat, drink }。
diet 犬のリスト 食べさせてはいけないもの、その動物にとって肉に数えるもの、飼い葉桶から自分で食べるもの。
maleChance 0.5 オスとして生まれる割合、0 から 1。
sterileMale false true にするとその品種のオスは繁殖しません。
species "dog" 繁殖は種族ごとに閉じています。第 6 節。
skills、canBreed、huntMaxPrey すべて有効 構造そのものを切るブロック。第 6 節。

定義に足した数値やフラグは、CD.getBreedDef(animal).field ではなく CD.breedNumber(animal, field) と CD.breedFlag(animal, field) で読み戻します。この二つは項目がない場合を面倒みますし、ベースが変わっても動き続けます。

本能: distract

distract = {
    prey = { chance = 0.25 },
},

これを宣言した犬種は、ときどき何かに気づいて、やりかけのことを放り出して追いかけます。追従中だけでなく待機中にもやります。その間、命令は服従判定がすでに使っている distracted の返事で拒まれます。通るのは「来い」だけで、これは気の散った状態を打ち切ります。

どの種類も同じ任意のつまみを取ります。書かなかったものは Mod の既定値になります:

項目 効果
chance 0..1。その種類のきっかけが何かを見つけたときだけ振られます。「25%」のつもりで 25 と書くと、名前入りの行をログに出して却下されます
radius きっかけが探す範囲のタイル数。そもそも探す種類の場合
durationMin 窓がひとりでに切れるまでのゲーム内の分数
cooldownMin 同じ動物がまた気を散らせるようになるまでのゲーム内の分数

prey の種類

ベース Mod が同梱する唯一の種類です。動物は野生動物を追いかけ、どれを数えるかは classes が選びます:

distract = {
    prey = { chance = 0.25, radius = 6, classes = { tiny = true, small = true } },
},

何を宣言しようと、二つの制限がかかります:

  • あなたの huntMaxPrey を超えることは決してありません。small で頭打ちの犬種は、large を宣言していても鹿を追いません。
  • レベルの門が飛ばされるのは tiny だけです。手なずけたばかりの狩り 0 の動物でも、もうネズミを捕ります。small を宣言すれば犬種はレベルを問わずウサギを追いますが、仕留めるにはラブラドールと同じ狩りレベルが要ります。

サンドボックスの狩りの切り替えが効くのはとどめだけです。動物は追いはしますが、獲物は逃げます。

こうして仕留めた獲物は普通の回収に乗るので、飼い主のところへ運ぶかどうかは犬種の huntFetchLevel が決めます。0 なら初日から届けます。猫が死んだネズミを持ってくるのはこれです。届ける動きは気が散った窓より長く続きます。獲物が死んだ時点で動物はまた命令を聞くようになり、帰り道は自分の huntDeliverTimeoutMin で走ります。

自分の種類を書く

distract はディスパッチャです。自分のものを登録します:

CD.registerDistraction("butterfly", {
    gate  = function(animal, cfg) return animal:isOutside() end,   -- optional, cheap pre-check
    find  = function(animal, cfg) return findButterflyNear(animal, cfg.radius) end,
    drive = function(animal, owner, d, cfg) return chaseIt(animal, d) end,
    stop  = function(animal, d) end,                               -- optional, cleanup
})

あとは他の種類と同じように犬種で宣言します: distract = { butterfly = { chance = 0.10 } }。

gate は Mod の全体スキャン予算の前に走ります。find は予算のあとに走り、重い探索をします。drive はまだ動物を動かしている間 true を返し、終わったら false を返して窓を閉じます。あなたのハンドラは自分の pcall の中で走ります。例外を投げると、Mod は名前入りの行をログに出し、そのセッションの間だけその種類の登録を外し、仲間の残りの機能はそのまま動かします。

今後ベースが使う予定の名前: drink、eat、play。今日この名前で種類を登録することはできますが、ベースが自前のものを出したときに名前を取られます。

休憩ポーズ

休んでいる動物は伏せます。restPoses はその一つのポーズを、動物が落ち着くたびに引くリストに置き換えます。

restPoses = {
    "cdRest",                                          -- the base lie-down
    { var = "cdSit", enter = "cdSitIn", exit = "cdSitOut",
      variation = { "cdSitGroom", "cdSitGroom2" } },
},

各要素はアニメーション変数の名前かテーブルです。var は休憩の間ずっと点いている真偽値です。enter と exit は、落ち着くときと立ち上がるときに一度だけ再生されるパルスです。variation は、ポーズを保っている間にときどき再生される一回きりのパルスのリストで、待機バリエーションと同じ時計で動きます。必須なのは var だけです。

抽選は一様で、休憩に入るたびに行われます。要素が二つなら 50% ずつで、同じものが二回続くこともあります。交互ではありません。

各パルスの長さは idleAnimMs がその変数に指定する値です。だからテーブルの形で渡し、すでに使っていた待機の変数も含めて、全部の変数に長さを与えてください。クリップの実際の長さを入れてください。

idleAnimMs = { cdIdle2 = 3800, cdIdle3 = 3800, cdSitIn = 1375, cdSitOut = 1417,
               cdSitGroom = 19833, cdSitGroom2 = 25167 },

クリップとノードはあなたのもので、ベースのものではありません。各変数にはアドオン側の media/AnimSets/raccoon/idle/ にノードファイルが要り、その m_AnimName が指すクリップはあなたのモデルに存在しなければなりません。モデルにないクリップを指すノードは黙って失敗し、動物はベースの待機で立ったままになります。ポーズのノードにはベースのものと同じく m_ConditionPriority に 10 を、その上で再生するものにはもっと大きい数を与えてください。バリエーションは 11、二つの遷移は 12 です。ノードは何も言わなければクリップをループ再生するので、一度だけ再生するノード、つまり両方の遷移とすべてのバリエーションには <m_Looped>false</m_Looped> が要ります。それがないとクリップは終わった瞬間に最初から再生され、変数が消えるのは次のサーバー tick なので、動物は目に見えて巻き戻って同じ動きを繰り返します。置き場所は idle/ だけにしてください。pathfind/ にあるポーズのノードはルートモーションなしのクリップを再生し、動物をその場で固めてしまいます。

鳴き声

voices がないと、どの犬種も犬の音を使うので、猫が吠えます。まず自分の音を登録します:

CD.registerVoices({
    CDCatMeow = "bark", CDCatGrowl = "bark", CDCatHiss = "bark", CDCatPurr = "bark",
    CDCatPet = "fx", CDCatMeowAmbient = "ambient",
})

そのうえで犬種をそこへ向けます:

voices = { bark = "CDCatMeow", growl = "CDCatGrowl", idle = "CDCatPurr",
           wildbark = "CDCatMeowAmbient", pet = "CDCatPet", whine = "CDCatHiss" },

CD.registerVoices がなくても音は鳴りますが、プレイヤーのカテゴリ別スライダーもゲームの効果音量も無視され、それを指すものはログのどこにも出ません。voices から抜いたキーは犬の音に落ちます。

whine は動物がけがをしたり病気になったりしたときの鳴き声です。eat と drink は食べる・飲むの効果音です。犬のものはループで、動物が食べ終わるとベースが止めます。自分のものもループなら CD.SOUND_LOOPED に入れてください。聞き手が範囲外へ出たときもベースが止めます。API 11 より古いベースはこの三つのキーを無視して犬の音を鳴らします。

第三引数は各サウンドの可聴距離、単位はタイルです。自分のサウンドスクリプトに書いた distanceMax とそろえてください:

CD.registerVoices(map, nil, { CDCatMeow = 22, CDCatGrowl = 10 })

サーバーでは、ベースはその範囲の中にいるプレイヤーにしか音を送りません。範囲を省くと、鳴き声はカテゴリごとの汎用の範囲をもらいます。10 タイル届くはずのシャーが 30 タイル以内の全員に飛びます。 distanceMax を超えると FMOD の逆二乗の減衰は無音へ向かわずに止まるので、素の .ogg はどんな距離でも distanceMin / distanceMax の音量で鳴り続けます。同じ理由で、大きな鳴き声ほど distanceMin は小さく保ってください。

食性

diet がないと、どの犬種も犬と同じものを食べます。中毒を起こす食べ物も、肉に数える食べ物も、飼い葉桶から食む食べ物も同じです。この項目は、その三つのリストをあなたの犬種にだけ差し替えます。

diet = {
    bad          = { Fruits = true },              -- fruit now makes it sick
    badParts     = { onion = false },              -- onion no longer does
    protein      = { Cheese = true },              -- cheese now counts as meat
    proteinParts = { tuna = true },                -- and so does anything with "tuna" in its type
    trough       = { Vegetables = false },         -- it will not graze vegetables
    troughItems  = { ["Base.CatFoodOpen"] = true },
},

書き方が二通りあるのは、ゲーム自身のカテゴリで届く範囲が限られているからです。Cheese は本物の FoodType なので、キーひとつでゲーム内のチーズ全部と、そのカテゴリを使い回す食べ物 Mod のチーズもまとめて覆えます。マグロは違います。開けた缶は他の魚と一緒くたの FoodType = Fish で、未開封の缶は食べ物のタイプを何も宣言しません。そこへ届くための手が badParts と proteinParts です。

どの項目も <key> = true(追加)か <key> = false(ベースにあったものを外す)のテーブルです。 true でも false でもない値は、名前入りの行をログに出して捨てられます。

項目 キーが何か
bad 動物に中毒を起こし、忠誠を減らし、給餌メニューで赤く出る食べ物の FoodType
badParts アイテムタイプの一部、小文字。どこに含まれていても一致します
protein 動物の肉の不足を埋める FoodType。衰弱の moodle が見ているもの
proteinParts アイテムタイプの一部。一致のしかたは badParts と同じ
nonProtein 肉ではないと分かっている FoodType。初めて見る食べ物のタイプにベースが出すログ行を黙らせるだけの項目
trough 飼い葉桶から動物が自分で食べる FoodType と AnimalFeedType
troughItems アイテムタイプそのもの。飼い葉桶に入る、自分の飼料タイプを持たないアイテム用

replace = true にすると、犬のリストではなく空のリストから始まります。あなたの動物が犬とほとんど何も共有しないときに使ってください。猫なら、たいていは足し引きを数個書くほうが楽です。

三つのリストは動物についてくるので、同じ飼い葉桶に並んだ犬と猫は別のものを食べます。例外は Mod 自身の食器で、これは中身のアイテムではなく食事のポイントを溜めるので、満たすときの判定は誰でもベースのリストで行われます。そもそも毒になるものは食器に入れられません。

生まれたときの性別

動物は出現するときにオスかメスを引きます。標準は半々です。maleChance はオスで生まれる割合で、0 から 1 まで。sterileMale はその品種のオスを繁殖から外します。

maleChance = 0.00033,
sterileMale = true,

この組み合わせは三毛猫そのものです。オレンジと黒のモザイクは X 染色体に乗っているので、三毛猫はほぼ必ずメスで、まれに出るオスは XXY、3000 匹に 1 匹ほどで、子を作れません。sterileMale の品種のオスは相手に選ばれず、コンテキストメニューから交配の項目が消えます。メスは普通に繁殖します。

そういう品種のオスには、ゲームが性別を出す場所すべてで「繁殖不能」が付きます。ステータス画面、コンテキストメニュー、野良のスキャン、犬舎のどこでも付くので、なぜ繁殖しないのかがプレイヤーに伝わります。

どちらのフィールドも品種の定義から読まれるので、動物側には何も書かれず、既存のセーブに移行処理は要りません。同じ抽選が出産時の子の性別も決めるので、世界でメスになる品種は生まれる子でもメスのままです。

6. システムを丸ごと切る

五つのスキルが、そのまま五つのシステムです:

スキル 対応するシステム
scent 見張り。ゾンビに気づいて知らせる
combat 戦闘、警護モード、自衛
obedience 芸のツリー
hunt 狩りと、飼い主に付く採集の補正
herding 囲いの中の家畜の世話

なので種族は、持っていないものを宣言します:

skills = { combat = false, herding = false },
canBreed = false,
huntMaxPrey = "tiny",

書かない、または true なら、その動物はそのスキルを持っています。

スキルを切ると経験も入らなくなり、バーとそれに紐づく命令がインターフェースから消えます。

canBreed = false がテーブルの外にあるのは、繁殖にはバーも経験もないからです。この動物は身ごもらず、相手として選ばれることもなく、同じ種類どうしでも繁殖しません。

種族

犬ではなく別の動物を足すアドオンは、それを宣言します:

CD.registerSpecies({ key = "cat",
                     nounKey  = "IGUI_PD_SpeciesNoun_cat",
                     youngKey = "IGUI_PD_Young_cat" })

CD.registerBreed({ ..., species = "cat" })

この項目は、これまで公開されたどの犬種にもありません。ないときは "dog" です。

繁殖は種族ごとに閉じています。命令でも、毎時の受動的な判定でも、デバッグ用のツールでも、両方が同じ種族のときにしか身ごもりません。そしてインターフェースはあなたの動物を犬と呼ばなくなります。 nounKey は "この%1はすでに他の生存者のものです。" のような文に入る裸の名詞で、youngKey は、犬なら 子犬 と出るところに、あなたの種族の子が背負うラベルです。

この二つの名詞が覆うのは %1 を組み込んだ文だけです。dog という語が文そのものに書き込まれている他の文("犬を調べる"、"犬がお腹を空かせています"、"犬がわざと吠える")は、API 7 から種族ごとのキーを取ります。翻訳ファイルに IGUI_PD_ScanStray_cat を同梱すれば、画面上の動物が猫のときベースは IGUI_PD_ScanStray の代わりにそちらを出します。同梱しなかったキーは犬の文に落ちます。接尾辞を受けるキーは次のとおり。ScanStray、ScanTitle、PauseGrowthTip、BadFood、AlertFullDesc、 AlertQuietDesc、AlertLockedTip、HuntModeDesc、すべての Trick*Desc、TrickDistractDone、 TrickNeedsBag、TrickGotoPick、TrickSniffNoItem、TrickFetchNoItem、AlertGroup、DogLost、すべての Refused_*、KennelStatusNoData、そして世話の moodle(病気、衰弱、空腹、渇き、休息、悲しみ)の Moodle_*_desc。画面に動物が出ていない文(登録簿のタブ、 "まず動物と仲良くなってください")は、ベースの時点ですでに種族に依らない書き方です。猫はこれを 14 言語ぶん全部同梱しています。その IG_UI.json をひな型にコピーしてください。

動物の group を読むのは、デバッグのスポーンメニューとクリップビューアだけで、 IGUI_Animal_Group_<group> を通します。振る舞いのヘルパーのあとに t.group = "cat" を置き、そのキーを同梱してください。しないと、あなたの種族はそこに犬の名前で並びます。

species を登録せずに宣言しても致命的ではありません。門は生の文字列を比べるので、あなたの動物は自分の種族の外では繁殖を断り続けます。犬のほうへ落ちるのは二つの名詞だけで、ログがそれを名指しで書きます。

API 6 より古いベースでは、この項目はログ行も出さずに無視され、あなたの動物は犬として繁殖します。犬と決して交わらせたくないなら、API 6 でガードするか、ベースが古い間は canBreed = false のままにしてください。猫がやっているのが後者です。

huntMaxPrey は、存在している狩りの中で獲物の大きさに上限をかけるもので、hunt = false の代わりにはなりません。取る値は "tiny"(マウス、ネズミ、リス)、"small"(ウサギ、アライグマ)、"large" (鹿。既定で、上限なし)です。上限より大きい獲物を、あなたの動物は追いも指しもしません。この段の外の値は "large" に落ち、名前入りの行をログに出します。

塞いだ選択肢はラジアルからもコンテキストメニューからもウィンドウからも消え、サーバーはメッセージなしでそれを断ります。ここに挙げた項目はどれも翻訳キーを増やさないので、何ができないかは犬種の説明文で伝えてください。

7. 動物定義

エンジンはメッシュを犬種ではなく動物タイプに結び付けるので、あなたの犬種には自分用のタイプが三つ要ります。近道は、既存のアドオンのファイルをコピーして名前を変えることです。

三つの部品を、Definitions/animal/ の下の shared のファイルひとつに:

AnimalDefinitions.stages["pug"] = {}
AnimalDefinitions.stages["pug"].stages = {}
AnimalDefinitions.stages["pug"].stages["pugpup"] = {}
AnimalDefinitions.stages["pug"].stages["pugpup"].ageToGrow = 3 * 30
AnimalDefinitions.stages["pug"].stages["pugpup"].nextStage = "pugfemale"
AnimalDefinitions.stages["pug"].stages["pugpup"].nextStageMale = "pugmale"
AnimalDefinitions.breeds["pug"].breeds["pug"].texture = "Pug"
AnimalDefinitions.breeds["pug"].breeds["pug"].textureMale = "Pug"
AnimalDefinitions.breeds["pug"].breeds["pug"].rottenTexture = "Raccoon_Rotting"
AnimalDefinitions.breeds["pug"].breeds["pug"].invIconMale = "CDDogPaw_64"
AnimalDefinitions.breeds["pug"].breeds["pug"].invIconMaleDead = "CDDogPawDead_64"
local pugfemale = {}
CD.applyDogModel(pugfemale, "Pug_Body")
CD.applyDogBehaviour(pugfemale)
pugfemale.female = true
pugfemale.babyType = "pugpup"
pugfemale.minSize = 1.5
pugfemale.maxSize = 2.0
pugfemale.minWeight = 5
pugfemale.maxWeight = 8
pugfemale.wildFleeTimeUntilDeadTimer = CD.STRAY_NO_BLEEDOUT
pugfemale.breeds = AnimalDefinitions.breeds["pug"].breeds
pugfemale.stages = AnimalDefinitions.stages["pug"].stages
pugfemale.genes = AnimalDefinitions.genome["dog"].genes
AnimalDefinitions.animals["pugfemale"] = pugfemale

ベースが用意しているヘルパー:

ヘルパー 埋めてくれるもの
CD.applyCompanionModel(t, "Body_Model") 体のモデル、スケルトン、解体後のテクスチャ、animset
CD.applyCompanionBehaviour(t) 仲間に必要なエンジン側の振る舞いフラグ一式
CD.applyCompanionAvatar(t) 動物ウィンドウの肖像用カメラ
CD.COMPANION_SOUNDS 足音と動作音の共有テーブル
CD.defineGenome("species") 新しい種族のゲノム。標準の遺伝子リスト付き(API 7)
CD.STRAY_NO_BLEEDOUT おびえた野良が出血死しないようにする値
CD.defineCompanionParts(typePrefix, engineBreed, meat) 三つの段階すべての解体部位。meat は任意で { item, minNb, maxNb, pupMinNb, pupMaxNb }。省くとベースの犬肉が取れます。エンジンは部位ごとの個数も満腹度も死体の大きさで掛けるので、大きなサイズを持つ種族(猫は見た目のスケールに 2.5 から 3.5 を使います)は、基礎の満腹度が小さく個数の少ない自前のアイテムが要ります

種族に依らない名前は API 7 で入りました。古いほうの CD.applyDogModel、CD.applyDogBehaviour、 CD.applyDogAvatar、CD.DOG_SOUNDS、CD.defineDogParts、CD.DogMoodles は、同じ関数とテーブルの最初の名前です。0.7.3 より古いベースでもロードしたいアドオンは、古い名前でガードして、新しいほうがあればそちらを取ります: local applyModel = CD.applyCompanionModel or CD.applyDogModel。

animset は "raccoon" のままにしてください。分岐させた animset はステートマシンをロードしないので、動物はその場で固まったまま立っています。

ゲノムは使い回します。犬種はベースのリストを指します: AnimalDefinitions.genome["dog"].genes。新しい種族は CD.defineGenome("cat") を一度呼び、返ってきたテーブルをすべての段階に渡します。エンジンが読むのは各段階の genes のリストだけで、ゲノムのキーは決めごとに過ぎないので、あなたの種族は自分の遺伝子リストを持たずに自分の項目を得られます。

mate を宣言してはいけません。あると、エンジンはどの動物ゾーンの中でも自前の交配を回します。これは Mod の繁殖ブロックもサンドボックス設定も無視し、絆のない子犬を作ります。ベースは起動時にこの項目を消しますが、それを当てにしないでください。

部位の定義は必須です。専用のファイルに 3 行:

if CompanionDogs and CompanionDogs.defineDogParts then
    CompanionDogs.defineDogParts("pug", "pug")
end

これがないと、あなたの動物を解体したときにバニラのコードが落ちます。部位の定義をタイプと犬種で引いて、 nil に当たるからです。

CD.applyDogAvatar の肖像のズームは犬に合わせてあり、アバターのズームは倍率です。動物が小さいほど数値は大きくなります。犬に対する大きさの比でズームを割り、オフセットには掛けてください。しないと、動物は自分の肖像の下のほうに小さく写ります。

8. モデルスクリプト

media/scripts/ の下のファイルひとつが、あなたのモデルの存在をゲームに伝えます:

module Base
{
    model Pug_Body
    {
        mesh = Skinned/Pug_Body,
        shader = animalEffect,
        static = false,
        animationsMesh = PugAnim,
        attachment head
        {
            offset = -0.0061 0.1671 0.0507,
            rotate = -179.0 -5.0 95.0,
            bone = Dummy01,
        }
    }

    animationsMesh PugAnim
    {
        meshFile = Skinned/Pug_Body,
        keepMeshAnimations = true,
    }
}

keepMeshAnimations = true は、モデルファイルの中のクリップを使える状態に保ちます。これがないとモデルはロードされますが、決してアニメーションしません。

head のアタッチメントは、帽子などが載る場所です。サドルバッグをあなたの動物に合わせたいなら、 saddlebags_l、saddlebags_r、saddlebags_c を同じやり方で背骨のボーンに足します。この数値は、ゲーム内で動物を見ながら少しずつ動かして出してください。モデルから測った値はバッグをメッシュの幾何中心に置きますが、体の上で見栄えがする位置はそこではありません。

9. その動物が現れる場所

野良の判定は建物ごと、チャンクごとに、そのチャンクが初めてロードされたときに振られます。判定は手順のリストとして書き、犬種定義の中の spawns に置くか、CD.registerStraySpawns(list) に渡します。

spawns = {
    { id = "pughouse", class = "house", suffix = "|pg", breed = "pug", indoor = 85,
      chance = function() return CD.strayChancePerHouse() / CD.PUG_HOUSE_RARITY end },
    { id = "pugpetvet", class = "petvet", suffix = "|pv", breed = "pug",
      chance = function() return CD.PUG_PETVET_CHANCE * CD.dogSpawnMultiplier() end },
},
キー 中身
id 手順の名前。あなた自身のログのため
class 判定をかける建物のクラス
chance パーセントを返す関数。判定の時点でサンドボックスの倍率を読めるようにするためです。
suffix 永続ストアのキー。すべての Mod をまたいで一意で、決して変えません。
breed スポーンさせる犬種のキー
gate 任意。真偽値を返す関数で、判定の前に見られます。ハスキーは CD.isWinter を使っています。
indoor 路上ではなく建物の中で生まれる確率のパーセント。既定は 35。

自分の確率には CD.dogSpawnMultiplier() を掛けるか、CD.strayChancePerHouse() を自分の珍しさで割ってください。どちらもプレイヤーのサンドボックス設定を効かせたままにします。

indoor は好みの指定です。そのチャンクの中に空いた屋内タイルがない建物では、外のタイルに落ちます。

建物のクラス

ベースは四つ登録しています:

クラス 一致するもの
house 店舗ではない住宅
police 警察署とその類
petvet ペットショップと動物病院
farm 農場の建物。町の外でも許されます

どれも合わないなら、自分で登録します:

CD.registerBuildingClass("mineshaft", function(def) return def:isX() end,
                         { skipUrbanGate = true })

一致の関数は建物の定義を受け取り、保護された呼び出しの中で走るので、その中のエラーでチャンクのロードが止まることはありません。skipUrbanGate は、市街ではないチャンクでもそのクラスの判定を通します。農場や森の小屋にはこれが要ります。exclusive は設定しないでください。ベースのクラスどうしは排他ですが、アドオンのクラスはその上に重ねて一致します。

すでに使われている接尾辞

接尾辞は、「この建物はもう判定済み」を保存するキーの一部です。同じ接尾辞を使う二つの Mod は、互いの判定を壊します。

どの接尾辞も縦棒で始まります。使用中のものは次のとおり:

  • ベース: 空の接尾辞と、g、h、hv、bc、gh、hh、bh
  • ロットワイラー: rw、r
  • ドーベルマン: db、dm、dh
  • ラブラドール: lb、lk
  • パグ: pg、pv
  • マリノア: ml、mm、mh
  • 猫: ct、cv、cs のどれかで始まる接尾辞すべて(毛色ごとに一組)

10. 犬種の moodle

moodle は client のファイルです。CD.DogMoodles にテーブルを足すと、ベースがそれを描き、追跡し、後始末します:

CD.DogMoodles[#CD.DogMoodles + 1] = {
    id = "shadow",
    breed = "pug",
    nameKey = "IGUI_PD_Moodle_Shadow",
    descKey = "IGUI_PD_Moodle_Shadow_desc",
    icon = "CD_Moodle_Shadow",
    fg = "CD_MoodShadowFG",
    tintR = 0.92, tintG = 0.52, tintB = 0.68,
    condition = function(player, dog)
        if not dog or dog:isDead() then return 0 end
        if CD.getBreed(dog) ~= "pug" then return 0 end
        if CD.isDisloyal(dog) then return 0 end
        return 1
    end,
    apply = function(player, dog, elapsedMin)
        CD.relieveMood(player, CD.moodleReliefPerMin() * elapsedMin)
    end,
}

condition は段階を返し、出さないときは 0 を返します。apply は前回の呼び出しからのゲーム内の分数を受け取るので、効果には elapsedMin を掛けてください。

アイコンは 32 から 128 ピクセルまで、6 サイズ同梱します。インターフェースはプレイヤーの拡大率でサイズを選ぶので、足りないサイズは空の四角として出ます。アイコンはバニラの moodle の列ではなく、Mod 自身の列に出ます。

11. フック

ベースがサーバー側で呼ぶ関数のリストが二つあります。shared のファイルから足してください。ハンドラはそれぞれ自分の保護された呼び出しの中で走るので、あるアドオンのエラーでベースが止まることはありません。

CD.onHuntDelivered は API 2 からで、動物が仕留めた獲物を飼い主の足元に落とした直後に呼ばれます:

CD.onHuntDelivered[#CD.onHuntDelivered + 1] = function(animal, owner)
    -- server side, so this is the right place to write mod data
end

CD.onUpkeepStress は API 3 からで、メンテナンスの周期ごとに、満たされない欲求ぶんのストレスをベースが足したあと、それが書き込まれる前に呼ばれます。返した数値が合計に足されます:

CD.onUpkeepStress[#CD.onUpkeepStress + 1] = function(animal, needsStress)
    return CD.pugHeatRatio(animal) * CD.PUG_HEAT_STRESS_MAX
end

メンテナンスは周期ごとに値を計算し直して上書きするので、外から書いたものは次のティックで消えます。暑さ、寒さ、雨のように環境から来る条件には、このフックを使ってください。

マルチプレイでは、client は Mod データを決して書きません。client 側で書くと次の同期でサーバーの写しを上書きしてしまい、症状は数分後に無関係なところに出ます。作業は server のファイルか、この二つのフックの中で行ってください。

SERVER TUNING パネルの数値は普通の CD.* 定数で、アドオンはロード時に shared か server のファイルから代入できます。API 9 からは、サーバーがロードを終えた時点で有効な値が、そのつまみの既定値になります。管理者が上書きするまで残り、リセットボタンはその値に戻り、パネルもそれを既定値として表示します。古いベースでは、パネルは起動のたびに出荷時の値を書き戻していました。代入はロード時に行い、後のイベントからは行わないでください。さもないと、どれかのつまみを次に変えたときに出荷時の値に戻ります。

12. 翻訳キー

media/lua/shared/Translate/<LANG>/ の下に、言語フォルダごとに IG_UI.json をひとつ同梱します。ベースは 14 言語ぶん同梱しています。EN、PTBR、CH、CN、DE、ES、FR、IT、JP、KO、RU、TH、UA、VI です。

犬種に要るキー:

キー どこに出るか
IGUI_PD_Breed_<key> 犬種名。Mod のインターフェースのあらゆる場所
IGUI_PD_BreedDesc_<key> 動物のカードに出る説明文
IGUI_Breed_<engineBreed> ゲーム自身の画面に出る犬種名
IGUI_AnimalType_<type> 三つのタイプそれぞれにひとつ
種族のキー CD.registerSpecies に渡した nounKey と youngKey
moodle のキー 名前と説明文
{
    "IGUI_Breed_pug": "Pug",
    "IGUI_AnimalType_pugmale": "Pug (male)",
    "IGUI_AnimalType_pugfemale": "Pug (female)",
    "IGUI_AnimalType_pugpup": "Pug (puppy)",
    "IGUI_PD_Breed_pug": "Pug",
    "IGUI_PD_BreedDesc_pug": "The house dog: the first companion whose worth is not fighting."
}

翻訳した文字列の中のパーセント記号そのものは、それを出す画面を落とします。フォーマッタがプレースホルダとして読むからです。バイトオーダーマークの付いた言語ファイルはコンパイルされず、しかもエラーは壊れているファイルとは別のファイルを名指しします。

13. セーブが覚えているもの

あなたの識別子のうち三つは、セーブファイルに書き込まれ、以後ずっと読み戻されます:

  • key。あなたの犬種の動物すべてに保存される犬種の識別子
  • typePrefix。エンジンが保存する動物タイプを組み立てるもの
  • suffix。「判定済み」ストアのキー。スポーンの手順ごとにひとつ

三つとも、リリース後には改名できません。key を改名すると、その犬種の生きている動物が全部みなしごになります。suffix を改名すると、既存のどのセーブでも全部の建物がもう一度判定され、何ヶ月も遊んだ世界があなたの動物で埋まります。

新しい接尾辞は、すでに動いている世界へ新しい犬種を届ける手でもあります。CD: Cats は新しい四つの毛色に新しい接尾辞を与えたので、既存のセーブにも出るようになり、元からいた黒猫は古い接尾辞のままで再判定されませんでした。

その犬種の動物が生きているセーブからアドオンを外すと、その動物は失われます。エンジンは、もう知らないタイプの動物を捨てるからです。ベースは落ちず、絆も犬舎もきれいに畳まれます。それを Workshop ページに書いてください。

あなたのアドオンがない場合、ベースは訊かれたものすべてについて既定の犬種に落ちるので、あなたの Mod なしで開いたセーブも読めます。

14. 公開する前に

  • ガードがすべての Lua ファイルにあり、実際に使う API を書いている。
  • ベースのバージョンの下限が、説明文に言葉で書いてある。versionMin では言えません。
  • 初回ロードのログを読んだ。読まないと CD.registerBreed は黙って却下しますが、却下ごとに名前入りの行が出ます。
  • CD.defineDogParts を呼んでいる。自分の動物を一頭解体して、落ちないことを見た。
  • key、typePrefix、スポーンの接尾辞が一意で、今後も変える必要がない。
  • 小型犬種は puppySize を設定し、実際に子を見て確かめた。
  • 別の種族なら、CD.registerVoices を呼び、whine も含めて voices を埋めた。そうでないと犬のように吠えたりクンクン鳴いたりします。
  • moodle のアイコンが 6 サイズすべてある。
  • 同梱するどの言語フォルダにも全部のキーがある。キーが欠けると画面にキーがそのまま出ます。
  • シングルプレイだけでなくマルチプレイでも試した。位置、所有権、Mod データは、どれもあちらでは挙動が違います。
  • 動物が生きているセーブからアドオンを外さないよう、Workshop ページで警告している。

15. よくある失敗

症状 正体
「動物が全然出てこない」 ほぼ必ずガードです。アドオンが要求する API を入っているベースが持っていないので、早々に戻って何も登録していません。
「出てくるが固まっている」 animset が "raccoon" ではないか、モデルにクリップが足りていません。
「黒い染みで描画されてログが溢れる」 モデルのボーンが 60 本を超えています。
「解体するとクラッシュする」 CD.defineDogParts を呼んでいません。
「子が成体の大きさで生まれる」 小型犬種が puppySize を設定していません。この値は絶対値で、掃き出しのたびに当て直されるので、タイプの大きさの範囲では覆えません。
「自分の二つの犬種が入れ替わり続ける」 engineBreed を共有しています。接頭辞はメッシュを選ぶもので、同じ系統なら共有できます。engineBreed はテクスチャを選び、その系統の中で犬種を見分けるものなので、犬種ごとに一意でなければなりません。
「音は鳴るが音量スライダーを無視する」 その音について CD.registerVoices を呼んでいません。
「シングルプレイでは動くのにサーバーだとおかしい」 どこかが client で Mod データを書いています。server へ移してください。
「戦闘を切ったのにゾンビと戦った」 動いているベースが API 5 より古いのです。そのベースはこの項目を知らず、無視します。項目が無視されたままロードさせるのではなく、ガードでそのベースを弾いてください。

このマニュアルに間違いや足りないものがありますか。 Discord で教えてください。