Создание модов для Fable 2 с Archon's Toolbox
Автор: JustSomeGuy
Это руководство предполагает, что у вас есть некоторый опыт работы с Lua, сопрограммами (coroutines), функциональными средами, метатаблицами и т.д., хотя понимание последних двух не требуется.
|
Примечание редакции: данный материал — перевод файла Authoring.txt, который можно скачать вместе с утилитой Achon's Toolbox (версия 0.3 от 14 июля 2024 года). Перевел Torionel. |
Краткие выводы по менеджеру модов / введение
Скриптовый менеджер модов (Script Mod Manager) предоставляет очень простой способ запустить в игре ваш собственный Lua-код, причем в формате, который идеально подходит для распространения [среди других пользователей — прим. ред.].
Моды состоят из единственной папки, содержащей как минимум два небольших файла (подробнее смотрите ниже в разделе "Структура папки с модом").
Чтобы перевести мод в формат для публикации, просто заархивируйте папку с модом.
Сейчас не буду вдаваться в подробности, но скриптовый менеджер модов состоит из двух компонентов: собственно менеджер и Runner.
Менеджер (manager) — приложение с GUI, которое пропатчивает игру и управляет файлами модов.
Runner — внутренний игровой компонент, который настраивает Lua-код модов, управляет им и запускает его.
Наверное, где-то ниже я напишу "менеджер", имея в виду Runner, но не думаю, что реально есть смысл их разделять.
Краткое введение в моды
У ваших модов будет доступ к состоянию всех игровых Lua, так что вы сможете делать всё, что захотите.
Код вашего мода будет запущен в новой функциональной среде, поэтому переменные будут определены в собственной таблице ваших модов, прямо как в модуле.
Среда вашего мода будет иметь свою метатаблицу ModMetatable, что даст ей доступ к таким вещам, как хуки (ModHooks) и патчинг игры.
Если ваш мод не статичный (см. свойство файла-манифеста: Static), вы можете определить функцию под названием Update(), которая автоматически будет превращаться в сопрограмму и выполняться итерациями.
Если вам нужно больше одной сопрограммы, просто сделате больше и выполняйте их из основной функции Update.
Вы можете определить следующие функции, которые будут автоматически вызываться при управлении вашим модом: Enable (включить), Disable (отключить), Uninstall (удалить). Первые две вызываются, когда ваш мод включен/отключен, а функция удаления вызывается после удаления.
Когда вызывается функция Uninstall, вы должны сделать всё возможное, чтобы восстановить игру в том виде, в каком она была до установки мода. Это означает удаление всех созданных вами NPC, вывод героя из режима взаимодействия, удаление правил катсцен, восстановление перезаписанных функций и значений, если это возможно, и т.д. После удаления ваша сопрограмма Update больше не будет выполняться.
С другой стороны, вы можете вернуть данные с помощью функции Uninstall. Если ваш мод удаляется, возвращаемое значение будет сохранено в таблице, содержащей данные об удалении (ключом будет идентификатор имени мода — NameID), если только игрок не выбрал в менеджере ClearData.
Если ваш мод обновляется, возвращаемое значение (return value) передается новому экземпляру (new instance). Представьте, что у нас есть мод, который хранит новый тип — XP (опыт), и этот мод обновляется. В функции Uninstall() мы можем вернуть количество опыта, который заработал игрок, или таблицу, содержающую много разных значений, включая опыт.
Если ваш скрипт более сложный и нужно сохранить позицию сопрограммы, я рекомендую взглянуть, как игра выполняет этапы квестов.
Основной цикл (main loop) инкапсулирует всю функцию целиком, но выполняется только код в рамках текущего этапа. Это позволит вам перезапустить сопрограмму из той же точки на основе переменной public Stage.
Структура папки с модом
Мод — это папка, в которой есть как минимум два файла. Первый — файл modmanifest.json, остальные файлы — Lua-скрипты.
Все эти файлы должны быть в папке, вложенной в папку /Mods/, и имя первой должно совпадать со значением NameID мода. Больше деталей можно узнать в секциях их свойств в файле-манифесте.
modmanifest.json. Файл-манифест мода содержит информацию, которую менеджер использует для установки мода в файл installedmods.lua (остальное ниже — в разделе с техническими деталями). Более важно то, что он содержит ID модов, путь к стартовому скрипту и версию.
Свойство манифеста NameID. Это главный внутренний идентификатор вашего мода. Очень важно, чтобы он соответствовал названию папки с вашим модом.
Если вы поменяете NameID после установки мода, менеджер и сама игра будут считать это отдельным модом.
Я ОЧЕНЬ рекомендую просто выбрать что-то уникальное и узнаваемое, так как изменение NameID — большая суета и риск для установок у ваших пользователей, если ваш мод уже выпущен.
Свойство манифеста StartScriptPath. Путь к Lua-скрипту, который вызывается менеджером для старта вашего скрипта.
Имеет собственную среду, поэтому можете определять переменные, как хотите. Имеет доступ к некоторым вещам, которые для дополнительного функционала обеспечивает менеджер, вроде хуков (ModHooks).
Свойство манифеста Static. Это свойство идеально подходит для модов, которые могут работать полностью без использования хуков и поэтому не нуждаются в постоянном обновлении сопрограммы.
Если ваш мод статичный, тогда ваш скрипт запускается сразу после установки, и на этом всё (в течение этого времени ваш мод, вероятно, должен подписаться на хук в ModHooks).
Если ваш мод НЕ статичный, менеджер будет пытаться создать сопрограмму из функции ModUpdate() и возобновлять ее работу каждый тик. Это идеально, если ваш мод должен делать что-то вроде перезаписи функции в основе поведения NPC или вызывать функцию во время загрузки новой локации.
Взгляните на мод DogTeleporter, который включен в архив с менеджером в качестве примера статичного мода, использующего ModHooks для реагирования на события в реальном времени.
Свойство манифеста Files. У игры есть файл dir.manifest с путем ко всем файлам, которые должны быть доступны из внутриигрового состояния Lua.
Менеджер автоматически читает список Files в файле-манифесте вашего мода и добавляет их в dir.manifest, когда ваш мод установлен. Если файла нет в dir.manifest, он не может быть загружен.
Свойство манифеста Version Major/Minor. Используется для отслеживания версий вашего мода. Сюрприз.
Когда вы поменяете свойство в файле-манифесте своего мода, Runner обнаружит это и завершит работу старой версии, когда вы загрузите сохранение.
Инструкции о том, как делать обновления, читайте в соответствующем разделе.
В данный момент переменные обеих версий имеют одинаковый функционал. Я планирую (планировал) использовать Major-версию для указания на совместимость с модами, которым требуется ваш мод.
То есть если моду нужна версия Major 2 вашего мода и версия Major 2 была установлена, будет доступна она (независимо от Minor-версии). В противном случае она не была бы предоставлена запрашивающему моду.
Я мог бы реализовать это позже, когда буду внедрять новые модули, но не знаю.
Другие свойства манифеста. В будущем могут быть добавлены прочие свойства. Поэтому думаю, что обновлю данный материал, когда это произойдет.
Mod Configuration Menu (MCM)
MCM позволяет добавить для вашего мода внутриигровое конфигурационное меню, в которое можно зайти нажатием сочетания LB + X.
MCM использует мой модуль MultipageMenu, который динамически создает (общепризнанно) неуклюжую, но полезную систему меню.
Лимита на количество опций, которые у вас могут быть, нет. Кнопки в вашем меню не обязательно должны просто настраивать что-то в вашем моде: они могут вызывать любую функцию, которую вы захотите.
Доступ ко всем функциям MCM можно получить с помощью индексации таблицы под названием MCM.
Если вы не понимаете нижеследующего, я бы порекомендовал просто посмотреть на мод, который использует MCM. Вот краткое руководство.
Есть функция под названием MCM.OpenMenu, которая должна быть передать заголовку вашего меню функцию, возвращающую таблицу с кнопками (если более конкретно, таблицу с записями возможных действий — Action Entries).
Action entry — это таблица, описывающая кнопку. Она парсится системой меню для создания кнопки и вызова функции при нажатии этой кнопки. Чтобы создать action entry, вызовите следующий код:
MCM.NewActionEntry("button text", enabled_bool, function_to_call_when_pressed, args_to_pass_to_function):
- значение параметра button text отображается как опция в меню
- параметр enabled_bool окрашивает опцию в серый, если установлено значение false
- параметр function_to_call указывает на функцию, которая вызывается, когда пользователь нажимает кнопку меню
- параметр args_to_pass_to_function передает аргументы функции function_to_call. Если это таблица, то параметры распаковываются и передаются по отдельности. Думаю, если вам нужно передать таблицу, вложите (nest) свою таблицу в другую таблицу
Пример:
{
MCM.NewActionEntry("Say Hello", true, GUI.DisplayMessageBox, "Hello World!"),
MCM.NewActionEntry("Heal Player", true, Health.Modify, {QuestManager.HeroEntity, 10000}),
}
Несмотря на то, что ваши записи находятся в таблице, определение этой таблицы на самом деле должно быть заключено в функцию вроде этой:
function GetMyMenuEntries()
return {
MCM.NewActionEntry("Say Hello", CanWeSayHelloRightNow(), GUI.DisplayMessageBox, "Hello World!"),
...
}
end
Это связано с тем, что вы можете использовать функции для динамического определения элементов в ваших записях (entries): например, вызывать функцию для определения enabled_bool.
Если бы таблица с entry была просто определена при первоначальном запуске вашего скрипта, функция, определяющая доступность кнопки, была бы вызвана только тогда.
Если бы таблица с entry возвращалась функцией, таблица переопределялась бы каждый раз при открытии меню, что позволяло бы повторно оценивать свойства ваших entry при каждом открытии меню.
Теперь вам нужно создать единственную запись, которая вызывает функцию MCM.OpenMenu и передает GetMyMenuEntries() следующим образом:
MyMcmOption = NewActionEntry("MyMod Config", true, MCM.OpenMenu, GetMyMenuEntries)
И, наконец, вызываем функцию MCM.AddOptionSet(), которая принимает два аргумента: ваш единственный ActionEntry, который возвращает ваши пункты вашего меню;ключ, который соотносит ваши action entries с вашим модом, позволяя вам удалить их позже.
То есть: MCM.AddOptionSet(MyMcmOption, "MyModOptions")
Вы также должны удалить свои опции MCM, если ваш мод отключен — Disable() — с помощью MCM.RemoveOptionSet. Передайте тот же ключ, который вы передавали выше в AddOptionSet.
То есть: MCM.RemoveOptionSet("MyModOptions")
Если хотите открыть свое меню с помощью собственного скрипта, можете просто сделать так:
MCM.OpenMenu("My Menu Title", GetMyMenuEntries)
Имейте в виду, что такое меню может быть вызвано (прямо или косвенно) только из сопрограммы по мере выполнения функции.
ModMetatable. Раздел ModHooks
ModHooks.AddHook(hook_name, unique_id, callback_func, arg):
- hook_name — название хука (то есть, OnSaveLoad, OnEnterArea, OnHeroHit)
- unique_id — идентификатор, используемый для добавления и удаления вашего хука
- callback_func — функция, которая вызывается, когда хук триггерится
- arg — аргумент, который передается в callback_func
ModHooks.RemoveHook(hook_name, unique_id):
- hook_name — название хука (то есть, OnSaveLoad, OnEnterArea, OnHeroHit)
- unique_id — идентификатор, используемый для добавления и удаления вашего хука
ModMetatable. Раздел ModMisc
ModMisc.DisplayAllKeys(table_to_print, bool_should_print_values):
- table_to_print — таблица, которая будет разбита на строки для вывода на экран
- bool_should_print_values — булевое значение, которое определяет, выводить ли значение или ключ
ModMetatable. Патчинг
(Примечание автора: задача — убедиться, что это всё еще работает)
Patching.AddPatch(scriptpath, patchpath, modkey, overwrite):
- scriptpath — путь к основному скрипту относительно папки /data/, не включающий bnk-файл, т.е. scripts/quests/qc010_childhood.lua
- patchpath — путь к файлу патча относительно папки /data/, т.е. scripts/Mods/childhoodmod/childhoodquest_patch.lua
- modkey — строка, которая используется для определения, какой мод добавил патч. Старайтесь, чтобы это значение оставалось неизменным для каждого патча, добавляемого вашим модом
- overwrite — если установлено значение true, перезапишите уже существующий кэшированный патч, если таковой имеется
Patching.RemovePatch(scriptpath, modkey, force):
- scriptpath — путь к скрипту или названию файла, который нужно избавить от патча. Старайтесь, чтобы это значение совпадало с тем, которое вы передали в AddPatch
- modkey — должен быть идентичен ключу, который передается в функцию AddPatch, если только значением параметра force не является true
- force (НЕ РЕКОМЕНДУЕТСЯ): если параметр modkey не соответствует, но вы должны удалить патч, выставляйте значение true. Влечет риск удаления патча другого мода. Должен использоваться ТОЛЬКО если пользователь хочет избавиться от патча в скрипте независимо от того, какой мод добавил этот патч
Технические детали
Когда менеджер установлен, скрипты WeaponInventory и GeneralScriptsManager (в файле gamescripts_r.bnk) будут пропатчены для вызова скрипта startrunner.lua:
- WeaponInventory модифицируется, чтобы запускать Runner при старте новой игры
- Функция LoadSaveTable() в скрипте GeneralScriptManager's модифицируется, чтобы запускать менеджер во время загрузки сохраненной игры (если она уже не запущена). Также это триггерит хук OnSaveLoad
Runner — это внутриигровой компонент менеджера. Это стандартный lua-скрипт, который управляет сопрограммами и таблицами модов. Он возобновляет работу включенных модов каждую итерацию, триггерит хуки и так далее.
Для коммуникации с игрой/Runner'ом менеджер создает файл под названием installedmods.lua в папке data/scripts/Mod Manager/.
Runner читает этот файл, чтобы узнать, какие моды установлены, включены, отключены и т.д. По сути это единственный способ, которым менеджер может "общаться" с Runner.
Менеджер делает запись в этом файле, когда происходят изменения, а Runner читает их при последующих проверках статуса установки мода (то есть при запуске).
В менеджере вы можете увидеть installedmods, находящиеся в памяти, из окна отладки (Debug Window) и посмотреть, как каждое действие изменяет их.
Больше интересного о Fable — в