Интро
Данная тема является продолжением предыдущей:
Поиск функций в клиенте RF Online. В ней мы нашли, как использовать умения. В данной теме речь пойдет про выделение мобов/персонажей в таргет.
Вот, что мы получим в итоге:
. Жмем "1" - выбираются персонажи, "2" - мобы.
Манипуляции будут проводиться на клиенте 2.2.3.2. Итоговый таб-таргет потестил на церберусе - работает. В теории должно работать на любом 2.2.3.2 клиенте. Если версия другая, то можно повторить шаги из гайда, чтобы найти оффсеты, либо попробовать
.
Содержание:
- Поиск функции выбора объекта в таргет
- Поиск массива мобов, персонажей и NPC
- Реализация простого таб-таргета
Просто напиши оффсеты не хочу ничего искать
Скролльте до параграфа "Реализация простого таб-таргета"
Поиск функции выбора объекта в таргет
- Заходим в игру, выбираем моба в таргет, начинаем искать в CE название монстра
Example
- Видим статически адрес (RF_Online.bin+1F38E40, он же 02338E40, зеленый на скрине). Если выберите другого моба - значение изменится. По этому адресу всегда лежит имя текущего выделенного моба.
Example
- Открываем x64dbg и ставим write breakpoint на данный адрес
Example
- Выделяем моба, breakpoint сработает после команды, выполнившей запись по адресу
Example
- Далее тактика такая: возвращаемся из функции (execute till return + step over) и NOP-ем ее. Выполняем это до тех пор пока выделение в таргет кликом не перестанет работать. То есть NOP-нули, кликнули на моба, если все еще работает - возвращаем исходный код и повторяем тоже самое с текущей функцией поднимаясь по стеку вызовов.
- В итоге мы найдем адрес 00522F6A, который вызывает функцию по адресу 005234E0
Example
Если подняться еще на одну ступеньку выше и NOP-нуть там, то персонаж еще и перестанет двигаться при клике на местность. То есть функция передвижения тоже где-то рядом.
- Даем функции имя SelectTarget и смотрим на аргументы. Функция вызывается от имени объекта, который лежит по адресу 12F13D10 (). Забегая вперед, первый аргумент - адрес моба, второй - константа 0.
Example
- Найти оффсет для 12F13D10 просто: во всей цепочке вызовов выше данный адрес передается в ecx. Делаем execute till return + step over пока не встретим первый статический оффсет при записи в ecx. Находим адрес 007002E3. Получаем формулу [edx + 75888] = [[ebp - 4] + 75888]. В [ebp - 4] лежит ecx для текущей функции.
Example
- Еще раз делаем execute till return + step over и находим базовый адрес. В итоге формула становится [23612C8 + 75888] = [23D6B50]. От имени объекта по этому адресу и вызывается SelectTarget.
Example
- Статический анализатор подсказал мне, что данный класс называется CGP_MainGame.
Example
В итоге получаем такую сигнатуру
Код на C++
Код:
typedef void* (__thiscall* _SelectTarget)(void* ecx, void* target, int zero);
_SelectTarget SelectTarget = (_SelectTarget)(0x005234E0);
void** pThis = (void**)0x23D6B50;
UPD: Дополнение по выбору в таргет
https://www.rf-cheats.ru/forum/showp...1&postcount=91
Поиск массива мобов, персонажей и NPC
- Ставим breakpoint на push первого аргумента для SelectTarget в стек - это адрес моба, сохраняем его в блокнот.
Example
- Ставим access breakpoint на данный адрес.
Example
- Сразу же сработает breakpoint. Мы будем находиться в функции по адресу 004F5DB0. Вот так она выглядит. Я уже дал всем функциям имена. При возврате из функции мы найдем базовый адрес для массива объектов вокруг персонажа - 233b5f0
Example
- В данной функции мы видим двойной цикл.
- Первый итерируется от 0 до 0xC по массиву объектов, каждый из которых состоит из двух адресов - адрес первого элемента и адрес последнего элемента вложенного массива. То есть это своего рода матрица из объектов, с которыми персонаж может взаимодействовать. В голове можно держать эту структуру так [{firstAddress, lastAddress}, {firstAddress, lastAddress}, ... {firstAddress, lastAddress}].
- Второй цикл берет значение по первому адресу 233b5f0 + index + 0xC + 4 и последнему адресу 233b5f0 + index + 0xc + 8 (перед подстановкой в формулу к index также применяется сдвиг влево на 4 бита - shl 4). В каждой итерации к первому значению прибавляется 4 до тех пор, пока адрес не станет равен последнему значению.
Example
В итоге мы имеем базовый адрес для массива объектов
233b5f0. Всего в массиве 12 элементов (
0xC). Каждый элемент содержит адрес первого и последнего элемента вложенного массива. Эти адреса достаются по формулам
233b5f0 + index (shl 4) + 0xc + 4 и
233b5f0 + index (shl 4) + 0xc + 8.
Реализация простого таб-таргета
Компилируем
DLL и инжектим её через Cheat Engine. После инжекта нажимаем
TAB для выбора мобов.
Реализация тривиальная: объекты берутся в таргет в том порядке, в каком они расположены в памяти. Сортировку по удалённости от персонажа, ХП и другим параметрам можете реализовать сами — оффсеты известны. По индексам 0, 1 и 2 находятся персонажи, мобы и НПС соответственно. Остальные массивы у меня были пустыми, но я не выходил за пределы генштаба.
DLL хукается к
WndProc, чтобы перехватывать нажатия клавиш и вызывать
SelectTarget в главном потоке игры.
Код на C++ (Updated 02.12.2024)
Код:
#include <Windows.h>
#include <cstdint>
#include <vector>
WNDPROC oWndProc = NULL;
HWND window = NULL;
struct Character {
uint8_t padding1[16]; // Padding to the 'name' field (16 bytes)
char name[64]; // Character's name (64 bytes)
float x; // Character's X coordinate (4 bytes)
float y; // Character's Y coordinate (4 bytes)
float z; // Character's Z coordinate (4 bytes)
uint8_t padding2[536]; // Padding to the 'hp_percent' field (536 bytes)
float hp_percent; // Character's HP percentage (4 bytes)
};
typedef void* (__thiscall* SelectTarget)(void* ecx, Character* target, int zero);
SelectTarget select_target = (SelectTarget)(0x005234E0);
void** select_target_ecx = (void**)0x23D6B50;
DWORD** surroundings_base_ptr = (DWORD**)0x005182C9; // Check the comments to see why this offset is different
size_t surroundings_len = 0xC;
int surroundings_players_index = 0;
int surroundings_mobs_index = 1;
int surroundings_npc_index = 2;
int inner_index = 0;
void handle_tab(int surroundings_index) {
DWORD* surroundings_base = *surroundings_base_ptr;
DWORD characters_ptr = *surroundings_base + 0xC + 4 + (surroundings_index << 4);
auto* characters = reinterpret_cast<std::vector<Character*>*>(characters_ptr);
if (characters && !characters->empty()) {
Character* current_character = (*characters)[inner_index];
select_target(*select_target_ecx, current_character, 0);
inner_index = (inner_index + 1) % characters->size();
}
}
BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
DWORD wndProcId;
GetWindowThreadProcessId(handle, &wndProcId);
if (GetCurrentProcessId() != wndProcId)
return TRUE; // skip to next window
window = handle;
return FALSE; // window found abort search
}
HWND GetProcessWindow()
{
window = NULL;
EnumWindows(EnumWindowsCallback, NULL);
return window;
}
LRESULT __stdcall WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN) {
if (wParam == VK_TAB) {
handle_tab(surroundings_mobs_index);
return true;
}
}
return CallWindowProc(oWndProc, hWnd, uMsg, wParam, lParam);
}
DWORD WINAPI injected_thread() {
do
window = GetProcessWindow();
while (window == NULL);
oWndProc = (WNDPROC)SetWindowLongPtr(window, GWLP_WNDPROC, (LONG_PTR)&WndProc);
return 0;
}
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
HANDLE ThreadHandle = NULL;
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
DisableThreadLibraryCalls(hModule);
ThreadHandle = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)injected_thread, hModule, 0, nullptr);
if (ThreadHandle == NULL)
return FALSE;
break;
}
if (ThreadHandle != NULL) {
CloseHandle(ThreadHandle);
}
return TRUE;
}
Готовое решение (Update 22.12.2024)
Допинал до рабочего состояния. Теперь умеет сортировать по HP и расстоянию.
При сортировке по ХП мобы сортируются по оставшемуся проценту ХП, если процент одинаковый — по расстоянию. Для сортировки по расстоянию — наоборот.
При сортировке по расстоянию выбирается следующий по удалённости после текущего выбранного (даже если он уже был выбран ранее).
HP
Расстояние
В архиве инжектор,
DLL и
README. Запускаете игру, выбираете персонажа, затем запускаете инжектор.
Тестил на церберусе и пвпваре x100.
Что нужно доработать:
- Выбирать только вражескую расу при выборе игроков.
- Исключать из выбора мёртвых мобов/игроков.
Архив прикреплен к комментариям: