Как внедрить свой скрипт в функцию Project Zomboid

Как внедрить свой скрипт в функцию Project Zomboid Project Zomboid

Что бы внедрить свой скрипт Project Zomboid предполагается, что вы уже хотя бы немного изучили Lua и знаете его основы. Возможно даже уже сделали несколько модов и опубликовали их. Теперь хотите научиться кодить правильно, чтобы ваш мод был максимально совместим с другими.

Высокая совместимость уменьшит количества багов, но не избавит вас от них полностью. Увы, «бажность» зависит не только от знаний языка Lua и некоторых приёмов, но и от аккуратности, внимательности, и от опыта программирования в целом.

Отличие Lua от API Project Zomboid

Lua — это язык программирования, причём довольно простой язык. Его можно выучить за 15-30 минут. Если вы не программист, то за 2-3 часа. И если совсем не любите математику, то, думаю, за 2-3 дня. Но всё равно это довольно мало.

Язык Lua используется не только в Project Zomboid. Он довольно популярен в моддинге в других играх. И его синтаксис можно изучить ОТДЕЛЬНО от игры. Вот вам пример онлайн-песочницы для Lua:
https://rextester.com/l/lua_online_compiler
Можете для теста набрать там что-то типа такого:

print(1+2)

И убедиться, что оно работает.

Там же можно проводить свои тесты синтаксиса Lua по мере его изучения.

Там же можно проверить корректность синтаксиса lua-файла, не запуская игру. Просто скопируйте текст файла туда целиком. Ошибка синтаксиса будет заключаться в том, что вы пропустили скобку или запятую. Ошибка времени исполнения — это вызов nil, например, но это нормально, сайт ничего не знает о Project Zomboid.

API Project Zomboid — это набор всех его функций, и по сути сюда также входит весь его Lua-код. То есть все lua-файлы, они являются также и документацией к себе же. Их можно читать и изучать.

Однако объём изучения ГОРАЗДО больше, чем синтаксис языка Lua.

Можно провести аналогию с книгой. Lua — это как бы алфавит, базовые правила чтения и произношения. API — это текст книги, сюжет, связность, и все-все-все слова, всё их многообразие. Очевидно, что изучить правила чтения быстрее, чем весь текст в книге. Да и не нужно учить текст в книге — всегда можно пролистать до нужной страницы и перечитать ещё раз. А вот без знания алфавита будет тяжко.

Приём: Замена файла

Прежде, чем говорить о хороших приёмах, поговорим о плохих — о замене файлов.
Этот приём имеет важный существенный плюс — он ПРОСТОЙ и незатейливый, и не требует глубокого понимания Lua.

Вы просто копируете файл игры в папку мода по тому же относительному пути, так сказать.
То есть файл \media\lua\client\XpSystem\ISUI\ISHealthPanel.lua
располагается по тому же пути и называется также:
\media\lua\client\XpSystem\ISUI\ISHealthPanel.lua
Но уже внутри папки вашего мода.

При этом во время загрузки игры, её файл временно заменяется вашим. Это очень удобно, поскольку суть модификации в том, чтобы что-то поменять в этом файле, как вам надо.

А теперь минусы:
1) Ваш мод будет несовместим с другими модами, которые делают также. Замена файла сработает только от одного из таких модов.
2) Ваш мод будет хуже совместим с будущими версиями игры. Ведь развитие игры тоже не стоит на месте, и именно этот файл может быть улучшен разработчиками. Однако ваш мод вместе со своими правками будет подсовывать игре старую версию файла. Чтобы избежать этого, вам нужно будет следить за обновлениями игры и конкретно за обновлением этого файла, и в случае новых фич от разрабов снова копировать файл и вносить те же изменения. Ну а если «забить» на свой мод, то он будет быстро устаревать по мере улучшения игры разрабами.

