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

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

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

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

Продолжение во второй части



Предисловие

Отладив несколько скриптов на предмет вылетов решил написать это небольшое руководство, чтобы помочь начинающим (да и опытным тоже) скриптописателям избежать потенциальных проблем.

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

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

Типы данных и проблемы с nil.

Сначала сделаю маленькое отступление для не знакомых с основами скриптеров, чтобы понятнее было. Итак, в Lua используются следующие логические операторы:

=	оператор присваивания
== сравнение, равно ли значение
~= сравнение, НЕ равно ли значение
< сравнение, меньше ли значение
> сравнение, больше ли значение
<= сравнение, меньше ли значение или равно
>= сравнение, больше ли значение или равно
and логический оператор И
not логический оператор НЕ
or логический оператор ИЛИ


Всё. Теперь поехали дальше… Язык Lua Изначально создавался для работы с большими строчными базами данных, поэтому ВСЕ виды конструкций в языке — это типы данных, то есть по сути либо переменные, либо константы. Это касается так же и функций! Поэтому даже с функциями в Lua можно и нужно обращатся как с переменными. Далее. Касательно, собственно «привычных» переменных. Обычные переменные в Lua получают свой тип данных только в момент присвоения им значения (запомните, это важно). При этом, в Lua есть такой важный и полезный тип данных как nil. nil — Это «пустота», то есть отсутствие какого-либо значения. При этом этот тип используется компилятором скриптов для «сбора мусора», то есть для освобождения занимаемой памяти. Смотрите пример, я объясню подробнее, что это значит:

local reminder_count
local exposure_count = 0

Тут у нас 2 локальные переменные, одна из которых просто создана, а вторая — создана и инициализарована. Если сейчас обратиться к переменной reminder_count, считав её значение, то мы получим в качестве значения — nil, то есть пустоту. Если же мы обратимся к переменной exposure_count — то получим, как и ожидали, 0 — так как мы это значение проинициализировали ЗАРАНЕЕ. При этом не надо путать nil и 0 — так как 0 — это всё-таки какая-то информация, а вот nil — это её полное отсутствие. Так вот, собственно, чем это чревато. Я уже сказал, что nil используется для сбора мусора. Вручную это работает так — когда вам переменная уже не нужна, вы просто пишете:

test_variable = nil

И ваша переменная test_variable сотрётся из памяти, и любые ссылки на неё будут выдавать на выходе nil (типа нет такой переменной, «абонент вне зоны действия сети»). Ну так собственно вот, зачем я это всё рассказываю… если не определить значение переменной сразу, как это было сделано с переменной reminder_count, то любые попытки обратиться к ней внутри скрипта приведут к вылетам. Происходит это следующим образом: допустим, reminder_count — это у нас счётчик напоминаний. То есть мы о чём-то напоминаем игроку текстовыми сообщениями, и помечаем, сколько раз мы это сделали. Определили мы переменную именно так, как в примере выше, то есть не задав ей никакого значения. Значение же её должно присваиваться ниже по коду, после первого оповещения юзера. При этом у нас есть в коде обращение к этой переменной, по которому решается что делать дальше. Выглядит это примерно так:

function check_antirad_supplies()
 
if auto_injection_active then
 
if antirad_check_delay == nil or game.get_game_time():diffSec(antirad_check_delay) > 60 then
 
if not db.actor:object(«antirad») and not (use_scientific_kit and db.actor:object(«medkit_scientic»)) then
 
--обратите внимание сюда
if reminder_count == 0 or reminder_count >= mins_till_next_remind then
--обратите внимание сюда
if use_text then
local news_text = «%c[255,160,160,160]Автоматическая система ввода медицинских
препаратов\\n".."%c[default]Напоминаю: %c[255,230,0,0]
Противорадиационные препараты отстутствуют! %c[default]
Автоматический ввод препаратов невозможен.»
db.actor:give_game_news(news_text, "ui\\ui_iconsTotal",
Frect():set(0,188,83,47), 0, 3000)
end
if use_sounds then
local snd_obj
if use_custom_sounds then
snd_obj = xr_sound.get_safe_sound_object([[HEV\no-anti-rad]])
else
snd_obj = xr_sound.get_safe_sound_object([[device\pda\pda_tip]])
end
if snd_obj then
snd_obj:play_no_feedback(db.actor, sound_object.s2d, 0, vector(), 1.0)
end
end
reminder_count = 1
elseif reminder then
reminder_count = reminder_count + 1
end
 
