Как писать скрипты, не приводящие к вылетам и бою сейвов (часть 2) — S.T.A.L.K.E.R. Inside Wiki

Как писать скрипты, не приводящие к вылетам и бою сейвов (часть 2)

Материал из S.T.A.L.K.E.R. Inside Wiki

Перейти к: навигация, поиск

Начало в первой части статьи



Как безопасно использовать коллбэки и таймерные события

В скриптовом движке есть удобный способ реакции на события - использование коллбэков - процедурных вызовов, привязанных к определённым событиям в жизни игрового объекта - получению повреждений, спавну, смерти и т.д. и т.п. Это hit_callback, death_callback из xr_motivator и многие другие... Все моддеры очень широко и совершенно спокойно пользуются ими, совершенно забывая при этом о таком важном факте, что это - обработки реального времени, как и таймерные события. Что это значит? А собственно вот что... Возьмём для примера коллбэк смерти неписей, мою головную боль последнего времени:

xr_motivator.script -
function motivator_binder:death_callback(victim, who)

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

Так вот, обычно, когда в функциях возникает конфликт параметров, бесконечный цикл, попытка индексации nil и т.д., функция вылетает со стандартным логом. Но только не в случае, когда она находится внутри коллбэка. Если функция внутри него, то в случае возникновении в ней любой нештатной ситуации, коллбэк наглухо виснет. Это происходит из-за того, что функции вызываются строго друг за другом, и каждая из них вызывается только тогда, когда её предшественница вернула управление коллбэку. В случае с death_callback опознать такого непися очень просто - в его трупе окажется фонарик, КПК и возможно ещё немного разных "мусорных" вещей, что говорит о том, что обработка его смерти повисла не дойдя даже до спавна лута. В подобной ситуации можно быть на 100% уверенным, что труп этот не был корректно разрегистрирован, и игра всё ещё считает его живым неписем. Кроме того, зависший коллбэк не освобождает стек (а он у Луа-подсистемы единый на все скрипты), что в итоге приводит к вылетам игры с переполнением памяти (вот она, реальная причина этих "родных" вылетов). Но было бы слишком хорошо, если бы всё ограничивалось этим... однако тут всё намного хуже... такие "зависшие" коллбэки, особенно если их произошло несколько подряд, очень серьёзно влияют на работу а-лайфа. В лучшем случае они, забивая, стек, мешают нормально работать схемам логики, в худшем вызывают зависания самого а-лайфа (этот эффект, кстати, производят и сами трупы таких неписей, так как они, как мы помним, не разрегистрировались корректно). Основной итог таких событий - бой сейвов, сделанных после возникновения таких ситуаций. Если повис один коллбэк, то такие сейвы ещё через раз загружаются, если же несколько, и остановилась работа а-лайфа - всё, сейвы бьются наглухо и реанимации не подлежат.

Поэтому, чтобы избежать такого развития событий, каждый раз, когда вы вносите в коллбэк новую функцию - проверьте её самым тщательным образом. Она не должна содержать никаких рекурсивных циклов, в ней обязательно должны быть проверки на валидность обрабатываемых объектов и значений, и обязательно должна быть обработка нештатных ситуаций - т.е. функция должна обязательно, в абсолютно любой ситуации вернуть управление коллбэку, так или иначе. В самом наихудшем случае - делайте как делали разработчики игры - вставляйте принудительный вылет на рабстол функцией abort - она позволяет передавать отладочное сообщение, и это всяко лучше чем незаметный бой сейвов. Если обработка оборвалась в самом начале, а ф-ция обязательно должна вернуть значение - заведите ей "безопасное" возвращаемое значение по-умолчанию, которое она будет выдавать, если всё пошло плохо. И никогда не пренебрегайте пошаговой отладкой коллбэков с выводом в лог, особенно когда пишете схемы логики - это критически важно для стабильности вашего мода.

Основные внутриигровые признаки зависания коллбэков типа hit_callback, death_callback

1. В трупах попадаются фонарики, КПК, разный мусор и общий лут слишком богат.

2. Частые вылеты во время интенсивных боёв с логами типа

   Sheduler tried to update object...
   smart_terrain:1145(1146)
   LUA: out of memory
   любой_модуль_логики:любая_cтрока - stack overflow

3. Частые "родные" вылеты в момент смерти непися или попадания по нему

