Как писать скрипты, не приводящие к вылетам и бою сейвов (часть 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 - на английском правда, но захотите - разберётесь, там всё просто.

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

Скрытые критические проблемы в обработке вылетов игрой

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

В одном из логов нашего бета-тестера я увидел стандартное сообщение о вылете внутри рабочего лога... да-да, то самое которое FATAL ERROR и дальше по тексту. При этом игра у него НЕ вылетала, это сообщение об ошибке мы обнаружили позже, по случайности. Я заподозрил, что что-то не в порядке, и вставил внутрь этой ф-ции контрольную метку, кидавшую в консоль сообщение, в котором содержался паттерн сообщения об ошибке и само сообщение. Так вот, оказалось, что эта самая функция abort вызывается в игре с завидным постоянством (вы удивитесь насколько часто), когда возникают исключения в схемах логики, звука и т.д., но игра от этого вылетает на рабочий стол максимум только 3 раза из 10 вызовов. Вылет НЕ происходит обычно, когда функции передан паттерн ошибки, а остальные параметры пустые, такое бывает, и частенько. И если не сделать внутри этой функции особой метки для вывода в лог, как сделал это я, её вызовы проходят совершенно незаметно, и игра после критических ошибок продолжается как ни в чём ни бывало. А приводит это вот к чему... Внутри xr_logic в процедуре записи пстора (хранилища логики и флагов) неписей есть вызовы этого самого аборта в случае если на запись в пстор передана некорректная величина. Ну а так как аборт периодически вообще не срабатывает, то часто попадается ситуация, что неписям в пстор пишется полный ахтунг: куски кода из ОЗУ, всякая муть из лтх-ов, куски аллспавна, всё что угодно. Происходит это оттого, что кодер, писавший эту функцию (xr_logic.pstor_store(obj, varname, val)), явно и думать не думал что abort может не сработать. У него запись в пстор стояла после проверки, а не внутри неё (very bad idea), и если abort не срабатывал, игра писала в пстор мусор совершенно спокойно и незаметно для игрока. Потом вся эта хрень попадала прямо в сейвы. Вот проблемный код для наглядности:

function pstor_store(obj, varname, val)
local npc_id = obj:id()
if db.storage[npc_id].pstor == nil then
db.storage[npc_id].pstor = {}
end
local tv = type(val)
if not pstor_is_registered_type(tv) then
abort("xr_logic: pstor_store: not registered type '%s' encountered", tv) --- вот тут мы должны если что вылететь
end
db.storage[npc_id].pstor[varname] = val -- а если не вылетели, всё, получим запись в пстор левой мути
end

Разумеется игра этим пстором в итоге давится, и сейвы сделанные после такой милой записи практически лопаются. Результат - "битые" (на самом деле подлежат реанимации) сейвы. Происходит это потому, что игра из сейва грузит неписям псторы сплошным чтением по словам, пока они не закончатся. В случае же если в псторе обнаруживается записанный ранее мусор, то обработка либо вылетает сразу, либо наглухо виснет, пытаясь запихать эдак с миллион слов в пстор особо отличившегося непися. Как вам например непись с размером пстора в 1697451 слова? В результате попытки его обработать игра просто на стадии синхронизации выжрала всю доступную ОЗУ и повисла.

Решение этой проблемы оказалось достатоно простым: во-первых я предположил максимальный размер полезной части пстора неписей в 20 слов r_u32() (пока ориентировочно, я ещё уточняю эту величину), и соответственно сделал остановку цикла загрузки пстора для неписей через 20 итераций. Там же, где в цикле стояла проверка на валидность записываемых данных (кстати тоже с вылетом в случае провала проверки), я сделал так, что если параметр не относится к валидному типу данных, то запись параметра в пстор не производится совсем. Это необходимо для того, чтобы если вдруг в сейве обнаружится мусор, то он был бы просто отброшен обработкой. Практика показала, что в итоге такие неписи вполне адекватны и в дальнейшем никаких проблем не вызывают, так как начало их пстора, с нормальными данными, обычно не повреждается - мусор дописывается после них, а не вместо них.

Ну и во-вторых модифицировал запись параметров в пстор, просто убрав запись под основание if-else так, чтобы если параметр неверен, он не записывался совсем. Теперь кстати, очень интересно, сохранились ли те же заморочки с ф-цией abort в Чистом Небе, и если да, то останутся ли в Зове Припяти?

Примечание другого автора: в Зове Припяти большая часть вылетов всё также не вылетает. Вы можете себя неприятно удивить, если раскомментируете в файле _g.script строку

--	error_log(reason)

Необходимые для стабилизации игры правки в модулях

Эта правка предотвращает запись в пстор если не сработал аборт:

xr_logic.script

Было:

function pstor_store(obj, varname, val)
local npc_id = obj:id()
if db.storage[npc_id].pstor == nil then
db.storage[npc_id].pstor = {}
end
local tv = type(val)
if not pstor_is_registered_type(tv) then
abort("xr_logic: pstor_store: not registered type '%s' encountered", tv)
end
db.storage[npc_id].pstor[varname] = val
end

Стало:

function pstor_store(obj, varname, val)
if not obj then return end
local npc_id = obj:id()
if db.storage[npc_id].pstor == nil then
db.storage[npc_id].pstor = {}
end
local tv = type(val)
if not pstor_is_registered_type(tv) then
dgblog("xr_logic: pstor_store: not registered type encountered - write in pstor_store cancelled")
-- abort убран, так как один хрен не работает. Пусть тогда хотя бы в лог что-то валится.
else
db.storage[npc_id].pstor[varname] = val
-- вот так и только так. Если значение не валидно, ничего не происходит.
end
end

А эта правка выкинет из пстора весь мусор при загрузке сейва, если он как-то в него попал

Было:

function pstor_load_all(obj, reader)
local npc_id = obj:id()
local pstor = db.storage[npc_id].pstor
if not pstor then
pstor = {}
db.storage[npc_id].pstor = pstor
end
local ctr = reader:r_u32()
for i = 1, ctr do
local varname = reader:r_stringZ()
local tn = reader:r_u8()
if tn == pstor_number then
pstor[varname] = reader:r_float()
elseif tn == pstor_string then
pstor[varname] = reader:r_stringZ()
elseif tn == pstor_boolean then
pstor[varname] = reader:r_bool()
else
abort("xr_logic: pstor_load_all: not registered type N %d encountered", tn)
end
printf("_bp: pstor_load_all: loaded [%s]='%s'", varname, utils.to_str(pstor[varname]))
end
end

Стало:

function pstor_load_all(obj, reader)
local npc_id = obj:id()
local pstor = db.storage[npc_id].pstor
if not pstor then
pstor = {}
db.storage[npc_id].pstor = pstor
end
local ctr = reader:r_u32()
if tonumber(ctr) > 20 and tostring(obj:name()) ~= "single_player" and npc_id ~= db.actor:id() then
-- максимум 20 итераций - это число ещё уточняется, возможно понадобится больше
-- если у вас в пстор что-то свое пишется, ориентируйтесь на свои значения
-- и обязательно убираем из проверки актора - у него очень толстый пстор, и к тому же
-- если уж поврежденным будет его пстор, то тут точно уже ничего не поможет
dgblog("ОБНАРУЖЕН ОБЪЕКТ С ПОВРЕЖДЕННЫМ PSTOR: "..tostring(obj:name())..
" БУДЕТ ПРОИЗВЕДЕНА ПОПЫТКА ВОССТАНОВЛЕНИЯ")
ctr = 20
end
for i = 1, ctr do
local varname = reader:r_stringZ()
local tn = reader:r_u8()
if tn == pstor_number then
pstor[varname] = reader:r_float()
elseif tn == pstor_string then
pstor[varname] = reader:r_stringZ()
elseif tn == pstor_boolean then
pstor[varname] = reader:r_bool()
else
-- не надо пытаться вылетать - просто не пишем поврежденные данные
-- при этом обязательно удалять саму переменную - в результате записи
-- мусора в пстор одно только ее название может повесить загрузку
pstor[varname] = nil
end
end
end

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

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

Я это сделал вот так:

_g.script

-- Крешнуть игру (после вывода сообщения об ошибке в лог)
function abort(fmt, msg)
local message = tostring(msg)
dbglog("ERROR PATTERN: "..tostring(fmt))
dbglog("ERROR REASON: "..message)
local reason = string.format(fmt, message)
assert("ERROR: " .. reason)
printf("ERROR: " .. reason)
dbglog("%s", reason)
printf("%s")
end

По поводу частичной неработоспособности ф-ции abort я беседовал с Колмогором, и он пришёл к выводу, что видимо вылет игры должен был бы производиться при обработке функции printf("%s") - ей тут передаётся заведомо отсутствующий оператор и она по уму должна бы сразу крашить игру. Однако в релизе функция printf фактически не работает(она реализована в _g.script через вырезанную функцию log). В результате игра не крашится. Но что поделать, в итоге мне пришлось модифицировать его таким образом, чтобы вылет при его срабатывании был гарантирован:

-- Крешнуть игру (после вывода сообщения об ошибке в лог)
function abort(fmt, msg)
local message = tostring(msg)
dbglog("ERROR PATTERN: "..tostring(fmt))
dbglog("ERROR REASON: "..message)
local reason = string.format(fmt, message)
assert("ERROR: " .. reason)
printf("ERROR: " .. reason)
dbglog("%s", reason)
printf("%s")
local crash
local ooops = 1/crash
end

Вылет происходит при попытке произвести арифметическую операцию с неинициализированной переменной crash.




Дополнение от cjayho (ecb team): Вообще крэшить игру ошибкой в скриптовом коде - далеко не самое очевидное решение. В windows-подобных системах статус, возвращаемый программой после ее выполнения, не имеет ни малейшего смысла, поэтому корректно ли мы выключим игру или некорректно - разницы не будет никакой. Поэтому есть смысл сделать крэш игры более очевидным и стопроцентно работающим способом: использовать консольную команду quit:

-- Крешнуть игру (после вывода сообщения об ошибке в лог)
function abort(fmt, msg)
local message = tostring(msg)
dbglog("ERROR PATTERN: "..tostring(fmt))
dbglog("ERROR REASON: "..message)
local reason = string.format(fmt, message)
assert("ERROR: " .. reason)
printf("ERROR: " .. reason)
dbglog("%s", reason)
get_console():execute( 'quit' )
end

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



Лечение зависаний алайфа при смерти персонажей

Недавно, отлаживая проблемы с зависанием алайфа, мне удалось найти причину этого периодически во всех модах всплывающего сбоя, приводящего к порче сейвов и сильно мешающего нормально играть. Сбой этот возникает при смерти некоторых NPC, обычно квестовых. В частности в моём случае изолировать и отладить это зависание удалось на Юрике, новичке со Свалке, учавствующем в сцене с гоп-стопом. Причина оказалась в обработке посмертной отрегистрации NPC из гулагов, причём сбой там был настоящей матрёшкой, составной из нескольких частей. Правок в итоге было совсем немного, но чтобы сделать их мне пришлось несколько часов распутывать клубок из кросс-вызовов между скриптами smart_terrain и xr_gulag. Итак, начнём с самого начала. Работая над OGSE 069 и 0691 я периодически сталкивался с зависаниями и вылетами в посмертных обработках неписей. Один из таких вылетов - всем хорошо знакомый вылет:

smart_terrain:1143 "attempt to index a nil value"

Происходящий в функции smart_terrain.on_death( obj_id ) - я тогда его заблокировал вызовом его внутри безопасного кода функцией pcall, однако, как теперь выяснилось, этого оказалось недостаточно - баг тут состоит из нескольких частей, и этот вылет указывает только на одну из них. Вот исходный код:

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

Во-первых выяснилось, что изредка вызов obj:smart_terrain_id() вызывает зависание алайфа даже когда он производится изнутри защищённого кода. Тогда я решил избавиться от использования этой функции в данном месте совсем. После нескольких экспериментов выяснилось, что самым простым, быстрым и вылетобезопасным способом будет считать нетпакет существа в таблицу и выудить идентификатор смарттеррейна из неё. Для этого можно написать свою обработку, однако я, как весьма ленивый программист, не склонен изобретать велосипеды, поэтому я воспользовался уже проверенной у нас и активно используемой в OGSE библиотекой функций для работы с нетпакетами m_net_utils Артоса. Кроме того, я сразу сделал более безопасным вызов обработки на отрегистрацию в гулагах. Вот, собственно, что в итоге получилось:

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 and obj.smart_terrain_id) then
local strn_id = 65535 -- значение по умолчанию
 
