Делать мне было нечего, а работать не хотелось, поэтому вы видите этот гайд
- Создание ASI-плагина с нуля
- Хуки – что это такое и как с ними работать
- Безопасная инициализация и работа с SAMP
- Работа с рендером и Directx9
- Обработка событий окна + 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:
добавим
, чтобы выполнение кода не пошло по другим веткам. Так как нас не интересуют события с потоками, скажем Windows, чтобы она вообще не дергала нас по этому поводу, вызвав функцию
Код:
DisableThreadLibraryCalls(hModule);
Переходим к созданию функции ожидания инициализации и добавления сообщения на экран.
Для функции
требуется функция определенного типа, а для std::thread подойдет любая. В этом гайде я покажу оба варианта.
Создаем функции:
C++:
Код:
DWORD WINAPI
InitializeAndLoad
(
LPVOID param
)
{
return
0
;
}
Или (для std::thread)
C++:
Код:
void
InitializeAndLoad
(
)
{
}
Функцию добавления сообщения на экран возьмем с
plugin-sdk
Далее переходим к инициализации.
Игра хранит свою стадию по адресу 0xC8D4C0
И пока значение по адресу не станет 9(полная инициализация игры) - спим и ждем
C++:
Код:
while
(
*
reinterpret_cast
(
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
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
(
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
(
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, ждем окончания сборки, заходим в игру и видим:
