Companion Dogs/Моддинг
Перейти к руководству «Возможности»Перейти к руководству «Породы»
Зайти в Discord
Автоперевод, без проверки. Эта страница переведена машиной с португальского, и её никто не вычитывал. В терминах и числах возможны ошибки. Если сомневаетесь, сверьтесь с английской версией или с оригиналом на португальском. Читать на английском

Companion Dogs: руководство по моддингу

Как собрать свой аддон: новую породу или целый новый вид, отдельным модом Мастерской, который подключается к Companion Dogs.

Это руководство для тех, кто пишет моды. Если вы просто хотите играть, читайте два других: Возможности о том, что умеет собака, и Породы о породах.

Предполагается, что вы читаете Lua и что у вас есть модель животного с ригом и анимацией. Пайплайн графики здесь не разбирается. Раздел 2 перечисляет, чему должен удовлетворять файл модели.

Всё, что названо в этом руководстве, является публичным контрактом и предназначено для вызова извне мода. То, что здесь не названо, является внутренним и может измениться в любой версии без предупреждения.

1. Что такое аддон

Аддон это обычный мод Мастерской, который объявляет require=CompanionDogs и при загрузке вызывает одну функцию. Он добавляет одну породу (или несколько, если они делят одно тело) и больше ничего.

База владеет:

  • машиной состояний анимации, набором анимаций и скелетом
  • следованием, поиском пути, боем, охотой, выпасом, дозором, потребностями и обслуживанием
  • всем интерфейсом: окном собаки, радиальным меню, контекстным меню, вольером, меткой на карте
  • приручением, привязанностью, разведением, щенками и метисами
  • настройками песочницы и репликацией в мультиплеере

Ваш аддон владеет:

  • числами, которые отличают ваше животное от Карамело
  • моделью тела, текстурой, портретом и, если он есть, иконкой мудла
  • тем, где его находят в мире
  • его именем и описанием на каждом языке, который вы поставляете

Два окраса с одинаковым поведением для базы всё равно две породы (два имени, две записи в вольере); вы пишете их из одной общей таблицы, как CD: Cats делает со своими пятью окрасами.

2. Что нужно перед началом

Вашему животному нужен собственный скиннутый .glb в media/models_X/Skinned/, с полным набором из 21 клипа анимации Rac_*. Клипы не наследуются между файлами моделей: модель с пятнадцатью клипами даёт животное, которое замирает при первом же обращении к одному из остальных шести. Модель также должна укладываться в 60 костей и иметь верхний узел в единичной матрице, иначе движок кидает ошибку каждый кадр, а животное рисуется чёрным пятном. Ещё два клипа необязательны, 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.info нет. Нижняя граница базы держится проверкой из следующего раздела и объявляется игрокам в вашем описании. Напишите её там, словами, иначе единственный симптом для игрока это животное, которое никогда не появляется.

4. Проверка версии

Ставьте это в начало каждого Lua-файла аддона, с тем номером, который нужен вашему аддону:

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

Если базы нет или она слишком старая, файл выходит на второй строке, и аддон не делает ничего: ни породы, ни появления, ни мудла, ни строки в логе.

Проверка ставится в каждом файле, и каждый файл проверяет то, чем пользуется. 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 идентификатор вашей породы, уникальный среди всех модов. Не меняйте его после релиза.
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. Каждый это { low, high } в пределах от 0 до 1.

Необязательные поля

Поле По умолчанию Что делает
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 (циклы ходьбы на 1.0 секунды с тем же root motion, что у Rac_Walk). Без клипов животное шагает, застыв на месте.
bandSkin выключено { base, front, back, cutFront, cutBack }, пять имён текстур тела. На раненой лапе животное носит вариант с порезом, пока рана кровоточит, и вариант с повязкой, пока она наложена. Четыре варианта соберите скриптом _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.breedNumber(animal, field) и CD.breedFlag(animal, field), а не через CD.getBreedDef(animal).field. Они справляются с отсутствующим полем и продолжают работать, когда база меняется.

Инстинкт: distract

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

Порода, которая это объявила, время от времени что-то замечает и бросается за этим, бросив своё занятие. Она делает это и в режиме Остаться, и в режиме Следовать. Пока окно открыто, приказы возвращаются отказом с тем же сообщением distracted, которое уже использует бросок послушания. Единственная команда, которая проходит, это Ко мне, и она отменяет отвлечение.

Все типы принимают одни и те же необязательные настройки. Всё, что вы не указали, берётся из значений мода по умолчанию:

Поле Что делает
chance 0..1, бросается только тогда, когда триггер типа что-то нашёл. Запись 25 вместо «25%» отклоняется с именованной строкой в логе
radius сколько клеток обыскивает триггер, если этот тип вообще ищет
durationMin игровых минут, сколько окно держится, прежде чем истечёт само
cooldownMin игровых минут до того, как то же животное сможет отвлечься снова

Тип prey

Единственный тип, который поставляет базовый мод. Животное бросается за дикими животными, а 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 выполняется до общего бюджета сканирования, который держит мод. find выполняется после бюджета и делает дорогой поиск. drive возвращает true, пока продолжает вести животное, и false, когда закончил, что закрывает окно. Ваш обработчик выполняется в собственном pcall. Если он падает, мод пишет в лог именованную строку, снимает этот тип с регистрации до конца сессии и оставляет остального компаньона работать.

Имена, зарезервированные под будущие типы базы: 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>. Без него клип начинается заново, едва закончившись, и животное заметно дёргается назад и повторяет движение, потому что переменная гаснет только на следующем тике сервера. Держите их только в idle/. Узел позы в pathfind/ играет клип без root motion и замораживает животное там, где оно стоит.

Голос

Без 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 })

На сервере база отправляет звук только тем игрокам, кто внутри этой дальности. Не укажете дальность, и голос получит общую дальность по категории: шипение, которое несёт на десять клеток, уйдёт всем в пределах тридцати. За 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, поэтому один ключ покрывает любой сыр в игре и в любом моде на еду, который переиспользует эту категорию. С тунцом не так: открытая банка это FoodType = Fish, вместе со всей остальной рыбой, а запечатанная банка не объявляет тип еды вообще. badParts и proteinParts это способ дотянуться до таких случаев.

Каждое поле это таблица вида <key> = true (добавить) или <key> = false (убрать то, что было у базы). Значение, отличное от true или false, отбрасывается с именованной строкой в логе.

Поле Что у него за ключи
bad FoodType еды, от которой животное болеет, теряет преданность и которая красной строкой идёт в меню кормления
badParts кусок типа предмета, в нижнем регистре, ищется в любом месте строки
protein FoodType, который закрывает мясной долг животного, тот самый, за которым следит мудл Ослаблен
proteinParts кусок типа предмета, поиск такой же, как у badParts
nonProtein FoodType, про который вы знаете, что это не мясо. Он только глушит строку лога, которую база пишет для незнакомого ей типа еды
trough FoodType и AnimalFeedType, которые животное само ест из кормушки
troughItems полный тип предмета, для лежащего в кормушке предмета без собственного типа корма

replace = true начинает с пустого списка вместо собачьего. Берите его, когда у вашего животного почти нет общего с собакой; кошку обычно проще описать горстью добавлений и удалений.

Три списка привязаны к животному, поэтому собака и кошка у одной кормушки едят из неё разное. Исключение это собственная миска мода: она хранит очки еды, а не предмет, которым её наполнили, поэтому наполнение идёт по базовому списку для всех. Ядовитое в миску всё равно не положить.

Пол при рождении

Каждое животное при появлении получает пол случайно, поровну. maleChance задаёт долю, рождающуюся самцами, от 0 до 1, а sterileMale убирает самцов породы из разведения.

maleChance = 0.00033,
sterileMale = true,

Эта пара и есть трёхцветная кошка. Мозаика из рыжего и чёрного лежит на X-хромосоме, поэтому трёхцветная почти всегда самка, а самец, который всё же появляется, это XXY, примерно один на три тысячи, и потомства он не даёт. Самца породы с 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. Любое другое предложение, где слово собака вписано прямо в текст ("Осмотреть собаку", "Ваша собака голодна", "Собака нарочно лает"), начиная с 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_*_desc мудлов ухода (Sick, Weak, Hunger, Thirst, Rested, Grief). Предложения, где животного на экране нет (вкладка реестра, "Сначала подружитесь с животным"), в базе уже нейтральны к виду. Кошка поставляет их все на четырнадцати языках; берите её 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. Определения животного

Движок привязывает меш к типу животного, а не к породе, поэтому вашей породе нужны три собственных типа. Самый короткий путь это скопировать файл существующего аддона и переименовать.

Три куска, в одном shared-файле в Definitions/animal/:

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") модель тела, скелет, текстуры разделки и набор анимаций
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.

Набор анимаций должен остаться "raccoon". Форкнутый набор анимаций не загружает свою машину состояний, и животное стоит на месте застывшим.

Переиспользуйте геном. Порода собаки указывает на список базы: AnimalDefinitions.genome["dog"].genes. Новый вид один раз вызывает CD.defineGenome("cat") и отдаёт каждой стадии таблицу, которую тот вернул. Движок читает только список genes каждой стадии, а ключ генома это соглашение, поэтому ваш вид получает свою запись без собственного списка генов.

Не объявляйте mate. С ним движок запускает своё родное спаривание внутри любой зоны животных, которое игнорирует блок разведения мода и его настройки песочницы и даёт щенков без привязанности. База стирает это поле при запуске, но не полагайтесь на это.

Части обязательны. Три строки в отдельном файле:

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 ключ постоянного хранилища. Уникален среди всех модов и никогда не меняется.
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. Классы базы эксклюзивны между собой, а класс аддона срабатывает поверх них.

Уже занятые суффиксы

Суффикс это часть ключа, под которым сохраняется «это здание уже разыграно». Два мода с одинаковым суффиксом портят броски друг друга.

Каждый суффикс начинается с вертикальной черты. Уже заняты:

  • База: пустой суффикс, а также g, h, hv, bc, gh, hh, bh
  • Ротвейлер: rw, r
  • Доберман: db, dm, dh
  • Лабрадор: lb, lk
  • Мопс: pg, pv
  • Малинуа: ml, mm, mh
  • Кошки: любой суффикс, который начинается с ct, cv или cs (свой набор на каждый окрас)

10. Мудл породы

Мудл это клиентский файл. Вы дописываете таблицу в 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 пикселей. Интерфейс выбирает размер по масштабированию у игрока, а недостающий размер выглядит пустым квадратом. Иконка появляется в собственной полосе мода, а не среди ванильных мудлов.

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

Обслуживание пересчитывает и перезаписывает значение каждый цикл, поэтому всё, что записано снаружи, теряется на следующем тике. Берите хук для состояния, которое приходит из окружения, вроде жары, холода или дождя.

В мультиплеере клиент никогда не пишет mod data. Запись со стороны клиента перетирает копию сервера на следующей синхронизации, а симптом всплывает минутами позже и в другом месте. Делайте работу в серверном файле или в одном из этих хуков.

Числа в панели SERVER TUNING это обычные константы CD.*, и аддон может присвоить одну из них при загрузке из shared или серверного файла. Начиная с API 9 панель берёт значение, действующее на момент, когда сервер закончил загрузку, как значение по умолчанию для этой ручки: оно держится, пока админ не впишет своё, кнопка сброса возвращает к нему, и панель показывает его как значение по умолчанию. На более старой базе панель при каждом запуске записывала обратно заводское значение. Присваивайте при загрузке, а не из более позднего события, иначе следующее изменение любой ручки вернёт заводское значение.

12. Ключи перевода