local t = nil -- сюда запихнём табличку из пакета
if IsStalker(obj) then t = m_net_utils.get_stalker_data(obj) elseif IsMonster(obj) then t = m_net_utils.get_monster_data(obj) end
-- вызываем парсинг пакета для неписей и монстров отдельно
 
-- print_table_inlog(t)
if t.smtrid then
strn_id = tonumber(t.smtrid) -- получаем идентификатор смарта, если его нету даже в пакете, хрен с ним, будет 65535
end
 
if strn_id ~= 65535 then -- если сняли идентификатор, попробуем отрегать...
local gulag = sim:object(strn_id)
if gulag and gulag.gulag then -- ...но сначала выясним если вообще такой гулаг и инициализирован ли он
sim:object(strn_id).gulag:clear_dead(obj_id)
end
end
end
end
end

При этом сделаю отсупление и предупрежу об одном очень странном сбое с которым я столкнулся редактируя эту функцию. Так вот, всё нормально работает только тогда когда у ф-ции этой есть строго определённая структура. Стоит только добавить пару строк, убрать закомментированную и сдвинуть пару условий, просто в тексте сдвинуть, не меняя внутренней логики, как игра начинает вылетать, причём ещё до загрузки сейва, при кэшировании (!) и с очень странными логами, хотя код написан синтаксически безупречно! Например с таким:

