Гайд Работа с рендером и Directx9 [4]

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

При использовании на других ресурсах необходимо указание авторства и ссылки на оригинальную темы!

Все действия производились на Visual Studio 2019 с параметром /std:c++17, в других версиях интерфейс может отличаться.

И так, начнем:

Создаем новый проект, настраиваем его, добавляем библиотеку хуков(буду показывать на примере своих хуков, как настроить проект и подключить хуки можете посмотреть в гайдах [1] и [3])
В свойствах проекта, в вкладке общие, стандарт языка C++ ставим /std:c++17

Устанавливаем Directx9 SDK на наш пк и подключаем к проекту:

Скачиваем установщик, производим полную установку, перезагружаем наш пк(желательно)
В панели меню сверху жмем Проект, и выпадающем меню выбираем пункт Свойства: $ProjectName
Сверху, в выпадающем меню в открывшемся диалоге выбираем Конфигурация -> Все конфигурации.
Директории VC++(VC++ Directories) -> Директории включения файлов(Include directories) -> добавляем в конец $(DXSDK_DIR)\Include\; (точка с запятой в конце обязательна)
Директории VC++(VC++ Directories) -> Директории библиотек(Library Directories) -> добавляем в конец $(DXSDK_DIR)\Lib\x86; (точка с запятой в конце обязательна)
Компоновщик(Linker) -> Ввод(Input) -> Дополнительные файлы зависимостей(Additional Dependencies) -> добавляем d3d9.lib;d3dx9.lib; (точки с запятой опять же обязательны)

Подключаем ImGui к нашему проекту:

ImGui можно подключать двумя путями.
Самый простой - vcpkg install imgui
Второй способ:
Скачиваем ImGui из репозитория и распаковываем все cpp и h файлы из корневой папки репозитория в папку нашего проекта. Также из папки backends берем файлы
  1. imgui_impl_win32.cpp
  2. imgui_impl_win32.h
  3. imgui_impl_dx9.h
  4. imgui_impl_dx9.cpp
Далее переходим в VS и делаем так:
89449

89450

Теперь перейдем к написанию кода:
Хукать directx в gta можно двумя путями:
  1. Хукать таблицу виртуальных методов
  2. Хукать функцию в библиотеке d3d9.dll

Покажу 2 и 3 способ, 1 способ делается на основе 3

И так, сначала расскажу что такое таблица виртуальных методов. Таблица виртуальных методов используется для разрешения виртуальных вызовов функций в коде. Благодаря ней выполняется получение и вызов нужной функции во время выполнения.

Обычно я оборачиваю все это в классы, но так как у нас базовый пример - буду показывать на примере свободных функций.

Чтобы хукать directx перед созданием девайса игрой, нам нужно искать виртуальную таблицу по сигнатуре в d3d9.dll
После ее нахождения мы возьмем оттуда адрес на функцию, и уже на самой функции будем ставить хук

C++:
#include "d3d9.h"

std::uintptr_t find_device(std::uint32_t Len) {
    static std::uintptr_t base = [](std::size_t Len) {
        std::string path_to(MAX_PATH, '\0');
        if (auto size = GetSystemDirectoryA(path_to.data(), MAX_PATH)) {
            path_to.resize(size);
            path_to += "\\d3d9.dll";
            std::uintptr_t dwObjBase = reinterpret_cast<std::uintptr_t>(LoadLibraryA(path_to.c_str()));
            while (dwObjBase++ < dwObjBase + Len) {
                if (*reinterpret_cast<std::uint16_t*>(dwObjBase + 0x00) == 0x06C7 &&
                    *reinterpret_cast<std::uint16_t*>(dwObjBase + 0x06) == 0x8689 &&
                    *reinterpret_cast<std::uint16_t*>(dwObjBase + 0x0C) == 0x8689) {
                    dwObjBase += 2;
                    break;
                }
            }
            return dwObjBase;
        }
        return std::uintptr_t(0);
    }(Len);
    return base;
}

Также напишем вспомогательную функцию для получения указателя функции в массиве:
C++:
void* get_function_address(int VTableIndex) {
    return (*reinterpret_cast<void***>(find_device(0x128000)))[VTableIndex];
}

Теперь у нас все есть для хука, и можем просто создавать хук:
C++:
#include "kthook/kthook.hpp"

// Сигнатуры функций
using PresentSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*);
using ResetSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);