Поставляйте по одному IG_UI.json на каждую папку языка, в media/lua/shared/Translate/<LANG>/. База поставляет четырнадцать: EN, PTBR, CH, CN, DE, ES, FR, IT, JP, KO, RU, TH, UA, VI.

Ключи, которые нужны породе:

Ключ Где он виден
IGUI_PD_Breed_<key> имя породы, везде в интерфейсе мода
IGUI_PD_BreedDesc_<key> описание на карточке животного
IGUI_Breed_<engineBreed> имя породы на собственных экранах игры
IGUI_AnimalType_<type> по одному на каждый из ваших трёх типов
ваши ключи вида nounKey и youngKey, которые вы передали в CD.registerSpecies
ваши ключи мудла имя и описание
{
    "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 дал своим четырём новым окрасам новые суффиксы, поэтому они начали появляться в существующих сохранениях, а исходная чёрная кошка сохранила старый суффикс и заново не разыгрывалась.

Снятие аддона с сохранения, где живут животные этой породы, теряет их. Движок выбрасывает животное, тип которого он больше не знает. База не падает, а привязанность и вольер деградируют чисто. Напишите об этом на своей странице Мастерской.

Если вашего аддона нет, база по любому запросу откатывается к породе по умолчанию, поэтому сохранение, открытое без вашего мода, читается.

14. Перед публикацией

  • Проверка стоит в каждом Lua-файле и называет тот API, которым вы на самом деле пользуетесь.
  • Нижняя граница версии базы написана в вашем описании, словами. versionMin сказать этого не может.
  • Вы прочитали лог при первой загрузке. Иначе CD.registerBreed отказывает молча, а каждый отказ печатает именованную строку.
  • CD.defineDogParts вызван. Разделайте одно из своих животных и убедитесь, что игра не падает.
  • Ваши key, typePrefix и суффиксы появления уникальны, и менять их не придётся.
  • Мелкая порода задаёт puppySize, и вы видели щенка, который это подтверждает.
  • Если это другой вид, CD.registerVoices вызван и voices заполнены, включая whine, иначе животное лает и скулит, как собака.
  • Иконка мудла есть во всех шести размерах.
  • В каждой папке языка, которую вы поставляете, есть все ключи. Недостающий ключ выводит на экран сам ключ.
  • Вы проверили в мультиплеере, а не только в одиночной игре. Позиция, владение и mod data ведут себя там иначе.
  • Ваша страница Мастерской предупреждает, что снимать аддон с сохранения, где есть живые животные, не стоит.

15. Частые ошибки

Симптом Что это
"Моё животное никогда не появляется." Почти всегда это проверка: аддону нужен API, которого у установленной базы нет, поэтому он выходит сразу и ничего не регистрирует.
"Оно появляется, но застыло." Набор анимаций не "raccoon", либо в модели не хватает клипов.
"Оно рисуется чёрным пятном, а лог заливает сообщениями." В модели больше 60 костей.
"Разделка роняет игру." CD.defineDogParts не был вызван.
"Щенок рождается размером со взрослого." Мелкая порода не задала puppySize. Значение абсолютное и применяется заново на каждом проходе, поэтому диапазон размеров у типа его не покрывает.
"Две мои породы всё время превращаются друг в друга." Они делят один engineBreed. Префикс выбирает меш и может быть общим для целой семьи. engineBreed выбирает текстуру и отличает породы этой семьи друг от друга, поэтому он обязан быть уникальным для каждой породы.
"Звук играет, но игнорирует ползунок громкости." Для этого звука не был вызван CD.registerVoices.
"В одиночной игре работает, а на сервере ведёт себя не так." Что-то пишет mod data на клиенте. Перенесите это на сервер.
"Оно дралось с зомби, хотя бой я выключил." База, на которой оно работает, старше API 5. Такая база не знает поля и игнорирует его. На такой базе проверка обязана заблокировать загрузку, а не грузиться с отсутствующим полем.

Что-то в этом руководстве неверно или чего-то не хватает? Напишите в Discord.