Description: xr_gulag:1035 value not found ObjectJobPathName[obj_id]

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

Итак, теперь проблема с получением идентификатора смарта разрешилась, однако алайф всё равно зависал! Простая трассировка показала, что теперь зависание происходило внутри обработки

sim:object(strn_id).gulag:clear_dead(obj_id)

И мне пришлость распутывать её по частям, доискиваясь до причины. Обработка честно говоря мерзкая, размазана по smart_terrain и xr_gulag, при этом взаимные вызовы идут не менее десятка раз, в следующем стиле: ф-ция в смарте вызывает ф-цию в гулаге, которая вызывает фцию в смарте, которая обрабатывает параметр фцией в гулаге, который передётся ф-цией в логике, которая получает её из смарта. Нечто подобное. Опуская все нецензурные выражения, употребленные мной при трассировке, я лучше расскажу что собственно вышло в итоге. А в итоге я вышел вот на этот код в xr_gulag, именно в нём при отрегистрации некоторых мёртвых неписей происходит зависание алайфа:

-- освободить объект от работы и переинициализировать логику.
-- если сталкер в онлайне и начал работу, то сбросить его схему поведения
-- как будто он только что загрузился
function gulag:free_obj_and_reinit( obj_id )
self:free_obj(obj_id)
 
local t = self.Object[obj_id]
if t ~= nil and t ~= true and self.Object_begin_job[obj_id] then
xr_logic.initialize_obj( t, nil, false, db.actor, self:get_stype( obj_id ) ) -- вот эта обработка вешает алайф
end
end

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

