Добавлен , опубликован
Раздел:
Триггеры и объекты

Предисловие

Отвечая на вопросы в QA мне бросилось в глаза отсутствие отдельной статьи посвященной такой важной вещи, как инжект функций в Lua - пришла пора это исправить.

Теория

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

Как это работает?

Поскольку функции в Lua хранятся в переменных - с ними можно делать что угодно, в том числе заменять одни функции другими во время выполнения кода.
Это выглядит примерно так:
do
  local f = FunctionName -- записываем функцию в переменную
  function FunctionName() -- заменяем оригинальную функцию своей
    f() -- вызываем оригинальную функцию из переменной
    -- здесь могла быть ваша реклама или ваш код
  end
end
Блок do end не является частью инжекта и использован для задания области видимости.
Также стоит учитывать что работает это только если выполнится после объявления оригинальной функции и до её первого вызова.
Возможен последовательный инжект одной и той же функции несколько раз, если не забывать вызвать оригинал это будет исправно работать, но лучше не злоупотреблять множественными инжектами одной функции и делать все о одном инжекте т.к. каждый лишний вызов функции это дополнительные затраты процессорного времени и в часто вызываемых функциях это будет заметно.

Практическое применение

То, ради чего была написана вся эта статья - этот трюк может использоваться для нескольких ключевых действий

Инжект InitGlobals

При работе с Lua средствами одного только WE у новичков обычно возникает вопрос вида - "а где точка входа?", ну или "куда бежать?". На данный момент оптимальной точкой входа при работе с WE считается именно инжект InitGlobals т.к. другие более интересные функции объявлены после пользовательского кода и из WE до них инжектом не добраться, а пользовательский код не привязанный к функциям старта карты может начать выполняться слишком рано и создание объектов в нем чревато проблемами в будущем.
do
  local f = InitGlobals -- записываем InitGlobals в переменную
  function InitGlobals() -- заменяем оригинальную InitGlobals своей
   f() -- вызываем оригинальную InitGlobals из переменной
   -- здесь могла быть ваша реклама или ваш код
  end
end
Код внутри этой функции выполнится во время инициализации карты, сразу после объявления и присвоения глобальных переменных объявленных в WE. В этот момент уже безопасно создавать триггеры и другие объекты, не опасаясь что они будут убраны сборщиком мусора или вызовут разсихронизацию или другие баги т.к. созданы слишком рано.

Инжект main

Для тех кто работает с кодом вне WE может быть интересен более крутой инжект - функция main, которая выполняет значительную часть работы по старту карты, в том числе вызывает InitGlobals, с которой мы работали выше.
do
  local f = main-- записываем main в переменную
  function main() -- заменяем оригинальную main своей
   f() -- вызываем оригинальную main из переменной
   -- здесь могла быть ваша реклама или ваш код
  end
end
Или продвинутый вариант, позволяющий отсрочить выполнение триггеров инициализации, если нам нужно выполнить что-то до них. Аналогичный результат можно получить просто применив инжект к RunInitializationTriggers, данный пример не оптимален и приведен в качестве альтернативного способа.
do
  local f = main-- записываем main в переменную
  function main() -- заменяем оригинальную main своей
    local init = RunInitializationTriggers  -- записываем оригинал функции RunInitializationTriggers в переменную 
    RunInitializationTriggers = function() end -- заменяем вызов триггеров инициализации пустышкой
    f() -- вызываем оригинальную main из переменной
    -- здесь могла быть ваша реклама или ваш код, который должен выполниться до триггеров инициализации
    init() вызываем триггеры инициализации
    -- здесь могла быть ваша реклама или ваш код, который должен выполниться после триггеров инициализации
  end
end

Инжект RemoveUnit

Некоторые системы могут требовать хранить данные с привязкой к юниту, а также требовать очистки этих данных при смерти или удалении юнита. Отследить смерть юнита достаточно просто, а с удалением обычно приходится оборачивать удаление юнита в свою функцию, которая сперва почистит данные, а потом уже выполнит удаление.
А можно просто применить инжект к оригинальной RemoveUnit и продолжать пользоваться ей не опасаясь забыть вызвать свое нестандарное удаление юнита.
do
  local f = RemoveUnit-- записываем RemoveUnit в переменную
  function RemoveUnit(u) -- заменяем оригинальную RemoveUnit своей
    -- здесь могла быть ваша реклама или ваш код
    --важно выполнить свой код до того как юнит удален оригинальной функцией
    f(u) -- вызываем оригинальную RemoveUnit из переменной
  end
end
Работает и с GUI триггерами, поскольку они в итоге тоже вызывают RemoveUnit в действии удаления юнита.

