Это пятый из серии гайдов по нововведениям NVSE4, освежившими моддинг сцену Нью Вегаса: UDFы, строковые переменные и массивы. Моддеры Обливиона, незнакомые с ОБСЕ 16+ тоже получат пользу от этих статей.
В данной части речь пойдет об Обработчиках Событий (Event Handlers) и Пользовательских Событиях (User-Defined Events).
Почему?
Как модер, вы точно знаете, что кое-какие штуки вполне возможно реализовать, но лучше этого не делать из-за конфликтов и других проблем.
Вот как бы вы реализовали:
- Изменение переменной «личная гигиена» каждый раз, когда активируется раковина или туалет?
- Размытие экрана при испитии алкоголя?
- Звук растегнувшейся молнии при снятии одежды?
- Джингл основной темы «Хороший, плохой, злой» при экипировке револьвера?
- Срабатывание мины-ловушки с определенным шансом при открытии запертой двери?
- Отслеживание изменений в пакетах ИИ у актора, чтобы заставить следовать определенному пакету?
- Маркировку к удалению всех выкинутых игроком предметов в определенной ячейке во избежание разбухания сохранения (savegame bloat), ну или сымитировать, что их кто-то забрал, пока игрока не было рядом?
- Форсировать кастомный диалог между неписями при уроне одним второго определенным оружием или типом оружия?
Пока что на ум приходит только привязка обьектного скрипта к обьектам с одно-фреймным блоком OnActivate, On(Actor)Equip, OnOpen, OnPackageChange, OnDrop или OnHit(With), срабатывающим при соответствующем событии.
Но проблема-то в том, что нельзя просто взять и присоединить эти скрипты к оригинальным обьектам без конфликтов с другими модами, меняющими те же обьекты. Тогда придется создавать уйму отдельных патчей для их разрешения. При этом New Vegas уже и так имеет настолько ограниченный предел модов, какой не снился ни одной игре Бесезды, и если большая часть порядка загрузки – в одних только патчах, то это просто трата драгоценных слотов esp. Не говоря уже о том, что все эти патчи надо создать и потом сотни раз повторять людям, пишущих о багах, как и в каком порядке эти патчи устанавливать.
Другая проблема – все эти скрипты надо прикреплять к огромному количеству обьектов, чтобы все работало, как задумано. То есть, ваш мод на личную гигиену затронет вообще все туалеты и раковины, или мод на звук револьвера – все револьверы, если вы хотите, чтобы при попадании персонаж кричал «Яппи-ка-эй, чертила!». А что насчет добавленных модами вещей того же типа? Предпочтете создавать патчи для каждого добавленного револьвера? Я пас.
Во многих случаях скрипт будет срабатывать даже когда не нужно: скрипт удаления выброшенного барахла должен срабаывать не на всех акторах, ловушки должны срабаывать не на всех референсах дверей. А единственный способ проверить условие срабатывания – только через уже работающий скрипт, что уже создает конфликты простым фактом прикрипления скрипта к базовой форме обьекта.
Это душнилово уже заставляет отбросить такие задумки. А что если…
- мы могли бы отслеживать активацию, открытие, сброс предметов, удары, потребленные предметы и прочие действия без привязки скриптов к базовым формам обьектов?
- мы могли бы фильтровать отслеживание через параметры, в духе кто активировал, что было открыто или кто открывает дверь и тому подобное?
Вот было бы круто, да? Так вот именно это и делают обработчики.
2. Стандартные События, Их Обработка, Параметры
Обработчики Событий – UDF, зарегистрированные с помощью NVSE с целью вызывать их в момент совершения события. Эти обработчики работают всю игровую сессию, даже если загрузить другое сохранение, они все равно работают до перезапуска игры. Если я хочу отслеживать в какой момент что-либо активировано или выброшено, я регистрирую обработчик событий с помощью команды SetEventHandler каждый раз, когда игра загружена:
SetEventHandler "OnActivate" myActivateEventHandler
SetEventHandler "OnDrop" myDropEventHandler
И теперь, каждый раз, когда нечто вот-вот будет (is about to be) активировано или выброшено, NVSE вызовет (call) обработчик событий прежде, чем активировать или выбросить обьект. Помните: некоторые функции, работающие в обычных блоках, не работают в их эквивалентах из обработчиков событий, поскольку то, что является условием срабатывания скрипта еще не сработало. Использование GetActionRef на блоке OnActivate ничего не вернет. Но, к счастью, у нас есть параметры обработчиков для таких целей.
В случае с событием OnDrop, NVSE вернет первым параметром бросавшего, а вторым – брошенный предмет. Так что наш myDropEventHandler нуждается в том, чтобы эти параметры зарегистрировать в следующем порядке:
Scn myDropEventHandler
ref rDropper
ref rDropped
Begin Function {rDropper, rDropped}
…
End
Если эти переменные не обьявлены прежде, чем быть параметрами, то NVSE выдаст ошибку.
В случае события OnActivate нужно помнить, что скриптовый блок OnActivate и команда activate сбивают с толку из-за неподходящей терминологии. Переключатели света, раковины и триггеры находятся в категории «Активаторы» в ГЕККе, хотя они должны быть активированы кем-либо. Хотя кажется, будто непись активирует референс контейнера, на деле, если форсировать скриптом активацию контейнера, мы увидим, что именно референс контейнера и будет активатором:
rContainerRef.activate rActorRef
Событие OnActivate сперва ожидает активатора, который должен быть активирован, и только потом вещь или актора, который будет активировать. Последнего мы и возвращаем командой GetActionRef в обычном скриптовом блоке OnActivate.
Представим это так:
Scn myActivateEventHandler
Ref rActivator
ref rActionRef
Begin Function {rActivator, rActionRef}
…
End
Большая часть стандартных обработчиков событий, а это те, чьи скриптовые блоки соответствуют обычным блокам, имеют по обыкновению такой же набор параметров. Они всегда присутствуют, вам нужно лишь их получать.

