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

Предисловие

Отвечая на вопросы в 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
24
5 лет назад
0
На самом деле вообще стоит писать полностью кастомную функцию main.
Иногда стоит, иногда и так сойдет. Имхо, это тема для отдельной статьи - оптимизация main и config.
0
24
5 лет назад
0
Добавил инжект DestroyTimer.
Добавил ссылку на фикс утекающих точек в ГУИ, тоже работающий через инжект.
0
28
5 лет назад
Отредактирован PT153
0
А где мой комментарий про инжект main?
Я же верно понимаю, что сначала выполнится блок do end с инжектом, и во время инициализации запуститься изменённая функция main?
Нашёл, спасибо за ответ.
0
24
5 лет назад
0
PT153, пропадающие комменты - беда редактирования, пока ресурс ждет публикации после редактирования - у него отдельные комменты, которые сливаются со старыми после публикации. Раньше была возможность переключать между последней опубликованной версией ресурса и ожидающей публикации, а сейчас куда-то пропала. Перечитай комменты еще раз - там и твой коммент есть и ответ на него.
0
21
5 лет назад
0
Стоит добавить хук функции InitBlizzard, так можно будет хукать не только InitGlobals, но и InitCustomTriggers, RunTriggerInitialization, CreateAllUnits и т.д.
0
29
5 лет назад
0
Стоит добавить хук функции InitBlizzard
Зачем? Из WE её не хукнешь, а если самому собирать, то можно сразу хукать main, из которого хукается всё.
0
24
5 лет назад
Отредактирован prog
0
ScopteRectuS, основная задача статьи - место куда можно отправлять читать на тему инжекта в целом и инжекта InitGlobals в WE, эта задача вполне выполняется в текущем виде. Все остальное - бонус. Двух шаговый инжект, да еще и специфичный для работы в WE мне не очень интересен, но если ты его подробнее распишешь, с кодом, или у меня будет свободное время - добавлю.
Из WE её не хукнешь
Почему же, вполне себе хукнеш, она же не в коде карты объявлена, а значит уже есть на момент выполнения кода карты.
В принципе, через двух шаговый инжект можно и до main добраться, хукнув SetMapName, которая вызовется из config, ведь config вызывается раньше main, если я еще не совсем с ума сошел.

Придется таки найти время и дописать этот двух шаговый инжект. Но не сегодня уже.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.