Обёртки функций Lua и Java hooks в Build 42
Обёртки функций Lua и Java hooks в Build 42
Цель руководства — менять поведение игры, не копируя её файлы, чтобы мод переживал обновления и сосуществовал с другими модами. Выбор способа зависит от того, где находится логика: в данных, Lua или Java. Страница описывает Build 42 без точной минорной версии.
Где находится поведение, которое нужно изменить?
| Где находится | Пример | Как менять по руководству | Указанный риск |
|---|---|---|---|
| Данные (скрипты .txt и распределения) | Вес предмета, ингредиенты рецепта или loot | Переопределить в собственном скрипте или изменить при загрузке Lua (модуль 13). | Низкий |
| Lua игры | Меню, действия, интерфейс и значительная часть логики | Оборачивать функцию (раздел 2). | Низкий при обёртке; высокий при копировании |
| Java с hook | Атака, автоматическое питьё, характеристики | Hook.X.Add или событие (раздел 4). | Средний: заменяется поведение Java |
| Java без hook | Формула loot или базовый урон | Косвенно изменить читаемые данные или отреагировать позже через событие. | Иногда невозможно |
Правило автора: вносить минимальные изменения на как можно более высоком уровне. Если достаточно данных, не трогать Lua; если достаточно обёртки — не копировать функцию.
Оборачивать функцию Lua
В Lua функции — это значения в таблицах. ISEatFoodAction.complete — поле complete таблицы ISEatFoodAction. Можно сохранить исходное значение и заменить поле другой функцией, которая вызывает исходную.
Статья приводит пример из неопубликованного мода автора DesarmeXP, файл DesarmeXP/.../shared/DesarmeXP_Hooks.lua. В примере есть многоточия, поэтому это иллюстрация, а не готовый код:
require "Entity/TimedActions/ISHandcraftAction" -- 1. asegura que el original ya existe
local origPerformRecipe = ISHandcraftAction.performRecipe -- 2. guarda el original
function ISHandcraftAction:performRecipe() -- 3. pon la tuya en su lugar
local eligible = ... -- algo ANTES
origPerformRecipe(self) -- 4. llama al original
if eligible then ... end -- algo DESPUÉS
end
Заголовочный комментарий файла, по словам автора, выражает подход: оборачивать исходную функцию, а не копировать её. Копия целиком может застрять на старой версии после обновления игры.
В руководстве показаны четыре приёма: выполнить подготовку до оригинала; вызвать оригинал, обработать и вернуть результат; изменить поведение только для выбранного случая; заблокировать действие (осторожно). Следующие иллюстративные фрагменты сохранены буквально:
local orig = Tabla.metodo
-- Antes: preparar o registrar
function Tabla:metodo(...)
CursoLab.log("va a pasar")
return orig(self, ...)
end
-- Después: reaccionar al resultado
function Tabla:metodo(...)
local r = orig(self, ...)
CursoLab.log("pasó y devolvió", r)
return r
end
-- Condicional: solo cambia en tu caso
function Tabla:metodo(a, b)
if not esMiCaso(a) then return orig(self, a, b) end
return miVersion(self, a, b)
end
-- Bloquear: impedir la acción (con cuidado)
function Tabla:metodo(...)
if prohibido(...) then return false end
return orig(self, ...)
end
- Сначала загружайте исходный файл через require и только потом сохраняйте ссылку: иначе orig может быть nil.
- Возвращайте результат оригинала. В статье приведены isValid и complete, которые возвращают true/false; без return обёртка вернёт nil.
- Передавайте все аргументы. Если имена не нужны, автор советует использовать ..., чтобы передавать также параметры, добавленные будущим патчем.
- Защититесь от повторной обёртки после перезагрузки файла клавишей F11. В примере для этого используется флаг:
if not CursoLab._envuelto_complete then
CursoLab._envuelto_complete = true
local orig = ISEatFoodAction.complete
function ISEatFoodAction:complete() ... end
end
Обёртка метода класса, например ISEatFoodAction.complete, влияет на все действия поедания. Чтобы изменить только один экземпляр, автор рекомендует оборачивать поле самого экземпляра; он упоминает неопубликованный мод xpranksCustom и inst.checkPlayer в XPR_RecordedMediaGuard.lua.
Функцию local нельзя обернуть напрямую: она не находится в таблице и недоступна из другого файла. Пример из ISInventoryPaneContextMenu.lua:
-- client/ISUI/ISInventoryPaneContextMenu.lua:12
local function predicateNotBroken(item) ... end
Если нужная логика находится в local-функции, варианты из источника: обернуть публичную функцию, которая её вызывает, и поправить результат, либо заменить весь файл как крайний случай (модуль 01).
Когда два мода оборачивают одну функцию, второй загруженный сохраняет обёртку первого как оригинал. Цепочка работает, пока каждый вызывает оригинал. Обёртка, которая заменяет функцию и не вызывает её, может прервать цепочку и отключить более ранние моды. Совместимость зависит от порядка загрузки и реализации других модов.
Где оборачивать действие: perform или complete
В примере ISEatFoodAction автор различает два завершения: perform отвечает за видимую очистку, например звуки и обновление инвентаря; complete применяет сам эффект — персонаж ест. Фрагмент, который источник относит к Build 42:
function ISEatFoodAction:perform() -- limpieza visible: sonidos, refrescar el inventario
...
ISBaseTimedAction.perform(self);
end
function ISEatFoodAction:complete() -- el efecto real: el personaje come
self.character:Eat(self.item, self.percentage, self.useUtensil);
return true;
end
Автор утверждает, что Lua напрямую не вызывает complete: его вызывает Java через zombie.characters.CharacterTimedActions.LuaTimedActionNew.complete. По описанию источника, complete следует оборачивать, чтобы реагировать на результат действия, а perform — для визуальных эффектов клиента. Модуль 11 рассматривает сторону выполнения в мультиплеере. Это утверждение о версии, отдельно не проверенное.
Java hooks
Помимо Events, источник перечисляет восемь hooks, которые Java запускает до продолжения: AutoDrink, UseItem, Attack, CalculateStats, ContextualAction, WeaponHitCharacter, WeaponSwing и WeaponSwingHitPoint. Они, по статье, регистрируются в zombie.Lua.LuaHookManager.AddEvents(). Использование похоже на событие: Hook.AutoDrink.Add(fn), Hook.AutoDrink.Remove(fn).
Автор интерпретирует байткод IsoGameCharacter.autoDrink() так: если TriggerHook возвращает false, Java продолжает автоматическое питьё; если true — выходит и больше ничего не делает.
88: ldc "AutoDrink"
91: aload_0
92: invokestatic LuaHookManager.TriggerHook(String, Object)Z
95: ifeq 99 // si TriggerHook devolvió false, sigue con el autodrink normal
98: return // si devolvió true, Java NO hace nada más
По утверждению статьи, TriggerHook возвращает true, когда зарегистрирована хотя бы одна функция, независимо от результата Lua-функции. Поэтому автор предупреждает: регистрация Hook в этой точке заменяет действие Java, а не просто сообщает о событии. Это интерпретация конкретной версии, независимо не проверенная.
Как описывает источник, игра временно регистрирует пустой hook, чтобы отключить автоматическое питьё при переливании воды, а затем удаляет его:
local hookAutoDrink
hookAutoDrink = function(character)
if not ISTimedActionQueue.hasActionType(character, "ISTransferWaterAction") then
Hook.AutoDrink.Remove(hookAutoDrink)
end
end
if getCore():getOptionAutoDrink() then
Hook.AutoDrink.Add(hookAutoDrink)
end
Статья также утверждает, что ISReloadWeaponAction.lua постоянно регистрирует Hook.Attack, из-за чего Java-атака IsoLivingCharacter.AttemptAttack не выполняется и её обрабатывает Lua. Перед регистрацией hook автор советует изучить вызывающий Java-метод и определить, что будет пропущено.
Если логика находится в Java, но hook нет
- Изменить данные, которые читает логика: например, не менять формулу loot, а изменить распределения и параметры sandbox.
- Отреагировать после: по статье, OnFillContainer вызывается после заполнения и позволяет добавить или удалить предметы.
- Оборачивать вызывающий Java код Lua-файл, если вызов Java начинается в Lua-функции.
- Принять ограничение: источник утверждает, что Workshop-мод не заменяет Java-классы; моды, требующие ручной установки для этого, выходят за рамки курса.
Упражнения
Упражнение 9.1 · Событие для sopaipilla
Оберните ISEatFoodAction:complete, чтобы отправлять OnCursoLabSopaipilla, если съеден предмет типа sopaipilla. Критерии источника: событие вызывается только для этого продукта; еда продолжает работать, а функция возвращает исходное значение; после перезагрузки файла через F11 сообщение не дублируется.
Пример решения из статьи; он зависит от CursoLab и не запускался. Тип предмета считывается до вызова оригинальной функции:
require "TimedActions/ISEatFoodAction"
if not CursoLab._wrapEat then
CursoLab._wrapEat = true
local orig = ISEatFoodAction.complete
function ISEatFoodAction:complete(...)
local tipo = self.item and self.item:getFullType()
local r = orig(self, ...)
if tipo == "CursoLab.Sopaipilla" then
triggerEvent("OnCursoLabSopaipilla", self.character, self.item)
end
return r
end
end
В источнике поясняется, что тип читается до оригинала, потому что после еды предмет может исчезнуть.
Упражнение 9.2 · Два мода, одна функция
Создайте два файла CursoLab, которые оборачивают одну функцию, например ISInventoryPaneContextMenu.createMenu (она определена через точку и не получает self). Пусть каждый пишет в консоль. Проверьте, что выполняются оба и в каком порядке; затем отключите вызов оригинала в первом и поменяйте порядок загрузки. Объясните, как порядок может привести к тому, что один мод нарушит работу другого.
Упражнение 9.3 · Функция local
Выберите local-функцию в ISInventoryPaneContextMenu.lua — например, formatFoodValue около строки 2052, как советует статья. Измените видимый эффект без замены файла и запишите, какую публичную функцию пришлось обернуть и почему.
Упражнение 9.4 · Hook отключает Java
Зарегистрируйте Hook.AutoDrink, который только пишет в консоль. Проверьте его при включённом авто-питье, жажде и бутылке в инвентаре. Объясните результат по байткоду IsoGameCharacter.autoDrink(). Затем ограничьте отключение авто-питья определённым типом бутылки. Подсказка статьи: после перехвата нельзя «дать Java продолжить»; игра регистрирует hook и снимает его в нужный момент.
Упражнение 9.5 · Выбрать инструмент
Для каждого изменения определите, находится ли логика в данных, Lua, Java с hook или Java без hook и какой инструмент использовать; реализовывать не нужно: уменьшить вес яблок вдвое; гарантированно добавлять sopaipilla при первом открытии холодильника; запретить огнестрельное оружие в помещениях; добавить действие в контекстное меню деревьев; удвоить урон зомби.
Задача со звёздочкой 9
Напишите CursoLab.wrap(tabla, nombre, fn), который оборачивает tabla[nombre] функцией fn(orig, ...), не создаёт повторную обёртку после перезагрузки, сохраняет оригинал для CursoLab.unwrap(tabla, nombre) и пишет предупреждение, если функция отсутствует. Перепишите с ним упражнение 9.1.