Сповіщення
Очистити все

Як писати скрипти, щоби у вас не вилітала гра (Частина 2)


Ранг:
Майстер
Роль:
Гість
Записи:
752
Приєднався:
7 місяців тому
 

Як безпечно використовувати коллбеки та таймерні події

У скриптовому движку є зручний спосіб реакції на події - використання колбеків - процедурних викликів, прив'язаних до певних подій у житті ігрового об'єкта - отримання пошкоджень, спавну, смерті і т.д. і т.п. Це 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() при смерті непису іноді викликало виліт Line 748 з кодом

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(причина)

Необхідні для стабілізації гри редагування в модулях

Ця правка запобігає запису в пстор якщо не спрацював аборт:

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 "спроба індексувати нульове значення"

Те, що відбувається у функції 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), що й монолітівці та зомбовані (неписи). І там і тут було проставлено "зомбі", і так воно було ще з оригіналу. Як виявилось, так робити категорично не можна. Справа в тому, що непис має такий функціонал, як хітова пам'ять - в ній якийсь час зберігаються посилання на атакуючі об'єкти. У монстрів теж є залишки цього функціоналу, але він непрацездатний і використовувати його не можна. У випадку коли монстри і неписи потрапляють в один вид, в ситуації коли вони знаходяться поруч в бою, хітова пам'ять монстрів автоматично отримує від неписів того ж виду інформацію про атакуючих, що поширюється всередині виду - а зберігати її монстрам не можна. Якщо після створення такої ситуації збережеться - сейв викликатиме виліт під час завантаження. Тобто простіше кажучи, якщо в бою з монолитовцями поряд опинялися цивільні зомбі – і гравець зберігав гру – цей сейв не завантажувався. Щоб запобігти цим проблемам, цілком достатньо створити для цивільних зомбі свій окремий вигляд, додавши його пропису в конфіг 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):

Взагалі тема псування збережень у сталкері досить поширена: я не раз в описі багатьох модів читав рекомендації про те, що краще не використовувати швидке збереження, або щоб грати в мод потрібно обов'язково починати нову гру. Взагалі, щоб боротися з псуванням збережень, необхідно для початку знати, чому вони б'ються і що при цьому відбувається.

Структура системи збережень така, що при команді, що надійшла на створення збереження, двигун у всіх скриптових об'єктах викликає метод save (якось так, не пам'ятаю навскидку), причому не одночасно, а по черзі для кожного скрипта. І всі змінні скриптових об'єктів зберігаються в бінарному файлі - збереження теж природно по черзі, по одній. Читаються дані скриптами при завантаженні сейва так само - по черзі і в тому ж порядку. Сам метод збереження даних у бінарний файл не передбачає будь-яких перевірок цілісності даних або хоча б опису, що за змінна зараз зчитується або записується.

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

1) скрипту потрібно зберегти змінні:

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

2) скрипт відправляє змінні відповідно

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

3) читання відбувається у тому порядку. Все гаразд, все працює.

А тепер уявімо що скрипт з якоїсь причини не зміг отримати дані про те, з якого Вася угруповання. Що відбувається у цьому випадку:

1) скрипт зберігає змінні:

имя_сталкера = вася
стан = накурений
що_робить = ломиться на радар

2) скрипт відправляє змінні відповідно

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

3) читання ж має на увазі чотири змінні, отже отримає дані

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

І слід зауважити, що всі скрипти, які завантажуватимуть змінні після даного збійного скрипту, отримають такий самий ахтунг зі зрушеними даними. Якби один скрипт отримав марення з збереження, то ще пес би з ним, можна постаратися його написати так, щоб він переварив неправильні дані, замінивши якісь значення за промовчанням. Але коли таке марення отримує половина скриптової системи, виліт неминучий.

Більше того, система збереження в сталкері настільки недосконала, що цілісність збережень залежить від імені скрипта та/або імені скриптового об'єкта. Відповідно якщо скрипт vasya_svoboda.script перейменувати на svoboda_vasya.script черговість виклику завантаження або збереження буде іншою, і отже старі збереження вважати буде неможливо, бо в результаті вийде той самий вінегрет з переплутаних місцями змінних. Саме тому багато скриптових мод ставлять у необхідність початку нової гри, і зовсім не через зміненого all.spawn. Хоча подібну проблему можна виправити, називаючи скрипти та скриптові об'єкти у форматі 001_одінскрипт.script, 002_іншийскрипт.script і так далі.

Щодо швидких збережень - якщо викликати меню гри, і вибрати збереження, вся скриптова (і не тільки скриптова) система знаходиться в замороженому стані. Відповідно зліпок із цього стану системи цілком реально записати, навіть якщо саме збереження даних – процес не миттєвий. Якщо ж натиснути швидке збереження під час гри, вся активність перемелювання даних у скриптах триває, а збереження даних у збереження відбувається не миттєво, і спочатку збереження один скрипт записав, що Вася знаходиться в точці А, проходить кілька збережень скриптів, і в цей момент Вася перемістився в точку Б, інший скрипт у той же збережений вже запише що Вася знаходиться в точці Б. Що станеться під час завантаження такого збереження, коли у нашого бідного Васі з'явиться клон в іншій точці ігрової печі, ми мабуть замовчимо, читач це зрозуміє сам.


   
Цитата
Ранг:
Майстер
Роль:
Гість
Записи:
752
Приєднався:
7 місяців тому

   
ВідповіcтиЦитата