Если вы совсем зелёный новичок, то делайте мод через замену. Если же вы готовы двигаться дальше и улучшить качество и совместимость своих модов, то см. ниже.

Глобальное пространство

Вообще, это основы Lua.
Но кто плохо представляет, что такое глобальное пространство, этот раздел для вас.

Объявлять переменные и функции можно двумя способами: глобально и локально.
Чтобы объявить локально, нужно использовать ключевое слово local
Если этого слова нет, то объявление глобальное.

Пример глобального объявления функции

function MyFunction(a,b)

return a+b

end

Функция с именем MyFunction объявлена глобально и будет доступна из любых других lua-файлов игры и других модов. Другие моды смогут также заменить эту функцию (специально или не умышленно), и ваш файл в итоге будет использовать их функцию.

Есть небольшой нюанс по поводу порядка загрузки файлов. Если эта функция используется в другом файле сразу на этапе загрузки lua-файла, то он должен загружаться после её объявления (т.е. после файла, в котором она объявлена). Если же вызов функции происходит из какого-то события или какой-то другой функции, то, как правило, порядок загрузки не важен.

Глобальная функция полезна, когда вы хотите, чтобы она была доступна для вызова всем — и игровым файлам, и модам.

Пример локального объявления функции

local function MyFunction(a,b)

return a+b

end

Функция с именем MyFunction объявлена локально и конкретно эта функция не будет доступна из других lua-файлов игры через глобальное пространство напрямую.

Возможно, есть какая-то другая функция с тем же именем, но глобальная — вот она будет доступна в других файлах, а в этом файле локальная функция будет перекрывать глобальную (если та вообще существует). Это полезно, когда функция делается чисто для себя, чтобы другие файлы и моды, не смогли вашу функцию испортить. Особенно это актуально для простых названий типа «func» или «old_func», которые будучи глобальными будут конфликтовать друг с другом у разных модов. Локальную функцию точно никто случайно не переопределит извне файла.

У каждого мода может быть своя личная (т.е. локальная) функция MyFunction — и никаких конфликтов имён не будет.

Приём: Замена функции

Это лучше, чем замена файла целиком, но ещё не самое лучшее решение. Нам придётся понять его, чтобы перейти к более крутым приёмам.

Приём замены функции заключается в том, что вы создаёте lua-файл у себя в папке мода и называете так, чтобы имя не совпадало с игровым файлом. Это приведёт к тому, что он не будет подменять файлы игры, а будет работать самостоятельно после загрузки многих файлов игры. И в этом файле можно определить функцию в глобальном пространстве, которая в точности совпадает по названию с ванильной. Это приведёт к переопределению этой функции.

Это тоже достаточно просто и удобно. Мы просто берём глобальную ванильную функцию и копипастим её к себе в файл. Затем делаем небольшие правки в ней. Сохраняем. И вуаля — мод готов.

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

Локальные зависимости

Иногда (не часто!) даже функция класса может быть написана не совсем в объектно-ориентированном стиле, например она может использовать локальные переменные (или локальные функции). А как мы помним, всё локальное работает только в пределах файла. Поэтому эти переменные тоже придётся создать у себя и проинициализировать.
Пример: в файле ISHealthPanel.lua в самом начале строка:

local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small)

Это и есть локальная переменная. Далее, если мы попытаемся тупо скопировать одну лишь функцию, например, ISHealthBodyPartListBox:doDrawItem, то она не будет работать должным образом в нашем моде, потому что использует эту переменную. Чтобы исправить это, локальную переменную тоже нужно пересоздать. Либо можно подправить функцию так, чтобы она больше не использовала внешнюю локальную переменную.

На заметку: аккуратная инъекция (см. ниже) лишена этого неудобства, хотя и сложнее в понимании и использовании.

Двоеточие в имени функции

Это тоже основы Lua, но всё же, повторим.

Объявление функции

Эта строчка кода:

function Foo:render()

Идентична этой строке кода:

function Foo.render(self)

Вызов функции

Эта строчка кода:

my_foo:render() — вызов функции

Идентична этой строке кода:

my_foo.render(my_foo) — вызов функции

Вот и всё, никакой магии. 🙂

Приём: Аккуратная инъекция

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

Предположим, в игре есть такая глобальная функция:

function GameFunc(a, b)

local c = a + b

return c

end

Не трудно догадаться, что функция просто складывает числа, но ЛОГИКА этой функции может быть связана с какой-то игровой механикой. Например, последующий урон тупо складывается с предыдущим или считается по хитрой формуле? Предположим, что это что-то игровое, и эту ЛОГИКУ разрабы могут поменять в будущем. И мы не хотим, чтобы наш мод полностью перезаписал эту логику. Нужно, чтобы апдейты игры не конфликтовали с модом.

Пусть мы хотим к старой логике добавить свою логику, которая заключается в том, что урон на +1 больше (ну или не урон, не суть, что там, это же просто пример).

Инъекцию можно сделать так:
1) Сначала сохраняем ссылку на старую функцию в локальной переменной.
2) Потом заменяем функцию своей функцией.
3) Однако в своей функции вызываем старую функцию, и используем её результат.

local old_func = GameFunc

function GameFunc(a, b, …)

local result = old_func(a, b, …)

result = result + 1

print(«My mod is working!»)

return result

end

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

Чтобы убедиться, что всё работает, мы добавили print, который выводит в лог сообщение, и тем самым доказывает, что это место программного кода было выполнено.

Инъекция контролирует вход и выход. То есть результат выполнения и параметры — это то, что мы можем изменять в своём моде через инъекцию. Можно, например, аналогичным образом поменять один из параметров (в данном случае эффект инъекции будет такой же):

local old_func = GameFunc

function GameFunc(a, b, …)

a = a + 1

return old_func(a, b, …)

end

Многоточие «…» в Lua

Это не условное обозначение, а реальный синтаксис языка!
Он означает все остальные параметры функции после перечисленных.

Пример:

function test(a, b, …)

print(…)

end

test(1, 2, 3, 4, 5)

Вывод будет: 3 4 5

Приём: Двойная инъекция

Начиная с этого момента, мы вступаем на опасную территорию. Нужно немного практики, чтобы привыкнуть ко всем этим инъекциям, так что если сомневаетесь, то не спешите сильно погружаться в это дальше.

Здесь я просто хочу показать, что такое возможно и вполне удобно.

Предположим, оригинальный ванильный код выглядит как-то так:

function Foo:render()

— очень много кода до

self:DrawText(GetText(«IGUI_ShowMessage», 2, 3), 7, 8)

— и очень много кода после

end

И мы хотим поменять цифру 3 на 333.

То есть там очень много кода, и нам нужно поменять всего лишь одно место в нём. Но результатом функции нам не обойтись. И в данном случае результата вообще нет, это скорее процедура, а не функция. Как же быть?

Очень просто, можно внедриться в глобальную функцию GetText:

local old_GetText = GetText

function GetText(txt, a, b, …)

if txt == «IGUI_ShowMessage» then

b = 333

end

return old_GetText(txt, a, b, …)

end

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

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

Поэтому можно сделать ВРЕМЕННУЮ инъекцию внутри другой инъекции:

— временная инъекция

local old_GetText

local function new_GetText(txt, a, b, …)

if txt == «IGUI_ShowMessage» then

b = 333

end

return old_GetText(txt, a, b, …)

end

— постоянная инъекция

local old_redner = Foo.render

function Foo:render()

old_GetText = GetText

GetText = new_GetText

old_redner(self)

GetText = old_GetText

end

Смотрите, мы подменяем функцию GetText, потом вызываем старую render, а затем быстро меняем GetText обратно, словно ничего и не было.

