Это перевод статьи мододела GOOGLEPOX, поделившегося прометеевым огнем знания о том, как создавать плагины, опирающиеся на динамическую библиотеку расширителя скриптов, более известную как OBSE.

Предисловие

После бесчисленных чашек кофе, поздних ночей, глухих ругательств и часов за IDA* я достиг такого статуса, что люди начали задавать мне вопросы "как мне сделать плагин для OBSE?" и "как поживает система ESL для Oblivion?". Будьте спокойны, всё будет. Но сегодня мы создадим свой собственный плагин для OBSE.

* IDA - это программа для реверс-инжиниринга игровых движков и программ. Она сканирует все действия, которые выполняет процессор, какие посылает ему программа или движок, и переводит их на язык Ассемблера. После программист сам должен перевести эти команды с ассемблера на С++. Таким образом был "собран обратно" движок для Морровинда только уже как опен-сорсный OpenMW. И к нему не может быть никаких претензий, поскольку код там не выдран из оригинальной игры, а фактически написан заново, ведь можно заставить движок выполнять одни и те же действия, только написав команды по-разному. - Примечание Переводчика.

Обзор

Это гайд не будет сразу обо всем. Многое придется узнавать самому и лучше всего для этого глубоко погрузиться в то, как работает Oblivion. Я очень рекомендую вам иметь конкретную цель или конкретный проект, когда вы будете всё это учить, поскольку увидеть, как ваша задумка воплотилась в жизнь, это непередаваемо. Когда я начинал делать свой первый плагин OBSE я практически ничего не знал о С++ (портируя Base Object Swapper для New Vegas от powerofthree). Лучший способ обучения - тотальное погружение.

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

Начинаем

1. Клонируйте GitHub репозиторий xOBSE. Рекомендую установить GitHub Desktop, чтобы легче было работать с репозиториями и обновлять все репозитории вовремя. Это еще и отличное средство, когда вы что-то перепутали или удалили и вам необходимо восстановить файлы с бекапа. У Гитхаба есть на этот счет отличные гайды.

2. Скачайте Visual Studio и откройте файл obse.sln внутри xOBSE/obse.

3. Взгляните на проект obse_plugin_example. Это база для создания нового плагина.

4. Не устрашитесь.

Пример Плагина

Погнали! Открываем и смотрим main.cpp. Глубокий вдох. Здесь люди начинают теряться из-за обширного количества самых разных вещей, которые тут видят. Не волнуйтесь, я просматривал этот файл бесчисленное количество раз и столько же раз сдавался пока наконец не получилось.

Хорошие новости: main.cpp нам не нужен для нашего плагина и существует только как пример того, как работают разные возможности OBSE. Я разобью этот файл на несколько секций.

  • #include - это буквально import из Python или Java (ну почти, не нужно к этому придираться)
  • Следующий отрывок - глобальные переменные для OBSE

IDebugLog                gLog("obse_plugin_example.log");

PluginHandle                                g_pluginHandle = kPluginHandle_Invalid
;
OBSESerializationInterface        * g_serialization = NULL
;
OBSEArrayVarInterface                * g_arrayIntfc = NULL
;
OBSEScriptInterface                        * g_scriptIntfc = NULL
;

  • Следующие несколько блоков это примеры  разных API для OBSE, такие как массивы (Arrays), команды OBScript, сериализация для сохранения данных и система сообщений Messaging API (мы к ней еще вернемся)

Если брать ХОТЬ ЧТО-НИБУДЬ из этого main.cpp то это следующее:

OBSEPlugin_Query  полезна для информации по версиям, совместимостям и тому подобному, действуя перед тем, как загрузится плагин.

OBSEPlugin_Load самая важная, поскольку в ней мы определим (define) что сделает OBSE, когда загрузит ваш плагин.

Ваш первый плагин

Удаляйте все из main.cpp кроме секций load, query и интерфейса сообщений (messaging interface). Я создал голую версию main.cpp, которую ищите здесь. Ну как теперь всё это выглядит? Намного яснее теперь, не так ли?

Теперь у нас есть OBSEPlugin_Load, который пока всего лишь инициализирует MessagingInterface, OBSEPlugin_Query, проверяющий и версию OBSE и версию Oblivion, чтобы убедиться, что они не устарели, и сам MessagingInterface.

Взглянем на этот самый интерфейс сообщений - начальная точка для создания своего кода. Этот интерфейс всего лишь смотрит, когда то или иное событие (event) случается (сохранение, загрузка, выход и проч.) и отправляет сообщение вашему плагину, давая ему знать, что это событие только что произошло. Так в моем main.cpp есть проверка для типа сообщений (message type) и для конкретного события kMessage_LoadGame. Ну пока это ничего нам не дает. Пока.

Добавляем простую возможность. Напишем "Hello, Wordl!" в консоли как только загрузили сохранение.

void MessageHandler(OBSEMessagingInterface::Message* msg)
{
   
switch (msg->type)
   {
   
case OBSEMessagingInterface::kMessage_LoadGame:
       Console_Print(
"Hello, World!");
       
break;
   
default:
       
break;
   }
}

Вот настолько всё просто.

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

Это уже сложнее, поскольку теперь нужно добавить проверку, что игрок инициализирован, например, такую:

void AddItemOnLoad()
  {
      
if (!g_thePlayer || !(*g_thePlayer))
          
return;

      // Add item here…
  }

А для функции AddItem напишем следующее:

AddItem(TESForm* item, ExtraDataList* xDataList, UInt32 count)