4. Произвольно бьются сейвы во время сражений, выброса и других насыщенных действиями событий

Использование защищённого кода в LUA

Периодически случаются такие ситуации, когда мы можем получить вылет при проверке аргумента, и не можем его адекватно заизолировать с помощью предварительной проверки на валидность значения. Вот простой пример: когда я отлаживал death_callback неписей, я периодически сталкивался с тем, что обращение к методу smart_terrain_id() при смерти непися иногда вызывало вылет

smart_terrain:1143 "attempt to index a nil value"

- хотя это свойство является родным методом объекта, и отсутствовать напрочь никак не может. В итоге я пришёл к выводу, что его просто иногда не успевает отработать движок, так как вылет этот проявлялся в основном в интенсивных боях и совершенно произвольно.

Вот код, в котором происходил вылет:

function on_death( obj_id )
-- printf( "on_death obj_id=%d", obj_id )
 
local sim = alife()
 
if sim then
local obj = sim:object( obj_id )
local strn_id = obj:smart_terrain_id() --- вылет происходит тут
 
if strn_id ~= 65535 then
sim:object( strn_id ).gulag:clear_dead(obj_id)
end
end
end

Это кусок родного кода из версии 1,0005 игры. Я долго пытался разными способами отсечь этот вылет, вводя предварительные проверки, однако это совершенно ничего не давало - вылет всё равно периодически случался, так как проверки эти сами его вызывали. Тогда я зарылся в документацию по Lua и обнаружил замечательную родную базовую функцию, введённую ещё с первых версий Lua, которой почему-то не пользовались ни разработчики игры, ни моддеры (хотя сама она в Lua сталкера присутствует, и работает отлично, без каких-либо нареканий). Вот она:

pcall (f, arg1, ···)

Вызывает функцию f с указанными через запятую аргументами в защищённом режиме. Это означает, что любая ошибка, даже критическая, внутри вызыванной функции, не передаётся наружу - вызавшей подсистеме. Вместо этого pcall перехватывает ошибку и возвращает код статуса. Первая возвращаемая переменная это сам код, (true или false) и если всё прошло хорошо, он равен true. В этом случае pcall сразу после статуса возвращает все результаты от работы защищённой им функции. Если же в защищённой функции произошла ошибка, то pcall вернёт false и затем сообщение об ошибке. (Обратите внимание, обработка ошибки присходит БЕЗ вылета! Вместо вылета вы получите вполне адекватную строку с ошибкой, которую можно вывести в лог для последующей обработки)

Я настоятельно советую пользоваться этой функцией в случаях, когда из коллбэков вызываются сложные комплексные обработки, вроде обработки из менеджера вооружений AI-пака. Это позволяет предотвратить как вылеты, так и зависания обработок, и в итоге позволяет хорошо стабилизировать игру.

Возвращаясь к нашим смарттеррейнам... вот как в итоге я подавил вылет типа smart_terrain:1143 с помощью pcall:

--- эта функция пытается проверить св-во smart_terrain_id объекта. Именно её мы вызовем в защищённом режиме.
function prot_smt_td(obj)
if IsStalker(obj) or IsMonster(obj) then
return obj:smart_terrain_id()
else
return 65535
end
end
 
 
function on_death( obj_id )
-- printf( "on_death obj_id=%d", obj_id )
local sim = alife()
if sim then
local obj = sim:object( obj_id )
if obj then
local strn_id = 65535 --- предварительно проинитим переменную, на
--- случай если у нас prot_smt_td выдаст ошибку
local result, smt_id = pcall(prot_smt_td,obj) --- вызываем prot_smt_td в защищённом режиме
--- и сразу присваиваем его вывод переменным
if result then --- если pcall выдало true
strn_id = smt_id --- тогда применям полученное значение
end
--- если же обработка выдаст ошибку, то strn_id останется неизменным...
if strn_id ~= 65535 then
sim:object(strn_id).gulag:clear_dead(obj_id)
end
end
end
end

В других местах это делается совершенно аналогично. Подробнее об этой и многих других функция для контроля кода, незаслуженно ингорируемых большинством моддеров, можно почитать тут: http://lua-users.org/wiki/FinalizedExceptions - на английском правда, но захотите - разберётесь, там всё просто.

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

--KamikaZze (OGSE Team) 11:04, 3 сентября 2009 (UTC)

Авторы

Статья создана: Kamikazze

Другие места
LANGUAGE