Такую инъекцию я ещё называю «ниндзя-инъекция». И это не предел. Может быть две временные в одной, либо вложенность из трёх инъекций, зависит от того, насколько вы готовы заморочиться и насколько вам нужна оптимизация. Но и напортачить больше шансов. Поэтому повторюсь, не усложняйте сильно, если не уверены, что потянете.

Приёмы работы с потенциальными ошибками

Поговорим о подводных камнях вышеописанных практик.

Давайте возьмём пример из предыдущего раздела и представим, что будет, если в old_render произошла ошибка. Пусть это случилось всего лишь один раз. Что же будет?

— постоянная инъекция

local old_redner = Foo.render

function Foo:render()

old_GetText = GetText

GetText = new_GetText

old_redner(self)

GetText = old_GetText

end

А будет вот, что. Последняя строчка не сработает:

GetText = old_GetText

Название GetText будет указывать именно на вашу new_GetText.
И что же произойдёт при следующем вызове?
old_GetText = GetText — то есть new_GetText
Вот оно! После этого ссылка на старую функцию будет безвозвратно потеряна.
И когда будет вызвана new_GetText, она в конце своей работы попытается вызывать old_GetText, то есть на самом деле new_GetText, то есть саму себя.
Это приведёт к бесконечной рекурсии и переполнению стека. Скорее всего, игра зависнет или вылетит. И это печально.

Простая подстраховка

Здесь я приведу простой и быстрый способ решить конкретно эту проблему переполнения стека.

— постоянная инъекция

local old_redner = Foo.render

function Foo:render()

if GetText ~= new_GetText then

old_GetText = GetText

end

GetText = new_GetText

old_redner(self)

GetText = old_GetText

end

Здесь мы убираем перезапись, если что-то пошло не так. Но факт подмены остаётся.
И вот вам домашнее задание: подумайте, что будет, если два мода таким одинаковым способом внедряются в логику функции. Да, функции выстраиваются в цепочку. Но что будет, если ошибка в первом моде? Что будет, если во втором?

Подстраховка вида «я умываю руки»

Суть этой подстраховки в том, чтобы отменить свою инъекцию, если что-то пошло не так. Фича мода как бы отваливается и перестаёт работать, хотя другие части вашего мода могут по-прежнему функционировать нормально.

Здесь мы исходим из предположения, что ошибка может быть как в вашем моде, так и в чужом, так и заключаться в несовместимости чего-то. И нам это не важно. Мы просто выпиливаемся, и больше не трогаем эту функцию. Если там ошибка осталась, то это уже не наши проблемы и не наша вина. «Red popup» всплывёт всего лишь один раз и не будет спамить (если баг на нашей стороне).

local old_redner = Foo.render

local in_progress = false

function Foo:render()

if in_progress then

if old_GetText then

GetText = old_GetText

end

old_GetText = nil

return old_redner(self)

end

in_progress = true

old_GetText = GetText

GetText = new_GetText

old_redner(self)

GetText = old_GetText

old_GetText = nil

in_progress = false

end

pcall()

pcall — встроенная в Lua функция. И довольно мощная. Она позволяет отделить мух от котлет. То есть работу вашего мода от других модов, чтобы ошибки других модов не влияли на ваш. Предполагается, что ваш код идеальный или почти идеальный, и лишь другие моды (или даже сама игра) могут его испортить.

Первый параметр — какая-то другая функция, а затем её другие параметры.
Результата два: 1) результат выполнения 2) результат самой функции, которую мы вызывали.

local old_redner = Foo.render

function Foo:render()

old_GetText = GetText

GetText = new_GetText

pcall(old_redner, self)

GetText = old_GetText

end

Если внутри old_redner произошла ошибка, то она конечно будет в логе, но управление передаётся обратно в pcall, поэтому наша инъекция выполнится до конца, в том числе:
GetText = old_GetText

Учтите, что pcall накладывает некоторый оверхед на свой вызов, поэтому желательно использовать пореже. Инъекция в :render() — не лучший пример. Лучше во всякие :perform() для действий персонажа.