AddItem включает в себя 3 аргумента, как вы видите. Предмет, который хотим добавить, дополнительный список информации (extra data list) и количество предмета, которое вы хотите добавить. Можно просто прописать nullptr вторым аргументом, посколько он необязателен.

Чтобы указатель обьекта (object pointer), TESForm, нашел нужный предмет, мы используем:

 LookupFormByID(formID)

Большая часть обьектов Обливиона имеет formID, 16-ричное число, выступающее уникальным идентификатором обьекта. Все formID есть на UESP.

Мы знаем, что formID одной монеты начинается с префикса 0xF, где 0х - обозначает 16-ричное число. 

Используем это, чтобы добраться до этой монеты: 

void AddItemOnLoad()
{
   
if (!g_thePlayer || !(*g_thePlayer))
       
return;

   TESForm* gold = LookupFormByID(
0xF);

   (*g_thePlayer)->AddItem(gold, nullptr, 
1000);
}

Теперь эту функцию выше мы вывозем в интерфейсе сообщений во время события kMessage_LoadGame

void MessageHandler(OBSEMessagingInterface::Message* msg)
{
   
switch (msg->type)
   {
   
case OBSEMessagingInterface::kMessage_LoadGame:
       Console_Print(
"Adding 1000 gold to the player");
       AddItemOnLoad()
       
break;
   
default:
       
break;
   }
}

Теперь скомпилируйте код (Build this) и проверяйте!

Поздравляю! Вы создали ваш первый плагин OBSE! Гордитесь собой, поскольку так далеко большая частть модеров не заходит.

Хуки

События Интерфейса Сообщений серьезно ограничены, поскольку код можно пустить только для пред-установленных событий (pre-defined events), подобных сохранению, загрузке и выходу из игры. Если вам нужно болше контроля, пристегните ремни, сейчас будет ликбез по адресам памяти и указателям (memory addresses and pointers).

  • Oblivion.exe (и любая программа) делится на мелкие части. Каждая часть имеет адрес, чтобы компьютер знал, какая часть кода сейчас работает и как прыгнуть к другим частям кода.
  • Есть программа IDA (Interactive Disassembler), позволяющая смотреть эти части и видеть, что они делают. Я СИЛЬНО рекомендую использовать её, но она довольно-таки дорогая, так что люди находят....креативные способы, чтобы разобраться в вопросе.
  • Собственно, хуки позволяют вам легчайше вставлять код в любой из этих адресов. Некоторые адреса сложнее пропатчить, чем другие.
  • Например, я знаю, что адрес памяти 0xA46CB0  (опять же, 0x  - это всего лишь префикс для 16-ричной системы счета или 16 чисел. Все формиды Обливиона записаны в этой системе, как и большая часть компьютерных материй) Но в адресе 0xA71160, как я узнал из просмотра кода в IDA, игра вызывает TESObjectREFR::LinkForm, то есть спрашивает, где референс обьекта инициализируется.

Nota Bene: хотя и формиды, и адреса записываются в формате 16-ричной системы счета, formID обозначают информацию (предметы, НИПов, оружие и проч.), а адреса памяти обозначают код. Они НЕ взаимозаменяемы.

Так, в моем коде для Base Object Swapper (исходный код лежит здесь) я написал:

static inline std::uint32_t originalAddressREFR;
static void Install()
{
   originalAddressREFR = DetourVtable(
0xA46CB0, reinterpret_cast<UInt32>(LinkFormHookREFR));
   _MESSAGE(
"Installed TESObjectREFR vtable hook");
}

Этот кусок кода просто перенаправляет (reroutes) код, когда тот отправляется к этому адресу, и вызывает мою функцию LinkFormHookREFRinstead вместо функции TESObjectREFR::LinkForm.

После чего моя функция LinkFormHookREFRinstead выглядит так:

static void __fastcall LinkFormHookREFR(TESObjectREFR* a_ref, void* edx)
       {
           
if (const auto base = a_ref->baseForm) {
               Manager::GetSingleton()->LoadFormsOnce();
               
const auto& [swapBase, transformData] = Manager::GetSingleton()->GetSwapData(a_ref, base);
               
if (swapBase && swapBase != base) {
                   a_ref->baseForm = swapBase;
               }
               
if (transformData != std::nullopt) {
                   transformData->SetTransform(a_ref);
               }
           }
           ThisStdCall(originalAddressREFR, a_ref);
       }

И вот она руководит логикой мода Base Object Swapper, затем включает в действие ОРИГИНАЛЬНЫЙ код, который должен был там включаться, то есть originalAddressREFR со строкой ThisStdCall, чтобы игра не вылетела, а код просто немножечко отклоняется (does a little detour through) от моей функции.

Вот эта логика обходного пути (same basic detour logic) может применяться в большинстве мест в коде, а другие просто требуют чуть больше работы. Можно назвать это кастомными событиями (custom events) для Интерфейса Сообщений, только вы их можете применять вообще где угодно!

Очевидно, что рассказывать еще можно МНОГО чего, а кривая обучения находится почти под прямым углом. Однако я не могу описать, насколько ценно выучить эту тему. Я рекомендую присоединиться к нашему сообществу в дискорде New Oblivion Modding Community для поддержки. Я почти всегда там активен (под юзернеймом googlepox) и всегда открыт для вопросов и поддержки.

Удачи!
Ваш GOOGLEPOX

Материал подготовлен ArtemSH специально для TGM — Tesall Game Magazine.
Переводчик: ArtSH
Автор: GOOGLEPOX
Источник: Перейти
1

Комментарии

Авторизуйтесь, чтобы оставить новый комментарий. Или зарегистрируйтесь.