Инжект DestroyTimer

Из комментариев. Постановка таймера на паузу перед удалением. Сам я с необходимостью этого действия не сталкивался, но часто вижу упоминания. Если мне не изменяет память, это актуально только для таймеров с нулевым периодом.
do
  local DestroyTimerOrigin = DestroyTimer -- записываем DestroyTimer в переменную
  local PauseTimerCached = PauseTimer -- локальная переменная используется для более быстрого вызова функции в дальнейшем
  function DestroyTimer(t)
	PauseTimerCached(t)  -- вызываем PauseTimer из переменной
	DestroyTimerOrigin(t) -- вызываем DestroyTimer из переменной
  end
end

Особое колдунство

Применив инжект к функции, можно затем его убрать, тем самым изменив поведение функции на определенное время. Этот следует использовать с осторожностью и пониманием происходящего, особенно при множественных инжектах одной функции т.к. тогда может быть нарушена цепочка инжектов.
do
  local create = CreateUnit -- записываем CreateUnit в переменную
  local function CustomCreateUnit(p,unitid,x,y,face)
    -- здесь могла быть ваша реклама или ваш код
    local u = create(p,unitid,x,y,face) -- вызываем оригинальное создание юнита
    -- здесь могла быть ваша реклама или ваш код
    return u -- возвращаем созданного юнита
  end

  local f = main-- записываем main в переменную
  function main() -- заменяем оригинальную main своей
    CreateUnit = CustomCreateUnit
    f() -- вызываем оригинальную main из переменной
    CreateUnit = create -- возвращаем CreateUnit к оригинальному виду
  end
end
В результате этого инжекта, для каждого юнита созданного путем постановки его на карту в WE или в триггерах инициализации выполнится кастомный код из CustomCreateUnit, но после этого CreateUnit вернется к прежнему виду и после инициализации карты кастомный код при создании юнитов выполняться перестанет.
Более того, можно не вызывать оригинальную CreateUnit из CustomCreateUnit, тогда юнит не будет создаваться. Можно использовать это для размещения в WE юнитов-индикаторов которые не нужны во время игры или, например, для отметки мест спавна юнитов без обязательного спавна со старта игры - в CustomCreateUnit нам доступна информация об игроке для которого создается юнит, равкод юнита в числовом виде, координаты и угол поворота, можно иcпользовать эти данные на месте или сохранить их для дальнейшего использования. Отмену создания юнита стоит использовать с большой осторожностью - если отменить создание юнита, который записывается WE в глобальную переменную или к которому добавляется дроп предметов или задаются нестандартные параметры вроде уровней способностей, то могут возникнуть неприятные последствия, с другой стороны, все эти дополнительные действия тоже можно подвергнуть инжекту и использовать их для получения еще большего количества данных.

Другие примеры использования

`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
29
5 лет назад
Отредактирован nazarpunk
0
Сам хотел об этом написать, но всё руки не доходили.

Есть ещё один хороший инжект от PT153:
DestroyTimerOrigin = DestroyTimer
---@param timer timer
DestroyTimer       = function(timer)
	PauseTimer(timer)
	DestroyTimerOrigin(timer)
end
2
24
5 лет назад
2
Исправил часть ошибок в коде, добавил еще один пример инжекта.
0
28
5 лет назад
0
По поводу инжекта main: иными словами я вовсе могу изменить данную функцию, и во время инициализации запустится моя версия?
0
17
5 лет назад
Отредактирован GetLocalPlayer
0
Это вернее будет назвать хуком, а не инжектом. Как минимум прояснится внезапное появление термина "которую мы хукали выше".
В качестве мощного примера можно было бы прицепить ссылки на автоустранение утечек бж функций и замена вейта на адекватно работающий. Или это только на хайве было?
0
24
5 лет назад
0
PT153, да, можно не вызывать оригинал, но есть два нюанса. Первый - старый main останется в коде карты мертвым грузом, он не будет вызываться, но в коде останется. Инжект должен быть после объявления оригинала, иначе оригинал перезапишет то что мы инжектим, а не наоборот - из WE инжектом до мейна не достать, только при использовании внешнего редактора.
0
24
5 лет назад
0
Внес коррективы, чтобы не было путаницы с инжектами и хуками.
0
26
5 лет назад
Отредактирован Hanabishi
0
На самом деле вообще стоит писать полностью кастомную функцию main. Большая часть ее функционала лишь засоряет карту, как та же инициализация триггеров и глобалок (очевидно если гуи мы не пользуемся), инициализация стандартной музыки, погоды, времени итд. В большинстве случаев все это можно выпилить, заодно и загрузка будет побыстрее.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.