-- освободить объект от работы и переинициализировать логику.
-- если сталкер в онлайне и начал работу, то сбросить его схему поведения
-- как будто он только что загрузился
function gulag:free_obj_and_reinit( obj_id )
self:free_obj(obj_id)
local t = self.Object[obj_id]
if t ~= nil and t ~= true and self.Object_begin_job[obj_id] then
if check_game() then -- тут проверяется, запущена ли игра, если ли актор и жив ли он. Если да, делаем по новому.
local s_obj = alife():object(obj_id) -- проверим есть ли у цели разрегистрации валидный серверный объект
if s_obj and (IsStalker(s_obj) or IsMonster(s_obj)) and s_obj:alive() then -- если есть, он жив и сталкер или монстр
xr_logic.initialize_obj( t, nil, false, db.actor, self:get_stype( obj_id ) ) -- только тогда инициализируем логику
end
else -- а если игра не запущена, то как раньше. Это нужно для того, чтобы обработка запуска игры нормально работала.
xr_logic.initialize_obj( t, nil, false, db.actor, self:get_stype( obj_id ) )
end
end
end
 
-- Проверка, запущена ли игра
function check_game()
if level.present() and (db.actor ~= nil) and db.actor:alive() then
return true
end
return false
end

После этих поправок зависания алайфа при смерти неписей удалось побороть окончательно. Решение с отрезанием реинита логики для трупов несколько грубовато, но честно говоря, у меня нет ни малейшего желания трассировать и разбирать на части функцию xr_logic.initialize_obj, выясняя, чем же ей так данный конкретный труп не приглянулся. Если хотите - займитесь, найдёте причину - дополните данную статью. Я же успокоился на том, что заблокировал баг, периодически убивающий людям игру, так как многие пользователи, игнорируя предупреждения, играют на одном-двух сейвах, а то и вовсе на квиксейвах всю игру.

