Гайд Создание ASI-плагина с нуля [1]

Делать мне было нечего, а работать не хотелось, поэтому вы видите этот гайд

  1. Создание ASI-плагина с нуля
  2. Хуки – что это такое и как с ними работать
  3. Безопасная инициализация и работа с SAMP
  4. Работа с рендером и Directx9
  5. Обработка событий окна + ImGui
В этом гайде мы создадим свой ASI-плагин с нуля, который выведет сообщение на экран.

Введение:
Для начала скажу, что вам понадобится Visual Studio(Так проще всего), и пакеты к ней, а именно - Разработка классических приложений на C++ и Разработка приложений на универсальной платформы Windows.
Все действия производились на Visual Studio 2019, в других версиях интерфейс может отличаться

Создание проекта:
И так, начнем с создания проекта. Жмем кнопку создать проект, и ищем "Библиотека динамической компоновки (DLL)" (Дело в том, что ASI это и есть DLL файл, только с измененным расширением).
Создаем проект. Я назвал его ASIPlugin.

После создания проекта мы видим перед собой окно редактора с подготовленным шаблоном. Шаблон содержит в себе подключение pch.h и функции DllMain.

Настройка проекта:
Начнем с настройки проекта.
В панели меню сверху жмем Проект, и выпадающем меню выбираем пункт Свойства: $ProjectName
Сверху, в выпадающем меню в открывшемся диалоге выбираем Конфигурация -> Все конфигурации.
После этого я обычно отключаю предварительно скомпилированные заголовки(pch.h), но вы можете их оставить(поэкспериментируйте сами)
Включить/Выключить можно в подменю C/C++ -> Предварительно откомпилированные заголовки -> Предварительно откомпилированный заголовок

После этого переходим в Дополнительно -> Расширение целевого файла, меняем .dll на .asi(чтобы подгружалось ASI Лоадером)
(ОПЦИОНАЛЬНО) После этого переходим в Общие -> Выходной каталог, здесь указываем путь до своей GTA

Настройка проекта окончена, переходим к написанию кода

Написание кода:
Функция DllMain - основная функция Dll библиотеки, которая в нашем случае играет роль Asi плагина. Эта функция вызывается при четырех условиях - создании/уничтожении потока, и при присоединении и отсоединении нашей библиотеки. Первые два условия в данный момент нас не особо интересуют, поэтому перейдем к другим двум. Функция принимает в себя 3 аргумента, один из которых зарезервирован системой(lpReserved). Остальные два аргумента показывают нам базовый адрес библиотеки(Адрес по которому начинается наша библиотека в оперативной памяти) и причину вызова функции. Причина вызова как я уже описал выше - имеет 4 значения: DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH и DLL_PROCESS_DETACH. На данные момент нас интересуют первое и последнее из них. Первое вызывается при присоединении к процессу, последнее - при отсоединении.

Дальше работаем с DLL_PROCESS_ATTACH(В нашем случае оно будет выполнять функцию int main, как в консольном приложении C/C++).
DLL_PROCESS_DETACH на данный момент нам не нужен, т.к. нам нечего освобождать после выгрузки.
Начну с того, что DllMain с причиной DLL_PROCESS_ATTACH вызывается еще до появления окна GTA, когда игра еще не инициализирована, поэтому мы не можем взаимодействовать с игрой на этом моменте, и придется дождаться ее инициализации, это можно сделать разными путями, но на этот раз сделаем через создание потока, но так лучше не делать, и в дальнейшем я возможно покажу как сделать лучше.
Дело в том, что потоки на процессоре могут выполняться совершенно параллельно, и может возникнуть ситуация когда сразу несколько потоков(в нашем случае это поток игры и наш поток) обращаются к одной и той же памяти. Возникает состояние гонки потоков, и это приводит к Undefined Behaviour (по-русски - все пойдет по пизде)
Начнем с того, что к case DLL_PROCESS_ATTACH: добавим break;, чтобы выполнение кода не пошло по другим веткам. Так как нас не интересуют события с потоками, скажем Windows, чтобы она вообще не дергала нас по этому поводу, вызвав функцию DisableThreadLibraryCalls(hModule);

Переходим к созданию функции ожидания инициализации и добавления сообщения на экран.
Для функции CreateThread требуется функция определенного типа, а для std::thread подойдет любая. В этом гайде я покажу оба варианта.
Создаем функции:
C++:
DWORD WINAPI InitializeAndLoad(LPVOID param) {

return 0;
}
Или (для std::thread)

C++:
void InitializeAndLoad() {

}

Функцию добавления сообщения на экран возьмем с plugin-sdk

