Timed actions в Build 42: очередь и жизненный цикл
Timed action — это действие, которое персонаж выполняет некоторое время: например, ест, режет или чинит. В руководстве она показана как таблица Lua, производная от ISBaseTimedAction и связанная с Java-классом zombie.characters.CharacterTimedActions.LuaTimedActionNew. Автор указывает каталоги действий игры shared/TimedActions/ (143 файла) и client/TimedActions/ (21 файл); числа и пути относятся к его снимку.
Действия добавляются в очередь и не запускаются немедленно.
ISTimedActionQueue.add(MiAccion:new(player, item))
Файл ISTimedActionQueue.lua выполняет действия по очереди. Источник также упоминает addAfter(anterior, accion), clear(character), hasActionType(character, "ISEatFoodAction") и isPlayerDoingAction(player). Имена API сохранены как в оригинале.
Жизненный цикл
Согласно описанию автора, Java вызывает функции в следующем порядке:
new() ← вызывается при создании: сохраняет данные и вычисляет maxTime
│
isValidStart() ← можно ли начать?
waitToStart() ← возвращает true, пока нужно ждать, например поворота
start() ← один раз: анимация, звук, модели в руках
update() ← каждый tick: прогресс и проверки
isValid() ← каждый tick: false отменяет действие
│
├── при отмене (движение, персонажа атакуют, ошибка isValid) → stop()
│
└── при завершении → perform() (видимая очистка на клиенте)
complete() (эффект; см. раздел о мультиплеере)
new() сохраняет данные и вычисляет maxTime; isValidStart() решает, можно ли начать; waitToStart() возвращает true, пока нужно подождать, например чтобы повернуться; start() один раз задаёт анимацию, звук и предметы в руках; update() вызывается каждый tick для прогресса и проверок; isValid() проверяется каждый tick, и false отменяет действие через stop(). При завершении вызываются perform() (видимая очистка на клиенте) и complete() (сам игровой эффект, согласно разделу о мультиплеере).
В ISBaseTimedAction:new можно менять stopOnWalk, stopOnRun и stopOnAim (по умолчанию true), caloriesModifier и maxTime. Java-класс LuaTimedActionNew также читает поля useProgressBar, forceProgressBar, loopedAction и allowedWhileDraggingCorpses, если они заданы. В примере еды stopOnWalk = false позволяет есть на ходу.
Пример Pumpkin Z DIY
Автор показывает PumpkinZCarveAction.lua из своего мода Pumpkin Z DIY как короткое действие для B42. Это ориентир, а не готовый код: фрагмент намеренно опускает замену тыквы и сетевую синхронизацию (многоточие).
PumpkinZCarveAction = ISBaseTimedAction:derive("PumpkinZCarveAction")
function PumpkinZCarveAction:isValid() -- ¿todavía tengo la calabaza y el cuchillo?
local inv = self.character:getInventory()
return self.pumpkin and self.knife and inv:contains(self.pumpkin) and inv:contains(self.knife)
and PumpkinZ.isValidCarve(self.carve)
end
function PumpkinZCarveAction:update()
self.pumpkin:setJobDelta(self:getJobDelta()) -- la barrita sobre el ícono del ítem
end
function PumpkinZCarveAction:start()
self.pumpkin:setJobType(getText("IGUI_PumpkinZ_Carving"))
self:setActionAnim("Craft") -- animación del personaje
self:setOverrideHandModels(self.knife, self.pumpkin) -- qué tiene en cada mano
self.sound = self.character:playSound("SliceFood")
end
function PumpkinZCarveAction:stop() -- cancelada
self:stopSound()
self.pumpkin:setJobDelta(0.0)
ISBaseTimedAction.stop(self) -- ¡siempre llamar al padre!
end
function PumpkinZCarveAction:perform() -- terminó (lado visible)
self:stopSound()
self.pumpkin:setJobDelta(0.0)
ISBaseTimedAction.perform(self) -- ¡siempre! avisa a la cola que siga
end
function PumpkinZCarveAction:complete() -- terminó (el efecto)
... quita la calabaza, agrega la tallada, sincroniza ...
return true
end
function PumpkinZCarveAction:getDuration()
if self.character:isTimedActionInstant() then return 1 end -- modo debug "acciones instantáneas"
return math.min(1800, 90 + PumpkinZ.countCuts(self.carve) * 8)
end
function PumpkinZCarveAction:new(character, pumpkin, knife, carve)
local o = ISBaseTimedAction.new(self, character)
o.pumpkin, o.knife, o.carve = pumpkin, knife, carve
o.maxTime = o:getDuration()
return o
end
В примере isValid() повторно проверяет наличие тыквы и ножа и их наличие в инвентаре, а также допустимость резьбы. update() обновляет полосу прогресса значка. start() задаёт название задания, анимацию Craft, предметы в руках и звук SliceFood. stop() останавливает звук, обнуляет прогресс и вызывает метод базового класса.
perform() очищает звук и прогресс и обязательно вызывает ISBaseTimedAction.perform(self), чтобы очередь продолжилась. В complete() должен находиться эффект: убрать тыкву, добавить вырезанную и синхронизировать; метод возвращает true. getDuration() возвращает 1 для мгновенных действий, иначе ограничивает значением 1800 выражение 90 + countCuts(carve) × 8. new() сохраняет предметы и вычисляет maxTime через getDuration().
Длительность
maxTime не измеряется в секундах. ISBaseTimedAction:create() вызывает adjustMaxTime и увеличивает длительность с учётом состояния персонажа. Пример из руководства:
maxTime = maxTime * (1 + infeliz / 4) -- nivel del moodle de infelicidad
maxTime = maxTime * (1 + borracho / 4)
maxTime = maxTime * (1 + dolorDeManos / 300) -- salvo que pongas ignoreHandsWounds = true
maxTime = maxTime * character:getTimedActionTimeModifier() -- temperatura del cuerpo
Формула умножает maxTime на коэффициенты, связанные с уровнем moodle несчастья, опьянением, травмой рук (кроме случая ignoreHandsWounds = true) и character:getTimedActionTimeModifier() (в тексте этот параметр связан с температурой тела). Если нужна точная длительность, например совпадающая с анимацией, автор предлагает переопределить adjustMaxTime и возвращать исходный maxTime без изменений; в качестве примера он называет DanceAction из Batalla de Aura.
Мультиплеер: perform() и complete()
Руководство представляет разделение perform/complete как важное изменение B42 на основании анализа LuaTimedActionNew. Это утверждение источника для конкретной версии; его нужно независимо проверить на целевой сборке.
public void complete() {
super.complete();
if (!GameClient.client) { // ← solo si NO soy un cliente
LuaManager.caller.pcall(LuaManager.thread, this.table.rawget("complete"), this.table);
}
}
В показанном коде сначала вызывается Java-метод super.complete(). Затем Lua-функция complete вызывается только если GameClient.client равно false. При создании действия флаг useCustomRemoteTimedActionSync включается, если в метатаблице нет complete():
if (table.getMetatable().rawget("complete") == null) {
this.useCustomRemoteTimedActionSync = true; // ← sin complete(): modo antiguo
}
| Где выполняется код | perform() | complete() |
|---|---|---|
| Одиночная игра | Да | Да |
| Клиент, подключённый к серверу | Да | Нет |
| Сервер | — | Да |
Согласно источнику, если действие определяет complete(), клиент запрашивает его у сервера, а Java управляет транзакцией через ActionManager; complete() выполняется на сервере. Автор советует размещать там эффект, чтобы решение принимал сервер. Без реальных проверок это нельзя считать универсальной гарантией защиты от читов.
Класс действия должен находиться в shared/, чтобы сервер мог создать его по имени. Если complete() не определён, применяется старый режим: действие целиком выполняется на клиенте без синхронизации. Автор считает его подходящим только для локальных действий, а не для изменений мира или инвентаря.
Изменения complete() нужно сообщать клиентам. В Pumpkin Z DIY после изменения инвентаря используются sendRemoveItemFromContainer и sendAddItemToContainer. Автор говорит, что isValid() выполняется на обеих сторонах; пример повторно проверяет isValidCarve, поскольку входящие от клиента данные нельзя считать доверенными.
Сначала подойти, затем действовать
Для действия над объектом мира сначала нужно подойти к нему. По словам автора, игра ставит в очередь ISWalkToTimedAction (52 использования) либо вызывает luautils.walkAdj(player, square) (281 использование), который ищет свободную соседнюю клетку, а затем добавляет действие:
if luautils.walkAdj(player, objeto:getSquare()) then
ISTimedActionQueue.add(MiAccion:new(player, objeto))
end
Упражнения
11.1 · Поджарить sopaipilla
Реализуй CursoLab.FreirAction: имея сковороду и сырую sopaipilla в инвентаре, приготовить её и получить готовую. Нужны анимация и звук, видимая в руке сковорода, отмена при отпускании предмета и сокращение времени с ростом Cooking. Готово, когда действие работает, корректно отменяется и после отмены можно добавить другое действие без блокировки персонажа.
11.2 · Ошибка очереди
Убери ISBaseTimedAction.perform(self), добавь в очередь два последовательных действия жарки и опиши результат. Найди в ISTimedActionQueue.lua невызванную функцию и объясни причину.
11.3 · Действие над объектом мира
Создай действие «Постучать в дверь»: персонаж подходит с помощью luautils.walkAdj, стучит несколько секунд и затем издаёт шум, привлекающий зомби. Найди в игре способ вызвать шум через addSound.
11.4 · Измерить длительность
Добавь print в start и perform, измеряй реальные секунды через getTimestampMs() при maxTime = 100: сначала на обычном персонаже, затем с сильным несчастьем. Сравни результат с adjustMaxTime.
11.5 · Подготовить действие к мультиплееру
- Файл действия находится в shared/?
- Эффект выполняется в complete(), и функция возвращает true?
- isValid() проверяет все данные, пришедшие от клиента?
- Изменения инвентаря передаются через sendAddItemToContainer и sendRemoveItemFromContainer?
Реальную проверку в мультиплеере источник оставляет модулю 12 с локальным сервером.
Задача со звёздочкой · Повторяющееся действие
Реализуй действие в цикле — например, ловлю рыбы до остановки или рубку дерева, пока не заполнится инвентарь. Оно должно повторяться до условия или отмены игроком. Найди игровой шаблон; подсказка — поле loopedAction, которое читает LuaTimedActionNew.