Другие частые проблемы

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

Вылеты при удалении объектов из игры

При использовании для удаления объектов родной движковой функции alife():release(alife():object(id), true) возможен целый ворох разнообразнейших вылетов, обычно - безлоговых, что сильно затрудняет их отладку. Вот из-за чего они возникают:

1) Вылет при удалении непися или монстра, находящегося в онлайне.

  Решение: с помощью alife():release можно удалять только мёртвые объекты. Поэтому если вам нужно удалить с её помощью непися или монстра, который жив и находится в онлайне, его нужно убить любым доступным методом, хотя бы нанеся ему hit() с любым запредельным уроном по вкусу.

2) Вылет при удалении оружия или артефакта.

  Решение: такая проблема часто встречается в случае если объект неудачно расположен или находится в руках у непися. Для того чтобы не произошло вылета, убедитесь что объект доступен как серверный перед удалением. Вот так:
local obj = alife():object(i)
if obj then
alife():release(obj, true)
end

Эту конструкцию вообще желательно использовать всегда, когда вы так удаляете объекты.

3) Вылет при удалении аномалии.

  Решение: аномалии - очень капризные при подобном с ними обращении объекты. Они влияют на своё окружение, и если рядом с ними находится непись или монстр, то удаление такой аномалии приведёт к вылету игры. Чтобы этого не произошло, аномалию надо сначала выключить функцией disable_anomaly, и удалять затем ТОЛЬКО тогда, когда она не будет занята влиянием на динамический объект. Для этого нужно получить список мобов на локации, и из их нетпакетов считать идентификаторы действующих на них рестрикторов. Если ваша аномалия будет в этом списке - удалять её нельзя. Дождитесь пока она освободится.

Вылет при открытии закладки "Контакты" в ПДА

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

Вылеты при вызове несуществующих функций из XML

В ХML-файлах, используемых для описания инфопоршенов, для многих инфопоршенов прописаны действия, которые игра вызывает при взятии этого инфопоршена. Вот так примерно:

<info_portion id="barman_document_have">
<action>dialogs.set_actor_prebandit1</action>
<action>bar_spawn.bandits2</action>
<action>bar_spawn.bandits3</action>
<action>bar_spawn.bandit7</action>
<action>bar_spawn.bandit8</action>
</info_portion>

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

Повреждения сейвов на Радаре и других местах в модах, основанных на OGSM

В оригинале ОГСМ и основанных на нём модах часто встречались проблемы с сохранениями на Радаре. Эту проблему долго не удавалось победить, пока наконец благодаря помощи Маландринуса не удалось выявить её первопричину. Как выяснилось, она очень проста - гражданские зомби в моде (монстры) имели в конфиге ту же пропись вида (параметр конфига specie), что и монолитовцы и зомбированные (неписи). И там и тут было проставлено "zombie", и так оно было ещё с оригинала. Как оказалось, так делать категорически нельзя. Дело в том, что у неписей есть такой функционал, как хитовая память - в ней какое-то время хранятся ссылки на атакующие объекты. У монстров тоже есть остатки этого функционала, но он неработоспособен, и использовать его нельзя. В случае же когда монстры и неписи попадают в один вид, в ситуации когда они находятся рядом в бою, хитовая память монстров автоматически получает от неписей того же вида распространяемую внутри вида информацию об атакующих - а хранить её монстрам нельзя. Если после создания такой ситуации сохраниться - сейв будет вызывать вылет при загрузке. То есть проще говоря, если в бою с монолитовцами рядом оказывались гражданские зомби - и игрок сохранял игру - сейв этот не загружался. Чтобы предотвратить эти проблемы, вполне достаточно создать для гражданских зомби свой отдельный вид, добавив его прописи в конфиг game_relations.