// Создаем хуки и сразу же инициализируем их на адреса в d3d9.dll
kthook::kthook_signal<PresentSignature> present_hook{ get_function_address(17) };
kthook::kthook_signal<ResetSignature> reset_hook{ get_function_address(16) };

Создаем функции коллбэки для хуков(пока пустышки):
C++:
std::optional<HRESULT> on_present(const decltype(present_hook)& hook, IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*) {

    return std::nullopt; // не нужно прерывать выполнение
}

std::optional<HRESULT> on_lost(const decltype(reset_hook)& hook, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {

    return std::nullopt; // не нужно прерывать выполнение
}

void on_reset(const decltype(reset_hook)& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {

}

Сейчас вы наверное скажете "кинч че за хуйню ты тут написал какие деклтайпы и опшионалы ваще"
kthook пробрасывает состояние хука внутрь коллбэка. Чтобы вывести правильный тип для аргумента hook - мы используем decltype, который выдает тип переменной хука и подставит его.
Также можно делать вот так:
C++:
template<typename HookT>
void on_reset(const HookT& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {

}
Но в таком случае IntelliSense не покажет вам что пошло не так внутри функции. А использовать kthook удобнее с лямбдами.
Вернусь к рассказу. Про decltype рассказал, теперь про std::optional.
std::optional это специальный библиотечный тип, который позволяет делать объекты в которых либо есть значение, либо его нет. std::nullopt это пустой std::optional.
Чтобы kthook не прерывал выполнение оригинальной функции нужно возвращать пустой std::optional

Теперь будем разбираться с Directx.
on_present - коллбэк который будет использоваться для обработки Present. Если не углубляться в подробности - функции вызывается каждый кадр для отрисовки.
on_lost - коллбэк который будет обрабатывать состояние перед Reset. Вызывается при сбросе девайса(например при разворачивании игры)
on_reset - коллбэк который будет обрабатывать после Reset. В нашем случае не понадобится, но показать думаю стоило

Перед отрисовкой нам нужно инициализировать ImGui, будем это делать в present:
C++:
#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"


static bool ImGui_inited = false;
if (!ImGui_inited) {
    // Создаем имгуи контекст
    ImGui::CreateContext();
    // Инициализируем OS зависимую часть(обрабатывает открытие шрифтов, обработку нажатия клавиш и т.д.)
    ImGui_ImplWin32_Init(**reinterpret_cast<HWND**>(0xC17054));
    // Инициализируем render framework зависимую часть(обрабатывает отрисовку на экране, создание текстур шрифтов и т.д.)
    ImGui_ImplDX9_Init(device_ptr);
    ImGui_inited = true;
}

В коде выше я использовать магическую константу 0xC17054
Это одно из полей информации о движке игры. Оно хранит хендл окна необходимый для инициализации. Но дергать напрямую по адресу не стоит. Я так делаю для упрощения примера :D

Также для корректной работы всего Directx нам нужно сбрасывать ресурсы, которые создает ImGui в процессе своей работы(текстуры шрифтов например).
Это делать нужно во время сброса девайса, т.е. в on_reset:
C++:
ImGui_ImplDX9_InvalidateDeviceObjects();

Теперь можно рисовать на экране.
Для этого в on_present будем инициализировать ImGui для отрисовки одного кадра, рисовать все что нам нужно, и завершать кадр, и отдавать команду на рендер:
C++:
// Инициализируем render часть для нового кадра
ImGui_ImplDX9_NewFrame();
// Инициализируем OS часть для нового кадра
ImGui_ImplWin32_NewFrame();
// Создаем новый кадр внутри ImGui
ImGui::NewFrame();

// тут будем рисовать

// Завершаем кадр ImGui
ImGui::EndFrame();
// Рендерим ImGuiв внутренний буффер
ImGui::Render();
// Отдаем Directx внутренний буффер на рендер
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

Но при выгрузке плагина нужно освобождать ресурсы ImGui, поэтому в DLL_PROCESS_DETACH добавляем:
C++:
case DLL_PROCESS_DETACH:
    ImGui_ImplDX9_Shutdown();
    ImGui_ImplWin32_Shutdown();
    ImGui::DestroyContext();
    break;

Рисовать примитивы будет через дравлисты ImGui, т.к. это очень сильно оптимизированный и удобный вариант, нежели всякие костыльные самодельные функции
В этом гайде я буду выводить красный текст на экране и квадрат
C++:
// получаем дравлист
auto drawlist = ImGui::GetBackgroundDrawList();
 
// Вычисляем размер текста
std::string text{ "Hello from kin4!" };
ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
// Рисуем прямоугольник с от 0;0 до text_size + 20; text_size + 20 белого цвета и закруглением 5 пикселей
drawlist->AddRectFilled(ImVec2(0, 0), ImVec2(text_size.x + 20.0f, text_size.y + 20.0f), 0xFFFFFFFF, 5.0f);
 
// Вычисляем позицию текста
ImVec2 pos{ 10.0f, 10.0f };
ImVec4 text_color{ 1.0f, 0.0f, 0.0f, 1.0f };
// Рисуем текст
drawlist->AddText(pos, ImGui::GetColorU32(text_color), text.c_str());

Теперь остается только подключить коллбэки к хукам:
C++:
case DLL_PROCESS_ATTACH: {
    DisableThreadLibraryCalls(hModule);
    present_hook.before += on_present;
    reset_hook.before += on_lost;
    reset_hook.after += on_reset;
    break;
}

Скомпилировать плагин и увидеть результат:
1639780273993.png

Теперь расскажу про второй способ.
Т.к. таблица виртуальных методов это просто массив из указателей (void* vtbl[];, то мы можем просто изменить указатель в ней на свой.
Сам указатель на объект IDirect3DDevice9 лежит по адресу 0xC97C28

Т.к. функция будет вызываться вместо оригинальной, нам нужно сделать их такими же как оригинал:
C++:
HRESULT __stdcall on_present(IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*);
HRESULT __stdcall on_reset(IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters);

При этом on_lost будет до вызова prev_reset_ptr(о нем чуть ниже), а on_reset - после вызова и проверки результата вызова на D3D_OK

Чтобы изменить указатель в таблице, напишем вспомогательную функцию:
C++:
void* set_vtable_pointer(void* class_ptr, std::size_t index, void* value) {
    void** vtbl = *reinterpret_cast<void***>(class_ptr);
    void* prev = vtbl[index];
    vtbl[index] = value;
    return prev;
}

Теперь мы можем подменить указатель на метод:
C++:
auto device_ptr = *reinterpret_cast<IDirect3DDevice9**>(0xC97C28);
void* prev_present_ptr = set_vtable_pointer(device_ptr, 17, &on_present);
void* prev_reset_ptr = set_vtable_pointer(device_ptr, 16, &on_reset);

При отгрузке плагина нужно возвращать прошлые значения указателей, иначе ваш крашнет.
Также стоит помнишь, что по адресу 0xC97C28 ничего не будет вплоть до создания окна игры

C++:
#include <windows.h>
#include <string>
#include "d3d9.h"
#include "kthook/kthook.hpp"
#include "imgui.h"
#include "imgui_impl_dx9.h"
#include "imgui_impl_win32.h"

// Сигнатуры функций
using PresentSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, const RECT*, const RECT*, HWND, const RGNDATA*);
using ResetSignature = HRESULT(__stdcall*)(IDirect3DDevice9*, D3DPRESENT_PARAMETERS*);

std::uintptr_t find_device(std::uint32_t Len) {
    static std::uintptr_t base = [](std::size_t Len) {
        std::string path_to(MAX_PATH, '\0');
        if (auto size = GetSystemDirectoryA(path_to.data(), MAX_PATH)) {
            path_to.resize(size);
            path_to += "\\d3d9.dll";
            std::uintptr_t dwObjBase = reinterpret_cast<std::uintptr_t>(LoadLibraryA(path_to.c_str()));
            while (dwObjBase++ < dwObjBase + Len) {
                if (*reinterpret_cast<std::uint16_t*>(dwObjBase + 0x00) == 0x06C7 &&
                    *reinterpret_cast<std::uint16_t*>(dwObjBase + 0x06) == 0x8689 &&
                    *reinterpret_cast<std::uint16_t*>(dwObjBase + 0x0C) == 0x8689) {
                    dwObjBase += 2;
                    break;
                }
            }
            return dwObjBase;
        }
        return std::uintptr_t(0);
    }(Len);
    return base;
}

void* get_function_address(int VTableIndex) {
    return (*reinterpret_cast<void***>(find_device(0x128000)))[VTableIndex];
}

// Создаем хуки и сразу же инициализируем их на адреса в d3d9.dll
kthook::kthook_signal<PresentSignature> present_hook{ get_function_address(17) };
kthook::kthook_signal<ResetSignature> reset_hook{ get_function_address(16) };

std::optional<HRESULT> on_present(const decltype(present_hook)& hook, IDirect3DDevice9* device_ptr, const RECT*, const RECT*, HWND, const RGNDATA*) {
    static bool ImGui_inited = false;
    if (!ImGui_inited) {
        // Создаем имгуи контекст
        ImGui::CreateContext();
        // Инициализируем OS зависимую часть(обрабатывает открытие шрифтов, обработку нажатия клавиш и т.д.)
        ImGui_ImplWin32_Init(**reinterpret_cast<HWND**>(0xC17054));
        // Инициализируем render framework зависимую часть(обрабатывает отрисовку на экране, создание текстур шрифтов и т.д.)
        ImGui_ImplDX9_Init(device_ptr);
        ImGui_inited = true;
    }
    // Инициализируем render часть для нового кадра
    ImGui_ImplDX9_NewFrame();
    // Инициализируем OS часть для нового кадра
    ImGui_ImplWin32_NewFrame();
    // Создаем новый кадр внутри ImGui
    ImGui::NewFrame();

    // получаем дравлист
    auto drawlist = ImGui::GetBackgroundDrawList();
 
    // Вычисляем размер текста
    std::string text{ "Hello from kin4!" };
    ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
    // Рисуем прямоугольник с от 0;0 до text_size + 20; text_size + 20 белого цвета и закруглением 5 пикселей
    drawlist->AddRectFilled(ImVec2(0, 0), ImVec2(text_size.x + 20.0f, text_size.y + 20.0f), 0xFFFFFFFF, 5.0f);
 
    // Вычисляем позицию текста
    ImVec2 pos{ 10.0f, 10.0f };
    ImVec4 text_color{ 1.0f, 0.0f, 0.0f, 1.0f };
    // Рисуем текст
    drawlist->AddText(pos, ImGui::GetColorU32(text_color), text.c_str());

    // Завершаем кадр ImGui
    ImGui::EndFrame();
    // Рендерим ImGuiв внутренний буффер
    ImGui::Render();
    // Отдаем Directx внутренний буффер на рендер
    ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
    return std::nullopt; // не нужно прерывать выполнение
}

std::optional<HRESULT> on_lost(const decltype(reset_hook)& hook, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {
    ImGui_ImplDX9_InvalidateDeviceObjects();
    return std::nullopt; // не нужно прерывать выполнение
}

void on_reset(const decltype(reset_hook)& hook, HRESULT& return_value, IDirect3DDevice9* device_ptr, D3DPRESENT_PARAMETERS* parameters) {

}


BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: {
        DisableThreadLibraryCalls(hModule);
        present_hook.before += on_present;
        reset_hook.before += on_lost;
        reset_hook.after += on_reset;
        break;
    }
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
 
Последнее редактирование:

Rafaelofff

Потрачен
120
5
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.
Куда именно скидывать Imgui? Я скинул куда смог, но не пашет, расскажи что куда кидать по подробнее
 

kin4stat

mq-team
Автор темы
Всефорумный модератор
2,731
4,693
Инструкцию написали позавчера.
Подключаем ImGui к нашему проекту:

ImGui можно подключать двумя путями.
Самый простой - vcpkg install imgui
Второй способ:
Скачиваем ImGui из репозитория и распаковываем все cpp и h файлы из корневой папки репозитория в папку нашего проекта. Также из папки backends берем файлы
  1. imgui_impl_win32.cpp
  2. imgui_impl_win32.h
  3. imgui_impl_dx9.h
  4. imgui_impl_dx9.cpp
Далее переходим в VS и делаем так:
Люди до:
 
  • Нравится
Реакции: loganhackerdff

sc6ut

неизвестный
Модератор
382
1,074
Объясни? Я не шарю как подключать файлы к проекту, если не тяжело конечно )
обьясняю: выучи язык (чтобы мог писать что-то кроме hello world), изучи как использовать твой ide (visual studio), возвращайся в тему и пробуй вновь. если ты не знаешь как подключить файлы в проект, тут нет смысла даже пытаться говорить об использовании imgui.
 
  • Нравится
Реакции: forever., AnWu и Andrinall

pwnz

Участник
94
74
Хорошо, спасибо, вообще +- сколько мне нужно времени потратить на то чтобы научиться писать свой чит с Imgui спидхак, адм чекер, аирбрейк и всё это на asi?
5 минут, интернет, и отсутствие бана в Google