Lua-скриптинг: Хуки и жизненный цикл
Эта страница описывает каждый хук, который может определить Lua-пауэр, порядок выполнения хуков, как каждый босс получает собственную изолированную среду выполнения, и какие функции стандартной библиотеки доступны внутри песочницы.
Если вы ещё не писали Lua-пауэр, начните с Начало работы.
Эта страница документирует хуки для Lua-пауэров боссов в plugins/EliteMobs/powers/. Lua-скрипты NPC используют собственную папку plugins/EliteMobs/npc_scripts/ и специфичные для NPC хуки, такие как on_npc_interact и on_npc_proximity_enter; см. Скрипты NPC.
Справочник хуков
Каждый файл Lua-пауэра возвращает таблицу. Каждый ключ в этой таблице (кроме api_version и priority) должен быть одним из хуков, перечисленных ниже. Среда выполнения вызывает соответствующую функцию всякий раз, когда срабатывает соответствующее игровое событие.
| Хук | Когда вызывается | Доступен ли context.player? |
|---|---|---|
on_spawn | Спавнится elite-моб | Нет |
on_game_tick | Каждый серверный тик, пока часы среды активны и не приостановлены; обычно каждые 50 мс при 20 TPS | Нет |
on_boss_damaged | Босс получает урон из любого источника | Нет |
on_boss_damaged_by_player | Босс получает урон от игрока | Да |
on_reinforcement_damaged_by_player | Призванное этим боссом подкрепление получает урон от игрока; хук выполняется в способностях призывателя | Да |
on_boss_damaged_by_elite | Босс получает урон от другого elite-моба | Нет |
on_player_damaged_by_boss | Игрок получает урон от этого босса | Да |
on_enter_combat | Босс вступает в бой | Да |
on_exit_combat | Босс выходит из боя | Нет |
on_heal | Босс исцеляется | Нет |
on_boss_target_changed | EliteMobs сообщает о выборе игрока целью, включая первоначальный выбор; не обо всех сменах или потерях цели | Да |
on_death | Босс умирает | Нет |
on_phase_switch | Фазовый босс переключается на новую фазу | Нет |
on_mind_action | Нативный Mind запрашивает действие у назначенных способностей; см. контекст действия | Нет |
on_zone_enter | Сущность входит в отслеживаемую зону | Да (если сущность — игрок) |
on_zone_leave | Сущность покидает отслеживаемую зону | Да (если сущность — игрок) |
Когда context.player указан как «Нет», обращение к нему возвращает nil. Всегда проверяйте на nil перед использованием.
on_boss_damaged и более конкретные хуки урона используют разные пути событий. Один удар может вызвать оба, поэтому они не исключают друг друга. Хук цели не позволяет отменить событие через эту таблицу Lua и не сообщает предыдущую цель.
on_zone_enter / on_zone_leave не вызываются зонами LuaЭти два хука принимаются и проверяются загрузчиком, но ничто в Lua-способности не может заставить их сработать. Они диспетчеризуются только из слушателя зон EliteScript, который запускается, когда YAML-eliteScript: на том же боссе перечисляет ZoneEnterEvent или ZoneLeaveEvent в своих Events:. Босс, у которого есть только Lua-способности, загрузит файл с этими хуками без нареканий и затем никогда их не вызовет.
Наблюдатели Lua — это отдельный механизм: context.zones:watch_zone(...) и context.script:zone(...):watch(...) вызывают переданные вами коллбэки on_enter / on_leave напрямую и никогда не проходят через эти хуки верхнего уровня. Используйте наблюдатели, если только вы намеренно не сочетаете Lua-способность с YAML-скриптом зон.
События зон YAML получают все подходящие способности босса. В событии есть входящая или выходящая сущность, но нет идентификатора исходной зоны или скрипта. Поэтому несколько слушателей YAML могут отправить несколько событий для одной сущности.
Lua-скрипты NPC устроены иначе. Их context.zones принадлежит MagmaCore, чей watch вызывает on_zone_enter / on_zone_leave. См. Скрипты NPC.
Типичный пауэр с несколькими хуками
Один Lua-пауэр может определять столько хуков, сколько ему нужно. Ниже приведён скелет, использующий три хука вместе:
return {
api_version = 1,
on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,
on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns:check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,
on_death = function(context)
-- Spawn an explosion particle on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}
Данные события (context.event)
Некоторые хуки получают таблицу context.event, которая предоставляет данные об игровом событии, запустившем хук. Доступные поля зависят от того, какой хук выполняется.
Хуки урона
Применяется к on_boss_damaged, on_boss_damaged_by_player, on_reinforcement_damaged_by_player, on_boss_damaged_by_elite и on_player_damaged_by_boss.
| Поле / метод | Тип | Описание |
|---|---|---|
event.damage_amount | double | Значение урона на момент создания таблицы события |
event.get_damage_amount() | double | Текущий урон с учётом предыдущих изменений |
event.damage_cause | string | Имя Spigot DamageCause (например, "ENTITY_ATTACK", "PROJECTILE") |
event.damager | таблица сущности | Непосредственный источник Bukkit, если базовое событие связано с уроном от сущности, в том числе в on_boss_damaged. При попадании снаряда это сам снаряд, а не стрелок. |
event.projectile | entity table | Сущность-снаряд, если урон нанёс снаряд. |
event.set_damage_amount(n) | — | Переопределить урон фиксированным значением |
event.multiply_damage_amount(n) | — | Умножить текущий урон на n |
event.cancel_event() | — | Полностью отменить событие урона |
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end
Вызывайте event.set_damage_amount(n) и event.multiply_damage_amount(n) через точку. Эти методы читают первый аргумент напрямую и не удаляют self, переданный при вызове через двоеточие. event:transfer_damage(target, amount) поддерживает двоеточие.
В хуках урона игрока по элите event.entity указывает пострадавшую элиту. В on_reinforcement_damaged_by_player поле context.boss по-прежнему означает призывателя, а context.player указывает атакующего игрока. Хук получает исходное изменяемое событие урона подкрепления.
Эти хуки также предоставляют event.is_damage_transfer и event:transfer_damage(target, amount). Целью должна быть другая живая элита, на которую босс-владелец может воздействовать. Величина должна быть конечной и неотрицательной. Отменённое событие, нулевая величина или вложенный перенос возвращает false. Допустимый вызов отправляет отдельный удар, приписанный игроку, и возвращает true. Он не вычитает урон из исходного удара и не гарантирует, что другой слушатель разрешит новый удар. Используйте event.set_damage_amount(...), чтобы разделить урон вместо его дублирования. Перенесённый урон нельзя рекурсивно перенести снова.
Хук спавна
Применяется к on_spawn.
| Поле / метод | Тип | Описание |
|---|---|---|
event.spawn_reason | string | Имя Spigot SpawnReason |
event.cancel_event() | — | Отменить спавн |
Хук смерти
Применяется к on_death.
| Поле / метод | Тип | Описание |
|---|---|---|
event.entity | entity table | Умирающая сущность |
Хуки зон
Применяется к on_zone_enter и on_zone_leave.
| Поле / метод | Тип | Описание |
|---|---|---|
event.entity | entity table | Сущность, входящая в зону или покидающая её |
Созданные в Lua наблюдатели зон не заполняют context.event; они передают входящую/выходящую сущность непосредственно в свой коллбэк.
Отменяемые события (общее)
Хуки с отменяемым базовым событием предоставляют event.cancel_event(). В on_game_tick, on_mind_action и обычных коллбэках планировщика context.event равен nil. У хуков вроде on_heal есть базовое событие, но эта таблица не содержит специальных полей лечения. Проверяйте документированные члены перед вызовом.
Таблица событий босса не предоставляет cancel(), uncancel() и is_cancelled общей обёртки. Отмена прекращает передачу события следующим способностям того же босса, но не завершает текущую функцию Lua. Добавьте return, если оставшийся код функции не должен выполняться.
Полные поля таблицы сущности см. в Босс и сущности. Значения причин урона и причин спавна см. в Перечисления и значения.
Порядок выполнения хуков
Диспетчер событий босса обходит назначенные скриптовые способности в порядке выполнения. Поле priority способности Lua определяет её позицию:
- Меньшие значения выполняются первыми (по умолчанию
0). - При равных приоритетах сохраняется порядок добавления способностей боссу. Не рассчитывайте на одинаковый порядок при разных путях загрузки.
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}
Способности YAML EliteScript участвуют в том же диспетчере с приоритетом 0. Отрицательный приоритет Lua выполняется до них, положительный после. Это порядок одного события на одном боссе, а не отдельных событий Bukkit или запланированных задач. Уже отменённое событие останавливает диспетчер, поэтому его получают не обязательно все способности. После способностей самого подкрепления его прямой призыватель получает хук урона подкрепления только если событие всё ещё не отменено.
Модель среды выполнения
Одна среда на способность и босса
Каждая назначенная способность Lua получает отдельную среду для каждого босса. При запуске EliteMobs выполняет загруженный код в новой песочнице и сохраняет возвращённую таблицу. При исчезновении или удалении босса среда закрывается. Таким образом, у босса с двумя способностями Lua две независимые таблицы состояния и среды.
Это означает:
- Локальные переменные и функции файла, а также определённые вами глобальные переменные Lua принадлежат только среде этой способности на этом боссе.
- Функции хуков возвращаемой таблицы никогда не разделяются между боссами.
Загрузчик также выполняет файл один раз для проверки возвращаемой таблицы. Инициализация на уровне файла не должна зависеть от контекста хуков. Способности только с тиковым хуком не нужен on_spawn для запуска часов. Инициализация среды и коллбэк спавна выполняются отдельно.
Изоляция состояния
У каждой среды своя таблица context.state, отдельная от других способностей этого босса и той же способности на других боссах. Храните в ней счётчики, флаги, таймеры и другие данные способности между хуками. Состояние существует во время активной среды и не сохраняется на диск между исчезновениями или перезапусками.
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}
Владение запланированными задачами
Все задачи, созданные через context.scheduler, принадлежат среде выполнения, которая их создала. Когда босс деспавнится:
- Среда выполнения вызывает
shutdown(). - Каждая принадлежащая задача — как одноразовая (
run_after), так и повторяющаяся (run_every) — автоматически отменяется. - Все наблюдатели зон очищаются.
- Изменения ИИ, осведомлённости, масштаба и неуязвимости, которые эта способность ещё удерживает, восстанавливаются, установленный ею флаг лечения снимается, а её контроллер самонаводящихся огненных шаров останавливается.
Среда очищает принадлежащие ей задачи планировщика при удалении босса. Некоторые игровые функции Java запускают отдельные задачи с собственным сроком жизни; проверяйте их документацию. Отменяйте повторяющиеся задачи, когда они больше не нужны во время игры:
return {
api_version = 1,
on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}
Поведение потиковых часов
Внутренние тиковые часы для экземпляра Lua-пауэра работают только тогда, когда пауэр определяет хук on_game_tick. Наблюдатели зон, созданные через context.zones:watch_zone(...) или context.script:zone(...):watch(...), создают собственные принадлежащие повторяющиеся задачи вместо того, чтобы включать хук on_game_tick верхнего уровня пауэра.
Без on_game_tick и наблюдателей зон среда не регистрирует эти тиковые часы. Явные задачи планировщика и задачи игровых функций продолжают работать по собственному расписанию. Тиковая работа и принадлежащие среде наблюдатели отменяются при исчезновении босса или закрытии среды.
Приостановка сохраняет состояние Lua и принадлежащие среде задачи, пропуская их работу. Путь событий босса также проверяет подавление способностей. Удаление босса или закрытие среды завершает принадлежащую ей работу, а не просто приостанавливает её.
Ошибки и производительность
EliteMobs устанавливает строгие лимиты на ошибки и производительность для Lua-пауэров:
Исключения
Если функция хука или запланированный коллбэк выбрасывает ошибку Lua (или Java-исключение всплывает из вызова API), пауэр немедленно отключается для этого экземпляра босса. Среда выполнения завершается, и все принадлежащие задачи отменяются.
Экземпляр после ошибки остаётся закрытым; последующие хуки урона не создают его заново без уведомления. Исправьте и перезагрузите определение, затем создайте новую среду. Намеренное удаление владельцем может очистить старую ссылку для последующего восстановления тела.
Ошибка записывается в консоль вместе с именем файла способности и выполнявшимся хуком. Номер строки добавляется, если доступен:
[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.
Бюджет выполнения
Lua-способности выполняются под жёстким бюджетом выполнения, который принудительно применяется внутри самой Lua VM, а не просто замеряется постфактум. Каждый хук, каждый запланированный коллбэк и даже первоначальное вычисление файла выполняются в рамках этого бюджета:
| Ограничение | Значение |
|---|---|
| Процессорное время потока на точку входа | 50 ms |
| Резервное прошедшее время, если время CPU недоступно | 250 ms |
| Инструкций байткода на точку входа | 250,000 |
VM считает инструкции и проверяет время каждые 1024 инструкции. Ошибка указывает превышенный предел:
Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)
Бесконечный цикл Lua прерывается во время выполнения. Вложенные вызовы делят бюджет точки входа. VM не может прервать выполняющийся вызов Java API; отдельного предела реального прошедшего времени в 50 мс после возврата нет. Поэтому бюджет не делает затратные операции с миром безопасными.
Чтобы оставаться в рамках бюджета:
- Избегайте неограниченных циклов внутри хуков. Используйте
context.scheduler:run_every(...), чтобы распределить работу по тикам. - Делайте обработчики
on_game_tickлёгкими — они выполняются каждый тик. - Переносите тяжёлую инициализацию в
on_spawnилиon_enter_combat, а не повторяйте её каждый тик. - Помните, что циклы частиц по большим зонам — обычный способ сжечь 250,000 инструкций. Используйте
coverage, чтобы отбирать меньше точек.
Песочница Lua
Lua-пауэры выполняются внутри изолированной среды LuaJ (песочницы). Несколько глобальных объектов, которые могли бы получить доступ к файловой системе или среде выполнения Java, удалены.
Удалённые глобальные объекты
Следующие стандартные глобальные объекты Lua установлены в nil и не могут использоваться:
| Удалено | Причина |
|---|---|
debug | Раскрывает внутреннее состояние VM |
dofile | Доступ к файловой системе |
io | Доступ к файловой системе |
load | Загрузка произвольного кода |
loadfile | Доступ к файловой системе |
luajava | Прямой доступ к классам Java |
module | Система модулей (не нужна) |
os | Доступ к операционной системе |
package | Система модулей (не нужна) |
require | Система модулей / доступ к файловой системе |
Отладочные хуки VM по-прежнему устанавливаются внутренне — именно так бюджет выполнения считает инструкции, — но сама таблица debug сразу после этого устанавливается в nil, поэтому скрипты не могут до неё добраться.
Доступная стандартная библиотека
Песочница сохраняет стандартные библиотеки LuaJ, включая следующие распространённые функции:
| Категория | Функции |
|---|---|
| Math | math.abs, math.ceil, math.floor, math.max, math.min, math.random, math.sin, math.cos, math.sqrt, math.pi и все остальные функции math.* |
| String | string.byte, string.char, string.find, string.format, string.gsub, string.len, string.lower, string.match, string.rep, string.sub, string.upper и все остальные функции string.* |
| Table | table.insert, table.remove, table.sort, table.concat и все остальные функции table.* |
| Итераторы | pairs, ipairs, next |
| Типы | type, tostring, tonumber, select, unpack |
| Обработка ошибок | pcall, xpcall, error, assert |
| Прочее | print, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable |
Все строки используют одну общую заблокированную метатаблицу. getmetatable("") возвращает false, а вызовы методов у строк, такие как ("text"):upper(), всегда используют стандартные строковые функции, даже если скрипт добавит или заменит функции в своей собственной таблице string.
print пишет в консоль, но предпочтительнее использовать context.log:info(msg) или context.log:warn(msg). Логи босса не включают имя файла способности автоматически. Добавляйте его в сообщение, если несколько способностей могут писать одновременно.
Вспомогательное пространство имён em
Таблица em доступна во время загрузки файла (до выполнения любого хука). Она предоставляет вспомогательные конструкторы для построения таблиц локаций, таблиц векторов и определений зон, используемых по всему API.
| Функция | Назначение |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | Создать таблицу локации с опциональным именем мира, рысканием (yaw) и тангажом (pitch) |
em.create_vector(x, y, z) | Создать таблицу вектора |
em.zone.create_sphere_zone(radius) | Создать определение сферической зоны |
em.zone.create_dome_zone(radius) | Создать определение купольной зоны |
em.zone.create_cylinder_zone(radius, height) | Создать определение цилиндрической зоны |
em.zone.create_cuboid_zone(x, y, z) | Создать определение прямоугольной зоны (cuboid) |
em.zone.create_cone_zone(length, radius) | Создать определение конической зоны |
em.zone.create_static_ray_zone(length, thickness) | Создать определение зоны статического луча |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | Создать определение зоны вращающегося луча |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | Создать определение зоны перемещающегося луча |
Подтаблица em.location предоставляет общие запросы к миру только для чтения: is_in_dungeon, is_protected, owned_by, owners, kinds_at и has_kind. Первый аргумент представляет собой таблицу позиции. Если позиция отсутствует или не является таблицей, мир не указан или не загружен, возвращается false либо пустая таблица. Отсутствующие координаты считаются нулевыми; неверные типы координат всё же могут вызвать ошибку. При создании позиции для этих запросов явно указывайте мир. Сигнатуры и возвращаемые структуры см. в Справочнике Lua API.
-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end
Привязки Lua для проверки прав на строительство нет. em.location.is_protected сообщает, претендует ли на локацию какой-либо плагин защиты; он не разрешает права на строительство для конкретного игрока.
Конструкторы зон возвращают цепочечные (chainable) таблицы с :set_center(loc) (или :set_origin(loc) / :set_destination(loc) в зависимости от типа зоны). Они предназначены для использования в начале файла или внутри хуков:
-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())
local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}
Полное описание форм зон, фильтров, наблюдателей и паттернов нацеливания см. в Зоны и нацеливание.
Дальнейшие шаги
- Босс и сущности —
context.boss,context.player, обёртки сущностей - Мир и окружение — частицы, звуки, спавн,
context.world - Зоны и нацеливание — нативные зоны, утилиты скриптов,
context.zones/context.script - Примеры и паттерны — полные рабочие пауэры, которые можно изучить и адаптировать
- Перечисления и значения — ссылки на Spigot Javadoc для всех строковых констант