Застывания NPC после боя / лечения ранения в модах, использующих дополнительные схемы поведения

Во многих модах, использующих дополнительные схемы поведения часто можно заметить NPC, которые после боя застывают, прицелившись в одну точку. Аналогично часто это встречается с вылеченными от ранения NPC - они встают и замирают намертво, до тех пор пока их не выведет из этого состояния атаковавший враг. Как правило сейв/загрузка этой проблемы не решают. В ходе работы над OGSE 0.6.9.3 мне удалось выяснить причину таких проблем и успешно её устранить. Причина проблемы заключается в том, что NPC управляются не только скриптовыми схемами, но и движком, и для переключения управления между одним и другим используется скрипт state_mgr - менеджер состояний. Его директивы имеют наивысший приоритет. Аналогичный же приоритет себе как правило назначают доп. схемы поведения, используя пропись эвалуатора в xr_motivatior.addCommonPrecondition(action). В итоге при выходе из движковой боевки или при переключении на движковый алайф (это происходит после излечения ранения) доп. схемы поведения вступают с менеджером состояний в конфликт, блокируя смену состояния NPC. Для того чтобы этого не происходило, необходимо во всех схемах поведения, использующих принудительное назначение состояния через функцию state_mgr.set_state сделать блокировку перехвата управления менеджером состояний, добавив соответствующие прописи в биндер. Вот таким образом, как в этом примере - тут я правил биндер схемы лечения/самолечения xrs_medic:

function add_to_binder(object, ini, scheme, section, storage)
local operators = {}
local properties = {}
 
local manager = object:motivation_action_manager()
 
operators["medic"] = actid_medic
operators["self_medic"] = actid_self_medic
 
properties["medic"] = evid_medic
properties["self_medic"] = evid_self_medic
 
local state_mgr_to_idle_combat = xr_actions_id.state_mgr + 1 ---< Это переключение на движковую боевку, но оно тут не использовано
local state_mgr_to_idle_alife = xr_actions_id.state_mgr + 2 ---< Это переключение на движковый алайф
local state_mgr_to_idle_off = xr_actions_id.state_mgr + 3 ---< Это переключение в статичное состояние
 
local zombi=object:character_community()=="zombied" or object:character_community()=="trader" or
object:character_community()=="arena_enemy" or object:name()=="mil_stalker0012" or object:name()=="yantar_ecolog_general"
 
if zombi then
manager:add_evaluator (properties["medic"], property_evaluator_const(false))
manager:add_evaluator (properties["self_medic"], property_evaluator_const(false))
else
manager:add_evaluator (properties["medic"], evaluator_medic("medic", storage))
manager:add_evaluator (properties["self_medic"], evaluator_self_medic("self_medic", storage))
end
 
local action = action_medic (object,"medic", storage)
action:add_precondition(world_property(stalker_ids.property_alive, true))
action:add_precondition(world_property(xr_evaluators_id.sidor_wounded_base, false))
action:add_precondition (world_property(properties["medic"], true))
action:add_effect (world_property(properties["medic"], false))
manager:add_action (operators["medic"], action)
 
local action = action_self_medic (object,"self_medic", storage)
action:add_precondition(world_property(stalker_ids.property_alive, true))
action:add_precondition(world_property(stalker_ids.property_enemy,false))
action:add_precondition(world_property(xr_evaluators_id.sidor_wounded_base, false))
action:add_precondition (world_property(properties["medic"], false))
action:add_precondition (world_property(properties["self_medic"], true))
action:add_effect (world_property(properties["self_medic"], false))
manager:add_action (operators["self_medic"], action)
 
action = manager:action (stalker_ids.action_alife_planner)
action:add_precondition (world_property(properties["medic"], false))
action:add_precondition (world_property(properties["self_medic"], false))
 