(параметры необязательно называть так, как я, имена ничего не значат и нужны только вам для различения).
3. Стандартные Параметры как Фильтры
Параметры обработчиков могут использоваться как фильтры, когда добавлены как параметры для функции SetEventHandler при регистрации обработчика. С ними обработчик будет вызываться только при необходимости, так что фильтры надо применять как можно чаще. Также убедитесь, что фильтры отфильтровали валидную форму (valid form), иначе система баганет, и фильтрация просто отвалится, а вы останетесь с обработчиком, срабатывающим без фильтра.
Если вы выбрали использовать один фильтр или вообще никакого, вам нужно определить какой именно параметр фильтрации, первый или второй, вы фильтруете, и затем поменить его двойными двоеточиями (как делаем это командой ar_Map):
SetEventHandler "OnDrop" myEventHandler "first"::playerref "second"::myCoolgunForm
В OBSE «first» назывался «ref», а «second» назывался «object». Это возможно и в NVSE:
SetEventHandler "OnDrop" myEventHandler "ref"::playerref "object"::myCoolgunForm
Но, в отличие от OBSE, NVSE позволяет любому параметру отсылать к референсу, базовой форме и даже к формлисту (!), содержащему референсы и\или базовые формы и\или другие формлисты, когда это возможно. Так что использование слов «first» и «second» просто менее запутывающее.
Так что используя обработчики и их фильтры, вам будет намного легче реализовать всё то, что я предложил во введении:
1.SetEventHandler "OnActivate" myEventHandler "first"::formlistofallsinkforms "second"::playerref
2.
SetEventHandler "OnMagicEffectHit" myEventhandler "first"::playerref "second"::ChemIncCHAlcohol
3.
SetEventHandler "OnActorUnequip" myEventhandler ; check what’s unequipped with the handler’s parameter
4.
SetEventHandler "OnActorEquip" myEventhandler ; check what’s equipped with the handler’s parameter
5.
SetEventHandler "OnOpen" myEventHandler "first"::formlistofdoorrefs "second"::playerref
6.
SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage
7.
SetEventHandler "OnDrop" myEventHandler "first"::playerref ; check the cell and the dropped item in the handler
8.
SetEventHandler "OnHitWith" myEventHandler "second"::myCoolGunorListofGuns
Для отслеживания пакетов также вполне возможно применить блоки старта, изменения и завершения пакетов в одном и том же обработчике:
SetEventHandler "OnPackageStart" myEventHandler "first"::rActor "second"::rPackage
SetEventHandler "OnPackageChange" myEventHandler "first"::rActor "second"::rPackage
SetEventHandler "OnPackageDone" myEventHandler "first"::rActor "second"::rPackage
В таком случае можно использовать команду GetCurrentEventName чтобы выяснить что за событие вызвано вашим обработчиком:
Scn myEventHandler
Ref rActor
ref rPackage
string_var sv_event
Begin Function {rActor, rPackage}
Let sv_event := GetCurrentEventName
if eval sv_event == "OnPackageStart"
; do something
elseif eval sv_event == "OnPackageChange"
; do something else
endif
End
4. Удаление Обработчиков Событий
Вполне возможно, что у вас есть настройка, позволяющая больше не применять обработчик к событию. Кроме того, обработчики событий все еще действуют при загрузке другого сохранения без перезахода в игру, поэтому вам захочется удалить обработчик ради безопасности чтобы загрузить сохранение, где условия срабатывания обработчика еще не наступили.
Чтобы удалить обработчик используем RemoveEventHandler, с тем же самым синтаксисом, что и у команды создания обработчика:
RemoveEventHandler "OnActivate" myEventHandler "first"::rActivator "second"::rActionRef
Это остановит вызов обработчика событий при срабатывании фильтров. Если это единственные фильтры, то можно просто их пропустить:
RemoveEventHandler "OnActivate" myEventHandler
Так что можно останавливать вызов обработчика при одних фильтрах и оставлять при других.
5. События NVSE
Помимо событий, связанных с оригинальными типами блоков, вы также можете регистрировать обработчики, которые будут вызываться при возникновении определенных метаигровых событий. Это особенно полезно для модов, которые пишут какие-то файлы совместного сохранения и т. д. (После тестирования выяснилось, что это касается только плагинов NVSE.)