else
radiation_warning = true
reminder_count = 0
end
antirad_check_delay = game.get_game_time()
end
end
end

Во время игры всё это скорее всего отработает хорошо, но вот при загрузке… тут могут быть проблемы, так как скрипт при запуске может пролететь ту часть, где переменной reminder_count присваивается значение! В итоге при запуске вышеприведённой функции check_antirad_supplies() нас получится что reminder_count не существует — он равен nil, и в итоге проверка

if reminder_count == 0 or reminder_count >= mins_till_next_remind then

выдаст ошибку и приведёт к вылету! Почему? Да потому что НЕЛЬЗЯ сравнивать НИЧТО с вещественным значением. Именно поэтому, когда вы создаёте новую переменную, вы ОБЯЗАНЫ задать ей значение по умолчанию, даже если вы полностью уверены, что она нигде не будет использована до инициализации. Поверьте — в программировании бывает всё, и запросто может так случиться, что ваша переменная будет использована и спровоцирует вылет.

Простой способ обойти такую кучу проверок и лишних инициализаций — использование or-оператора. пример

if (reminder_count or 0) >= mins_till_next_remind then

если переменная равна nil, то выражение вычисляется дальше и получается равным 0, а это уже число и сравнение происходит безболезненно.

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

«Смерть» переменных

Положим, у нас с reminder_count происходят ещё какие-нибудь занимательные вещи, например мы его значение зачем-нибудь присваиваем другой переменной вот так:

antirad_check_delay = reminder_count

Ну и вот, если у нас reminder_count в момент присвоения ещё не имеет никакого значения, то мы получим занимательную штуку — у нас это выражение сработает как

antirad_check_delay = nil

В результате чего переменная antirad_check_delay «сдохнет» и будет деловито убрана «сборщиком мусора» из памяти, что моментально приведёт к вылету, когда в дальнейшем какая-то часть кода обратится к значению antirad_check_delay.

Ошибки деления на 0

Теперь положим что у нас reminder_count используется в каком-нибудь расчёте вот так:

antirad_check_delay = exposure_count/reminder_count

Как мы помним, у нас reminder_count не инициализирована, и как следствие равна nil. В итоге мы получим попытку поделить exposure_count на ноль (в нашем случае на nil, что суть одно и то же в нашем случае) и получим «классический» программистский вылет из-за ошибки деления на ноль.


Ошибки присвоения

Предположим что мы написали скрипт, который помогает выбирать оружие при наличии вблизи врагов. И вот, у нас в скрипте есть такой кусок кода, где NPC вынимает из рюкзака оружие:

function weapon_manager:set_weapon(wpn)
 
 
local enemy = self.npc:best_enemy()
if wpn then
--обратите внимание сюда
self.weapon_id = wpn:id()
--обратите внимание сюда
self:return_items(self.weapon_id)
else
printw(«set_wpn:weapon not exist»)
end
if self.modes.process_mode == "3" and enemy then
for k, v in pairs(self.weapons) do
for i, w in ipairs(v) do
if w.id ≈ self.weapon_id then
local item = level.object_by_id(w.id)
if item and item:parent() and item:parent():id() == self.npc_id then
printw(«set_weapon[%s]:process %s[%s]», self.npc:character_name(), w.id, w.sec)
self:process_item(item)
end
end
end
end
end
if enemy then
self.npc:set_item(object.idle, wpn)
end
self.weapons = nil
 
end

В помеченном выражении self.weapon_id = wpn:id(), и всё вроде бы отлично, так как стоит проверка if wpn then, однако иногда движок умудряется передать объект функциям некорректно, либо сам скриптер может перепутать при вызове игровой объект с объектом а-лайфа, и присвовение self.weapon_id = wpn:id() либо приравняет self.weapon_id к nil либо сразу приведёт к вылету из-за того что соответствующее свойство объекта не было обнаружено в его классе.

Как с этим бороться

Бороться с такими ситуациями очень просто на самом деле. Во-первых: ВСЕГДА ИНИЦИАЛИЗИРУЙТЕ ПЕРЕМЕННЫЕ. То есть вот такое описание переменной:

local reminder_count

НЕДОПУСТИМО! ВЫ ОБЯЗАТЕЛЬНО ДОЛЖНЫ ЗАДАТЬ ПЕРЕМЕННОЙ ЗНАЧЕНИЕ! Если переменная числовая, сделайте это так:

local reminder_count = 0

Если строчная — то сделайте это так:

local reminder_count = ""
(получится вместо nil пустая строка)

Если логическая, то так:

local reminder_count = false

В общем, делайте так, как вам удобнее, но ДЕЛАЙТЕ ОБЯЗАТЕЛЬНО!

И во-вторых: ВСЕГДА ПРОВЕРЯЙТЕ ПЕРЕМЕННУЮ ПЕРЕД ИСПОЛЬЗОВАНИЕМ!

То есть в кусках кода, чреватых вылетами (к ним относятся АБСОЛЮТНО ВСЕ части, где идёт работа с вещами или оружием) обязательно вставляйте проверки переменных на nil следующим образом:

if wpn and wpn:id() then
self.weapon_id = wpn:id()
end

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

Кстати, что самое дурацкое — ошибки, приведённые выше, часто в лог попадают совершенно непохожим на ошибки скриптов образом. Например, ошибка присвоения из примера 3 приводила к вылету с сообщением «Assertion failed», и поймать её я смог только изучив внимательно предыдущие строки лога, где зафиксировались операции с вещами…

Точно так же, если вы используете ЛЮБЫЕ математические операции например деление (но не только деление, всего остального тоже касается), тоже ОБЯЗАТЕЛЬНО поверяйте переменную. Вот так:

if reminder_count and reminder_count ≈ 0 then
antirad_check_delay = exposure_count/reminder_count
end

Таким образом потенциальные вылеты будут аккуратно изолированы, и больше не будут создавать проблем. И ещё одно небольшое дополнение к этой же теме. Итак, вы вставили обходные проверки вот таким образом:

if wpn and wpn:id() then
self.weapon_id = wpn:id()
end

Или таким:

if reminder_count and reminder_count ≈ 0 then
antirad_check_delay = exposure_count/reminder_count
end

И у вас в случае некорректного значения компилятор спокойно пропустил этот код, ничего не сделав ни с self.weapon_id (из первого примера), ни с antirad_check_delay (из второго). Однако вам всё-таки нужно, чтобы с ними что-то происходило, даже если проверяемые переменные неверны. Тогда я бы советовал вам дополнить этот обход отработкой нештатной ситуации. Делается это банально просто:

if wpn and wpn:id()then
self.weapon_id = wpn:id()
elseif wpn == nil then
--вставьте сюда код, что делать если wpn не существует
elseif wpn:id() == nil then
--а сюда, если wpn:id() не существует
--тут скорее всего перепутан объект, и можно попробовать
--обратиться к wpn.id
end

И для второго случая аналогично:

if reminder_count and reminder_count ≈ 0 then
antirad_check_delay = exposure_count/reminder_count
else
--а тут что мы сделаем если reminder_count не существует
--я бы сделал например вот так:
antirad_check_delay = exposure_count/1
--и все дела
end

Более конкретная реализация зависит от того, что именно вы пытаетесь сделать — тут уже вам виднее…

Типичные ошибки при именовании функций, присвоении и инициализации переменных.

Типичная ошибка, приводящая к вылету:

function remove_item(remove_item)
if remove_item~=nil then
alife():release(alife():object(remove_item:id()), true)
return true
end
return false
end

В этом коде агрумент функции совпадает с её именем, и в итоге функция пытается передать функции alife():release вместо указателя на вещь — указатель на саму себя. А так как у функции remove_item нет никакого id, код выпадает с ошибкой:

Arguments     : LUA error: ...\s.t.a.l.k.e.r\gamedata\scripts\test.script:486: attempt to call method 'id' (a nil value)

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

function remove_item(item_to_remove)
if item_to_remove then
alife():release(alife():object(item_to_remove:id()), true)
return true
end
return false
end

Однажды, отлаживая скрипт, натолкнулся на вот такой код:

function hit_callback(npc_id)
if db.storage[npc_id].wounded ≈ nil then
db.storage[npc_id].wounded.wound_manager:hit_callback()
end
end

Казалось бы, всё верно, и значение проверяется перед использованием. Ан нет, скрипт вызывал вылет, причём именно на проверке. Методом тыка было выясено, что nil вызвавший вылет, был не в результате обработки, а был передан фукнции. Это был параметр npc_id.

Пришлось сделать вот так:

function hit_callback(npc_id)
if npc_id and db.storage[npc_id].wounded ≈ nil then
db.storage[npc_id].wounded.wound_manager:hit_callback()
end
end

Введя предварительно проверку аргумента npc_id. Вывод — если ставите проверку, старайтесь проверять не только саму функцию, но и вначале — аргумент, который ей передаётся. Мало ли в каком виде он до вас доедет… :)

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

И ещё один совет — для самых начинающих:

Когда пишете скрипты, не забывайте о соблюдении синтаксиса! То есть внимательно и вдумчиво ставьте в концах функций и оснований ключевые слова end — помните, что как отсутствие нужных end’ов, так и наличие лишних приводят к тому, что границы фукций сбиваются, скрипт не парсится обработчиком и вызывает при попытке своего вызова вылет, примерно вот такой:

Arguments : LUA error: ...g\s.t.a.l.k.e.r\gamedata\scripts\bind_stalker.script:75: attempt to index global 'test_main_new' (a nil value)

Для того, чтобы вам проще было следить за синтаксисом и не допускать лишних end’ов, выстраивайте текст скриптов каскадными лесенками:

Начало_Работы
Начало_Внуренней_Функции
Процедуры_Внутреней_Функции
Конец_Внутренней_Функции
Конец_Работы

..и пользуйтесь редакторами с подсветкой синтаксиса, такими как Notepad++ например. В результате каждый уровень вложенности находится на своём отступе. Вы всегда увидите, какую конструкцию нужно закрыть, а какую — нет. Я же например, делаю ещё проще — я, открывая новую функцию, сразу ставлю сразу в её конце, парой строк ниже, end, и потом уже пишу наполняющий её текст с отступами как в примере выше.

И ещё — касательно жизни переменных: не забывайте, что определённые внутри проверок переменные живут только до выхода из проверки. То есть, например:

if check == 1 then
local test = true
else
local test = false
end

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

local test = false; — инитим переменную и задаём сразу ей значение по умолчанию
 
if check == 1 then
test = true
else
test = false
end

Смертельные циклы по таблицам

Если вы строите цикл по таблице следующим образом:

for i=1, table.getn(table_name) do

То никогда, ни при каких обстоятельствах не используйте внутри этого цикла удаление/добавление строк, т.е:

table_name[i] = nil

или

table.remove(table_name, index)
table.insert(table_name, index)

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

Чтобы избежать подобных ситуаций, стройте циклы по таблице лучше следующим образом:

for k, v in pairs(table_name)

А удаление строк внутри них делайте вот так:

table_name[k] = nil

Циклы же for i=1, table.getn(table_name) do следует использовать только для операций, не изменяющих структуру изменяемой таблицы!



Продолжение во второй части


--KamikaZze (OGSE Team) 08:16, 1 сентября 2009 (UTC)

Авторы

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

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