Далее переходим к инициализации.
Игра хранит свою стадию по адресу 0xC8D4C0
И пока значение по адресу не станет 9(полная инициализация игры) - спим и ждем
C++:
while (*reinterpret_cast<unsigned char*>(0xC8D4C0) != 9) {
    std::this_thread::sleep_for(std::chrono::milliseconds(1u));
}
Далее спокойно вызываем функцию AddMessageJumpQ, ведь мы знаем, что игра уже инициализирована
C++:
void AddMessageJumpQ(const char* text, unsigned int time, unsigned short flag, bool bPreviousBrief)
{
    ((void(__cdecl*)(const char*, unsigned int, unsigned short, bool))0x69F1E0)(text, time, flag, bPreviousBrief);
}
C++:
AddMessageJumpQ("~r~Hello from blast.hk", 5000, 0, false);
Теперь создадим поток инициализации в DllMain, передав ему нашу функцию (не забывайте что для std::thread нужно подключить заголовок thread):
std::thread(InitializeAndLoad).detach();
Либо:
CreateThread(0, 0, &InitializeAndLoad, 0, 0, 0);
Итого должно выйти примерно так:
C++:
#include "pch.h"
#include <thread>

void AddMessageJumpQ(const char* text, unsigned int time, unsigned short flag, bool bPreviousBrief)
{
    ((void(__cdecl*)(const char*, unsigned int, unsigned short, bool))0x69F1E0)(text, time, flag, bPreviousBrief);
}

void InitializeAndLoad() {
    while (*reinterpret_cast<unsigned char*>(0xC8D4C0) != 9) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100u));
    }
    AddMessageJumpQ("~r~Hello from blast.hk", 5000, 0, false);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hModule);
        std::thread(InitializeAndLoad).detach();
        break;

    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
Либо так, в случае с CreateThread:

C++:
#include "pch.h"

void AddMessageJumpQ(const char* text, unsigned int time, unsigned short flag, bool bPreviousBrief)
{
    ((void(__cdecl*)(const char*, unsigned int, unsigned short, bool))0x69F1E0)(text, time, flag, bPreviousBrief);
}

DWORD WINAPI InitializeAndLoad(LPVOID) {
    while (*reinterpret_cast<unsigned char*>(0xC8D4C0) != 9) {
        Sleep(100);
    }
    AddMessageJumpQ("~r~Hello from blast.hk", 1000, 0, false);
    return 0;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hModule);
        CreateThread(0, 0, &InitializeAndLoad, 0, 0, 0);
        break;

    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Жмем Ctrl+Shift+B, ждем окончания сборки, заходим в игру и видим:
1622062765879.png
 
Последнее редактирование:

manukhov

Известный
126
128
Начнем с того, что к case DLL_PROCESS_ATTACH: добавим break;, чтобы выполнение кода не пошло по другим веткам. Так как нас не интересуют события с потоками, скажем Windows, чтобы она вообще не дергала нас по этому поводу, вызвав функцию DisableThreadLibraryCalls(hModule);
ну тогда наверно стоит просто bool fdwReason присвоить
C++:
BOOL APIENTRY DllMain(HMODULE hModule, bool fdwReason, LPVOID lpReserved)
{
    if (fdwReason)
    {
        g_hModule = hModule;
        DisableThreadLibraryCalls(hModule);
        CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)main, NULL, NULL, NULL);
    }
    return TRUE;
}
 

kin4stat

mq-team
Автор темы
Всефорумный модератор
2,730
4,710
ну тогда наверно стоит просто bool fdwReason присвоить
C++:
BOOL APIENTRY DllMain(HMODULE hModule, bool fdwReason, LPVOID lpReserved)
{
    if (fdwReason)
    {
        g_hModule = hModule;
        DisableThreadLibraryCalls(hModule);
        CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)main, NULL, NULL, NULL);
    }
    return TRUE;
}
Сигнатура не совпадет, может линкер ошибкой отбить, мол не то подаешь. А вообще это IFNDR. В целом подмена entrypoint, это уже отдельная тема, там много нюансов, слишком большое обсуждение выйдет
 

manukhov

Известный
126
128
Сигнатура не совпадет, может линкер ошибкой отбить, мол не то подаешь. А вообще это IFNDR. В целом подмена entrypoint, это уже отдельная тема, там много нюансов, слишком большое обсуждение выйдет
Окей, но я делал так в течении какого-то времени и у меня проблем пока не возникало, я думаю здесь это не так принципиально.
 
  • Злость
Реакции: THERION

Leatington

Известный
258
71
Возможно, мелкое замечание, но почему бы не создавать сразу пустой проэкт, чтобы потом не вычищать оттуда прекомпилированные заголовки, раз ты их всё равно не используешь?
 

askfmaskfaosflas

Потрачен
1,089
513
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
И так, начнем с создания проекта. Жмем кнопку создать проект, и ищем "Библиотека динамической компоновки (DLL)" (Дело в том, что ASI это и есть DLL файл, только с измененным расширением).
в каком разделе? я не нашёл (lmao)