action = manager:action(state_mgr_to_idle_alife)
action:add_precondition (world_property(properties["self_medic"], false)) ---< Блокируем попытки переключиться на движковый алайф пока работает самолечение
 
action = manager:action(state_mgr_to_idle_off)
action:add_precondition (world_property(properties["self_medic"], false)) ---< Блокируем попытки переключиться статичное состояние пока работает самолечение
 
end

Вставка этих директив в биндеры всех схем, которые меняют состояния принудительно устраняет конфликты, и NPC больше не виснут при смене схем.

Зависания алайфа из-за ошибок в конфигах торговли

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

novice_outfit				;NO TRADE

Запомните - в секции buy_supplies конфига торговли таких записей быть не должно вообще. Если вы хотите убрать вещь из общего ассортимента - удалите её строку из buy_supplies.

--KamikaZze (OGSE Team) 11:04, 31 марта 2011 (UTC)


Дополнение от cjayho (Dez0wave team):

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

Структура системы сохранов такова, что при поступившей команде на создание сохрана, движок во всех скриптовых объектах вызывает метод save (как-то так, не помню навскидку), причем не одновременно, а по очереди для каждого скрипта. И все переменные скриптовых объектов сохраняются в бинарном файле сохрана тоже естественно по очереди, по одной. Читаются данные скриптами при загрузке сейва точно так же - по очереди и в том же порядке. Сам же метод сохранения данных в бинарный файл не подразумевает каких-либо проверок целостности данных или хотя бы описания, что за переменная сейчас считывается или записывается.

Для примера представим себе сферический скрипт в вакууме, и проследим что происходит в нормальном режиме сохрана/загрузки.

1) скрипту нужно сохранить переменные:

имя_сталкера = вася
группировка = свобода
состояние = накуренный
что_делает = ломится на радар

2) скрипт отправляет переменные соответственно

|вася|свобода|накуренный|ломится на радар|

3) чтение происходит в том же порядке. Все нормально, все работает.

А теперь представим что скрипт по какой-то причине не смог получить данные о том, из какой Вася группировки. Что происходит в этом случае:

1) скрипт сохраняет переменные:

имя_сталкера = вася
состояние = накуренный
что_делает = ломится на радар

2) скрипт отправляет переменные соответственно

|вася|накуренный|ломится на радар|

3) чтение же подразумевает четыре переменные, следовательно получит данные

имя_сталкера = вася
группировка = накуренный
состояние = ломится на радар
что_делает = <а вот тут уже будут считаны данные из скрипта, который сохранялся после данного сбойного>

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

Более того, система сохранений в сталкере настолько несовершенна, что целостность сохранов зависит от имени скрипта и/или имени скриптового объекта. Соответственно если скрипт vasya_svoboda.script переименовать на svoboda_vasya.script очередность вызова загрузки или сохрана будет иной, и следовательно старые сохраны считать будет невозможно ибо в итоге получится все тот же винегрет из перепутанных местами переменных. Именно поэтому многие скриптовые моды ставят в необходимость начала новой игры, и вовсе не из-за измененного all.spawn. Хотя подобную проблему исправить можно, именуя скрипты и скриптовые объекты в формате 001_одинскрипт.script, 002_другойскрипт.script и так далее.

По поводу быстрых сохранов - если вызвать меню игры, и выбрать сохран, вся скриптовая (и не только скриптовая) система находится в замороженном состоянии. Соответственно слепок из этого состояния системы вполне реально записать, даже если само сохранение данных - процесс не мгновенный. Если же нажать быстрый сохран во время игры, вся активность перемалывания данных в скриптах продолжается, а сохранение данных в сохран происходит не мгновенно, и сначала сохрана один скрипт записал что Вася находится в точке А, проходит несколько сохранов скриптов, и в этот момент Вася переместился в точку Б, другой скрипт в тот же сохран уже запишет что Вася находится в точке Б. Что произойдет во время загрузки такого сохрана, когда у нашего бедного Васи появится клон в другой точке игровой печочницы, мы пожалуй умолчим, читатель это поймет сам.


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


Авторы

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

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