pcall(), когда нужен результат вызова

local old_GameFunc = GameFunc;

function GameFunc(a, b, …)

local success, result = pcall(old_GameFunc, a, b, …)

if success then

result = result + 1

else

— здесь мы даже как бы исправляем краш,

— но это не обязательно

result = a + b + 1

end

return result

end

Совместимость модов

Выше уже было сказано, что замена одного и того же файла разными модами приводит к их конфликту. При этом ошибки или какого-то сообщения не будет. Просто сработает замена лишь от одного из модов. Это всё как бы очевидно, и в целом вы сами можете догадаться до этого, но дальше начинаются интересные вещи.

Замена функции

Замена функции совместима с заменой файла. Но только если замена файла не меняет ту же самую функцию. Как это работает: сначала на стадии загрузки игры, файл временно заменяется модовским. Поэтому все изменения в этом файле вступают в силу уже на стадии загрузки. После этого загружается файл второго мода (без замены), который меняет лишь одну функцию (из того же файла). Получается, два изменения стакуются без особых проблем.

Замены разных функций в одном файле — совместимы между собой. Думаю, здесь тоже очевидно.

Если же моды заменяют одну и ту же функцию, то актуальной будет только последняя замена.

Аккуратная инъекция

Инъекция совместима с заменой файла на 100%. Даже если инъекция происходит в ту же функцию, которую уже исправили через замену файла, то всё равно совместима. Как мы помним, инъекция не меняет старую логику, а лишь добавляет свою.

Инъекция совместима с другой аккуратной инъекцией на 100%, даже в ту же самую функцию. Крайне редкое исключение — когда моды делают практически одно и то же. Меняют одну и ту же цифру для своих целей. Можно сказать, что несовместимость уже в самой логике. Но это надо постараться поставить такие моды, которые делают очень похоже вещи.

Ещё бывает, что логика одна и та же, но всё равно стакуется. Например, первый мод увеличивает скорость чтения в 2 раза, а второй — в 3 раза. Два мода вместе увеличивают в 6 раз. Только совместимость инъекций способна на такое. Через замену функций такого результата нельзя добиться.

Таким образом, несколько инъекций в одно и то же место как бы выстраиваются в цепочку, и их эффекты складываются. Как раз поэтому я называю инъекцию аккуратной. Она не должна ничего ломать. Хотя… может быть и неаккуратная инъекция — тогда жди багов и жалоб.

Аккуратная инъекция vs замена функции

Инъекция совместима с заменой функции. Но только если замена происходит до инъекции.

Да! Именно так. Замена функции, которая происходит после инъекции, отменяет инъекцию! Поэтому моды через замену функции я обычно называю «грубыми» (по отношению к другим модам). Таки грубые моды, добавляя свою механику, отменяют механику другого мода. Механики не стакуются. Очевидная несовместимость.

Но автор аккуратной инъекции может приложить все усилия, чтобы его инъекция произошла как можно позже — на самой поздней стадии загрузки игры.

Аналогично автор замены функции может сделать так, что его замена происходит как можно раньше. Для этого в файле (или папке) нужно использовать младшие символы ascii, ведь порядок загрузки в PZ завязан на сортировку файлов по имени.
Примеры:

/media/lua/client/!aaa.lua

/media/lua/client/!aaafolder/my_fix.lua

Памятка

Помните, что в сохранении мод нужно включить отдельно. Просто включить в главном меню не достаточно. Это самая частая причина жалоб «мод не работает».

Помните, что при разработке своего мода, который уже опубликован, от него нужно отписаться. Так будет наверняка использована именно локальная версия. На всякий случай стоит убедиться, что файлы из воркшопа действительно удалены с диска:
C:\Program Files (x86)\Steam\steamapps\workshop\content\108600
И дальше id вашего мода.

Источник
Оцените статью