6. Пользовательские События (UDE)
Еще круче то, что можно вводить кастомные события, которые другие люди могут регистрировать тем же путем, что и стандартные, и получать информацию из параметров.
Это привычнее для моддеров, работающих с глобальным функционалом, желающим отслеживать нечто произошедшее и одномоментное, то, что классифицируется как «событие», чтобы другие моддеры тоже могли это отслеживать (нечто в духе Conscribe – прим. Переводчика).
Допустим, вы создали полный ребаланс скрытности и хотите показать, когда игрок обнаружен или скрыт независимо от того, согласуются ли с этим оригинальные функции игры.
Возможно, вашему моду необходимо пройти ряд инициализаций, прежде чем он заработает, и вы хотите, чтобы зависимые моды знали, что ваш мод действует, когда инициализация завершена.
Возможно, вы хотите, чтобы ваш сортировщик инвентаря не только уведомлял других о том, что сортировка прошла, но и отправлял информацию о том, где все оказалось.
Возможно, вы даже работали над кое-каким модом, который отслеживает кое-какие вещи, происходящие до тех пор, пока один из неписей, участвующих в этом, не столкнется с каким-то кульминационным событием, и вы хотите, чтобы другие кое-какие моды знали об этом, чтобы, например, воспроизводить соответствующие звуки и тому подобное.
В таких случаях нужно отправить событие (dispatch an event) с помощью функции DispatchEvent, за которой должно следовать как минимум имя события, опционально строковая карта с данными и опционально еще одна строка, переписываающая изначальное имя отправителя (overriding the default sender name):
DispatchEvent "eventName":string args:stringmap sender:string
DispatchEvent "SomeEventName"
Array_var ar_mystringmap
; populate ar_mystringmap
DispatchEvent "SomeEventName", ar_mystringmap
DispatchEvent "SomeEventName", ar_mystringmap, "somestring"
Моды, желающие зарегистрировать обработчик через него, смогут это сделать вот с какими ограничениями:
- поскольку у диспатча нет стандартных параметров, фильтрами нельзя воспользоваться при создании обработчики для кастомных событий
- единственный параметр обработчика UDF, который должне быть, это переменная массива, которая хранит строковую карту, которую всегда получает, даже если ничего не было отправлено (even if none was dispatched):
SetEventHandler "SomeEventName" myEventHandler
Scn MyEventHandler
Array_var args
Begin Function {args}
…
End
Если строковая карта не была отправлена (), то в диспатче будут только два элемента:
[eventName] == "SomeEventName"
[eventSender] == "SomeSenderString" (обычно == "DispatchingMod.esp")
Диспатчер может определить «SomeSenderString», добавив третий необязательный параметр в строку DispatchEvent. Если параметр пуст, то это будет имя мода диспатча. Если была отправлена строковая карта, к ней добавляются стандартные элементы.
Ну и всё. Вперед модифицировать!
ИЗ КОММЕНТАРИЕВ
Вопрос:
- Обработчик событий OnDrop, он вызывается в menumode или при закрытии Пип-бой и переходе в gamemode?
- Могу ли я получить полный пример того, как написать скрипт для обнаружения заклинаний? Если говорить точнее, мне интересен эффект пьянства, я думал о нем давно, но так и не реализовал как раз из-за проблем с совместимостью.
- Есть ли также AddEventHandler?
Ответ:
1) Хороший вопрос; я не уверен. Но учитывая, что события «обрабатываются» до того, как они действительно произойдут, и что вещь, которую вы выбрасываете, уже исчезла из инвентаря, пока вы все еще находитесь в режиме меню, я думаю, что это работает в menumode.
Честно говоря, я не тестировал события так же тщательно, как строки и массивы, и даже они никогда не тестировались мной полностью; я всегда фокусировался на изучении того, как управлять этими вещами, а не на том, как работает движок, если вы понимаете, о чем я.
2) Ну вот пример:
SetEventHandler “OnMagicEffectHit” myEventhandler "first"::playerref "second"::ChemIncCHAlcohol
«Магические эффекты» — это обливионская версия «базовых эффектов» из Fallout. ChemIncCHalcohol — это базовый эффект, прикрепленный к выпивке. Однако, возможно, что у некоторых добавленных/измененных модами выпивок его нет. Вместо этого можно заполнить формлист базовыми эффектами. Возможно, событие «OnActorEquip» тоже сработает на еде, хотя некоторые моды (например, Sofo) накладывают эффект выпивки на людей как заклинания, а не экипируют их, поэтому эффект магии должен лучше их улавливать.
Так что обработчик нужен для уведомлений, ну или для применения скрипта или отслеживания тех же изменений визуала (image space modifier) скриптом токена или заклинанием.
3) Не уверен, зачем он нужен. Есть стандартные события, к ним добавляются обработчики командой SetEventHandler. Есть кастомные, созданные пользователями, которые творятся точно так же.
Вопрос:
3 - Я имел в виду что-то вроде противоположности RemoveEventHandler. Можно остановить обработчик событий, но нельзя запустить его снова, верно?
2 - Дело в том, что я понимаю сам обработчик событий, но не то, как практически заскриптовать все это. Я понял, что это своего рода UDF с принудительными параметрами, но тогда я не понимаю остального. Мне всегда нужно регистрировать его внутри GameMode и в условиях GameRestarted / GameLoaded?
И еще: вы сказали, что UDF всегда будут выполняться (ценой статтера для остального...), потому я начал использовать UDF для своих небольших скриптов, для которых важна надежность. Есть ли такая же надежность для обработчиков? Они железно срабатывают или нет? Мне очень интересен пример, который вы привели с пакетами, поскольку мы все знаем, что они иногда немного того... может быть, это и есть окончательное решение вопроса, чтобы они, наконец, не ломались.
Ответ:
Помимо строки кода SetEventHandler, я бы написал обработчик, который произносит заклинание при его срабатывании:
scn myHandler
ref rTarget
ref rBaseEffect
Begin Function {rTarget, rBaseEffect}
rTarget.cios mySpell
End
А в заклинании я бы начал модификатор изображения с "imod" в блоке scripteffectstart и продолжал бы проверять, находится ли игрок все еще под влиянием в блоке scripteffectupdate, чтобы можно было удалить его с помощью rimod в какой-то момент.
Что касается UDF и статтеров, это не обязательно вина UDF, это скорее вопрос комбинированной нагрузки скрипта, вызывающего UDF, и самого UDF, в сочетании с другими скриптами, которые, возможно, делают то же самое примерно в тот же момент. Другие типы скриптов могут просто останавливать скрипт, если они не могут завершиться в течение фрейма, но UDF очень-очень стремится к завершению своих функций как можно сокрее.
Я понятия не имею, ведут ли себя обработчики событий как-то по-другому; я предполагаю, что нет, но jaam шарит лучше меня. Я предлагаю использовать обработчики в первую очередь как метод уведомления о происходящих событиях и о том, с кем это происходит и т. д., а затем использовать другие скрипты, чтобы что-то с этим сделать.
Вопрос:
2 – Простите за духоту, но я хочу быть уверен, что понял:
myHandler - это простой скрипт объекта, как UDF, но затем мы помещаем свою строку SetEventHandler в игровой режим в каком-то состоянии GameRestarted / GameLoaded? Например, когда делаем свою первую инициализацию?
(однако, просто интересно... для совместимости с другими модами на выпивку, может быть, такой обработчик событий можно было бы больше применять к эффектам отмены алкоголя, может быть, у некоторых пользовательских алкоголиков есть другие эффекты, но у всех есть абстиненция)
Ответ:
А, да, нельзя зарегистрировать свой обработчик событий с помощью обработчика событий. Потому что этот скрипт никогда не будет выполнен. Поэтому мы региструем его в другом скрипте — у большинства из нас уже есть первоначальная инициализация и проверка GGL/GGR где-то в наших модах, так что это закономерно.
Комментарии