Jump to content
V3ct0r

Пример создания модификации: Вывод дополнительных характеристик персонажа

Recommended Posts

[Гайд] Пример создания модификации: Вывод дополнительных характеристик персонажа

 

graf_stats01.jpg

 

Привет!

 

В данной статье я расскажу как создать мод для клиента игры, который позволит выводить дополнительные характеристики персонажа на форме "Персонаж" (см. скриншот выше). Благодарю участника нашего форума @Graf за идею мода и гайда:

 

 

Вам понадобится

 

1) Game.exe для которого будет создаваться модификация;

2) OllyDbg v1.10;

3) IDA Pro (необязательно, можно пользоваться только OllyDbg);

4) Visual Studio 2022 Community Edition и библиотека MS Detours;

5) Исходный код клиента;

6) Знание основ программирования на C/C++ и ассемблере. Навыки написания DLL-библиотек. Опыт работы с вышеуказанными программами.

 

 

Создание мода

 

В качестве примера реализуем вывод трех дополнительных характеристик: удача (ATTR_LUK )шанс критического удара (ATTR_CRT ) и шанс дропа (ATTR_MF).

 

1) Поместим на форму с характеристиками (frmState) персонажа 3 текстовые метки: удача (labLukShow), шанс критического удара (labCriticalShow), шанс дропа (labMfShow).

 

preperty.clu:

-- Lucky
labLukShow  = UI_CreateCompent( frmState, LABELEX_TYPE, "labLukShow", 26, 8, 16, 370 )
UI_SetCaption( labLukShow, "L" )
UI_SetTextColor( labLukShow, COLOR_PURPLE )
UI_SetLabelExFont( labLukShow, DEFAULT_FONT, TRUE, COLOR_WHITE )

-- Critical chance
labCriticalShow  = UI_CreateCompent( frmState, LABELEX_TYPE, "labCriticalShow", 26, 8, 80, 370 )
UI_SetCaption( labCriticalShow, "C" )
UI_SetTextColor( labCriticalShow, COLOR_PURPLE )
UI_SetLabelExFont( labCriticalShow, DEFAULT_FONT, TRUE, COLOR_WHITE )

-- Drop chance
labMfShow  = UI_CreateCompent( frmState, LABELEX_TYPE, "labMfShow", 26, 8, 144, 370 )
UI_SetCaption( labMfShow, "M" )
UI_SetTextColor( labMfShow, COLOR_PURPLE )
UI_SetLabelExFont( labMfShow, DEFAULT_FONT, TRUE, COLOR_WHITE )

 

screenshot_1.png

 

2) Изучив исходный код клиента, находим функцию void CStateMgr::RefreshStateFrm(), в которой происходит обновление меток с характеристиками персонажа. Далее нам будет необходимо перехватывать её вызов и обновлять наши новые метки. Так же есть функция void CStateMgr::_evtMainShow(CGuiData *pSender), она вызывается при открытии формы характеристик персонажа и её можно использовать для получения указателей на объекты текстовых меток в памяти игры. Параметр pSender это указатель на форму frmState, объекта класса  CCompent, у которого есть метод CCompent* CForm::Find(const char* str), этот метод понадобится для получения указателей на метки по их именам, указанных в lua скриптах GUI клиента. Чтобы установить текст меток, нам будет нужен метод void CLabel::SetCaption(const char * str). Наконец, для доступа к параметрам нашего персонажа, нам будет нужен указатель на персонажа, которым управляет игрок.

 

3) Для поиска адресов в Game.exe будем использовать программу IDA Pro

 

void CStateMgr::RefreshStateFrm(). Данный метод можно найти по какой-либо уникальной строке, например, "%4.2f%%". Ищем эту строку в IDA Pro и находим её использование только в одной функции по адресу 0x0047F190 - это и есть искомый метод.

 

screenshot_2.png

 

void CStateMgr::_evtMainShow(CGuiData *pSender). Это обработчик события открытия формы характеристик персонажа, значит он где-то "вешается" на объект в формы. Проверим в исходном коде клиента игры в методе инициализации формы frmState:

bool CStateMgr::Init()
{
	CFormMgr &mgr = CFormMgr::s_Mgr;

	frmState = _FindForm("frmState");
	if( !frmState )		return false;
	frmState->evtShow = _evtMainShow;

	labStateName  = dynamic_cast<CLabelEx*>(frmState->Find("labStateName"));    
	if( !labStateName )		return Error( g_oLangRec.GetString(45), frmState->GetName(), "labStateName" );
	labStateName->SetIsCenter(true);

 

Теперь необходимо найти этот код в Game.exe. Для этого выполняем поиск по строке "frmState". В этот раз нам повезло меньше, найдено 3 использования такой строки:

screenshot_3.png

 

Смотрим первое использование:

screenshot_4.png

 

Рядом так же видно использование строк "btnState", "btnSkill", и "frmSkill". Но в искомом коде таких строк рядом нет. Очевидно, этот адрес нам не подходит.

 

Смотрим второе использование:

screenshot_5.png

 

Строка находится в самом начале функции и рядом есть строка "labStateName". Судя по всему, это наш искомый метод инициализации. Здесь видно, как загружается указатель на форму frmState в регистр EAX, а далее по смещению 0x78, относительно адреса в регистре EAX, помещается какая-то функция. Наверняка, это и есть метод void CStateMgr::_evtMainShow(CGuiData *pSender). Итак, адрес метода void CStateMgr::_evtMainShow(CGuiData *pSender) 0x0047F7F0.

 

CCompent* CForm::Find(const char* str). Этот метод скорее всего находится в функции инициализации формы, такую мы уже видели: bool CStateMgr::Init(). Просмотрев исходный код, видим следующее:

labStateName  = dynamic_cast<CLabelEx*>(frmState->Find("labStateName"));    
if( !labStateName ) return Error( g_oLangRec.GetString(45), frmState->GetName(), "labStateName" );
labStateName->SetIsCenter(true);

Это код получения указателя на метку labStateName, нам предстоит сделать то же самое с нашими новыми метками. Идем по адресу 0x0047F9F0 метода bool CStateMgr::Init() в IDA Pro и видим следующий код:

.text:0047FA25 push offset aLabstatename ; "labStateName"
.text:0047FA2A call dword ptr [eax+48h]

Похоже на вызов метода CCompent* CForm::Find(const char* str) для получения указателя на метку labStateName, но его адрес не указан явно. Это говорит о том, что метод виртуальный. Проверим это в исходном коде:

virtual CCompent* Find( const char* str )   { return _frmOwn->Find( str ); }

Метод действительно является виртуальным. Как же получить его адрес? Как вариант, открыть Game.exe в отладчике, поставить точку останова на адрес 0x0047FA2A и проверить адрес вызываемой функции, но есть проблема - мы не можем запустить клиент напрямую через отладчик, а после запуска эта функция уже не вызывается. Есть второй вариант: поставить точку останова на метод void CStateMgr::_evtMainShow(CGuiData *pSender) (его адрес мы уже знаем), получить адрес формы frmState через параметр pSender, прибавить к нему смещение 0x48 и посмотреть адрес искомого метода. Идем в игру и открываем форму характеристик персонажа:

screenshot_6.png

 

На стеке смотрим адрес параметра pSender - 0x109D5568. По этому адресу находится адрес объекта CStateMgr - 0x006089B4. Прибавляем к этому адресу смещение 0x48, получаем адрес 0x006089FC и переходим по данному адресу в окне отображения памяти процесса:

screenshot_7.png

 

Итак, адрес метода CCompent* CForm::Find(const char* str) равен 0x004941F0.

 

void CLabel::SetCaption(const char * str), в исходном коде вызов этого метода встретится в методе void CStateMgr::RefreshStateFrm(), например:

	if ( labStateEXP)
	{
		if (max!=0)
			sprintf( pszCha , "%4.2f%%" , num*100.0f/max );	
		else 
			sprintf( pszCha , "0.00%");

		labStateEXP->SetCaption( pszCha );
	}

Снова ищем строку "%4.2f%%" в IDA Pro и переходим по адресу её использования:

screenshot_8.png

 

Очевидно, что вызов неизвестной функции после двух вызовов библиотечной функции sprintf устанавливает текст метки labStateEXP. Функция является виртуальной, поэтому вновь открываем отладчик, ставим точку останова по адресу 0x0047F2A1 и открываем форму характеристик персонажа в игре:

screenshot_9.png

 

Отладчик подсказывает нам, что по адресу [EDX+3C] находится адрес метода void CLabel::SetCaption(const char * str)0x0042B1A0.

 

Указатель на персонажа игрока можно посмотреть в том же методе void CStateMgr::RefreshStateFrm():

	CForm * f = g_stUIState.frmState;
	if( !f->GetIsShow() )	return;

	CCharacter* pCha = g_stUIBoat.GetHuman();
	if( !pCha )			return;

	SGameAttr* pCChaAttr = pCha->getGameAttr();
	if (!pCChaAttr )	return;

Переходим на начало метода в IDA Pro:

screenshot_10.png

 

Анализ ассемблерного листинга показывает, что указатель на персонажа игрока находится по адресу 0x00668B6C.

 

Мы нашли все необходимые адреса:

0x0047F7F0 void CStateMgr::_evtMainShow(CGuiData *pSender)
0x0047F190 void CStateMgr::RefreshStateFrm()
0x004941F0 CCompent* CForm::Find(const char* str)
0x0042B1A0 void CLabel::SetCaption(const char * str)
0x00668B6C CCharacter* CBoatMgr::_pHuman

 

4) Все готово для создания DLL. В качестве языка программирования будем использовать C++. Создаем новый проект Dynamic-Link Library (DLL) в Visual Studio 2022 Community. Поскольку мы будем перехватывать вызовы void CStateMgr::_evtMainShow(CGuiData *pSender) и void CStateMgr::RefreshStateFrm(), то нам понадобится библиотека MS Detours, подключаем её к проекту.

 

Записываем код DLL:

#include <windows.h>
#include <detours.h>
#include <cstdio>

namespace pkodev
{
    // Addresses of imported functions from Game.exe
    namespace address
    {
        // void CStateMgr::_evtMainShow(CGuiData *pSender)
        const unsigned int CStateMgr___evtMainShow = 0x0047F7F0;

        // void CStateMgr::RefreshStateFrm()
        const unsigned int CStateMgr__RefreshStateFrm = 0x0047F190;

        // CCompent* CForm::Find( const char* str )
        const unsigned int CForm__Find = 0x004941F0;

        // void CLabel::SetCaption( const char * str )
        const unsigned int CLabel__SetCaption = 0x0042B1A0;

        // CCharacter* CBoatMgr::_pHuman
        const unsigned int CBoatMgr___pHuman = 0x00668B6C;
    }

    namespace pointer
    {
        // void CStateMgr::_evtMainShow(CGuiData *pSender)
        typedef void(__cdecl* CStateMgr___evtMainShow__Ptr)(void*);
        CStateMgr___evtMainShow__Ptr CStateMgr___evtMainShow = (CStateMgr___evtMainShow__Ptr)(void*)(address::CStateMgr___evtMainShow);

        // void CStateMgr::RefreshStateFrm()
        typedef void(__cdecl* CStateMgr__RefreshStateFrm__Ptr)(void*);
        CStateMgr__RefreshStateFrm__Ptr CStateMgr__RefreshStateFrm = (CStateMgr__RefreshStateFrm__Ptr)(void*)(address::CStateMgr__RefreshStateFrm);

        // CCompent* CForm::Find( const char* str )
        typedef void* (__thiscall* CForm__Find__Ptr)(void*, const char*);
        CForm__Find__Ptr CForm__Find = (CForm__Find__Ptr)(void*)(address::CForm__Find);

        // void CLabel::SetCaption( const char * str )
        typedef void (__thiscall* CLabel__SetCaption__Ptr)(void*, const char*);
        CLabel__SetCaption__Ptr CLabel__SetCaption = (CLabel__SetCaption__Ptr)(void*)(address::CLabel__SetCaption);
    }

    namespace hook
    {
        // void CStateMgr::_evtMainShow(CGuiData *pSender)
        void __cdecl CStateMgr___evtMainShow(void* pSender);

        // void CStateMgr::RefreshStateFrm()
        void __fastcall CStateMgr__RefreshStateFrm(void* This, void* NotUsed);
    }

    // Label "labLukShow"
    void* labLukShow = nullptr;

    // Label "labCriticalShow"
    void* labCriticalShow = nullptr;

    // Label "labMfShow"
    void* labMfShow = nullptr;
}


// Entry point
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
        // DLL attached to the proccess
        case DLL_PROCESS_ATTACH:

            // Enable hooks
            DetourRestoreAfterWith();
            DetourTransactionBegin();
            DetourUpdateThread(GetCurrentThread());
            DetourAttach(&(PVOID&)pkodev::pointer::CStateMgr___evtMainShow, pkodev::hook::CStateMgr___evtMainShow);
            DetourAttach(&(PVOID&)pkodev::pointer::CStateMgr__RefreshStateFrm, pkodev::hook::CStateMgr__RefreshStateFrm);
            DetourTransactionCommit();

            break;
        
        // DLL detached from the proccess
        case DLL_PROCESS_DETACH:

            // Disable hooks
            DetourTransactionBegin();
            DetourUpdateThread(GetCurrentThread());
            DetourDetach(&(PVOID&)pkodev::pointer::CStateMgr___evtMainShow, pkodev::hook::CStateMgr___evtMainShow);
            DetourDetach(&(PVOID&)pkodev::pointer::CStateMgr__RefreshStateFrm, pkodev::hook::CStateMgr__RefreshStateFrm);
            DetourTransactionCommit();

            break;
    }

    return TRUE;
}

// void CStateMgr::_evtMainShow(CGuiData *pSender)
void __cdecl pkodev::hook::CStateMgr___evtMainShow(void* pSender)
{
    // Get pointers to the labels
    pkodev::labLukShow = pkodev::pointer::CForm__Find(pSender, "labLukShow");
    pkodev::labCriticalShow = pkodev::pointer::CForm__Find(pSender, "labCriticalShow");
    pkodev::labMfShow = pkodev::pointer::CForm__Find(pSender, "labMfShow");

    // Call original CStateMgr___evtMainShow() function
    pkodev::pointer::CStateMgr___evtMainShow(pSender);
}

// void CStateMgr::RefreshStateFrm()
void __fastcall pkodev::hook::CStateMgr__RefreshStateFrm(void* This, void* NotUsed)
{
    // Call original function CStateMgr::RefreshStateFrm()
    pkodev::pointer::CStateMgr__RefreshStateFrm(This);

    // Get pointer to the main character
    // CCharacter* pCha = g_stUIBoat.GetHuman();
    void* pCha = reinterpret_cast<void*>(
        *reinterpret_cast<unsigned int*>(pkodev::address::CBoatMgr___pHuman)
    );

    if (pCha == nullptr)
    {
        // Exit from the hook
        return;
    }

    // Define ATTR constants
    const unsigned int ATTR_LUK = 30;
    const unsigned int ATTR_MF  = 38;
    const unsigned int ATTR_CRT = 39;

    // Get offset to the attribute
    auto attr_get = [&pCha](unsigned int id) -> unsigned int
    {
        return *reinterpret_cast<unsigned int*>(
            (0xD0 + 4 * id + reinterpret_cast<unsigned int>(pCha))
        );
    };

    // Buffer for labels captions
    char buf[128]{ 0x00 };

    // Show character's ATTR_LUK
    if (pkodev::labLukShow != nullptr)
    {
        sprintf_s(buf, sizeof(buf), "LUK: %d", attr_get(ATTR_LUK));
        pkodev::pointer::CLabel__SetCaption(pkodev::labLukShow, buf);
    }

    // Show character's ATTR_CRT
    if (pkodev::labCriticalShow != nullptr)
    {
        sprintf_s(buf, sizeof(buf), "CRT: %d", attr_get(ATTR_CRT));
        pkodev::pointer::CLabel__SetCaption(pkodev::labCriticalShow, buf);
    }

    // Show character's ATTR_MF
    if (pkodev::labMfShow != nullptr)
    {
        sprintf_s(buf, sizeof(buf), "MF: %d", attr_get(ATTR_MF));
        pkodev::pointer::CLabel__SetCaption(pkodev::labMfShow, buf);
    }
}

 

1. В начале определяем найденные нами в пункте 3 адреса методов, функций и объектов;

2. Далее определяем указатели на необходимые функции и методы;

3. Затем определяем функции-перехватчики и глобальные переменные под указатели на текстовые метки;

4. В функции DllMain() записываем код для установки и снятия перехватчиков с помощью библиотеки Detours;

5. Реализуем перехватчик pkodev::hook::CStateMgr___evtMainShow(). В этой функции получаем указатели на объекты новых меток labLukShowlabCriticalShow и labMfShow с помощью метода CCompent* CForm::Find(const char* str);

6. Реализуем перехватчик pkodev::hook::CStateMgr__RefreshStateFrm(). Сначала получаем указатель на персонажа, которым управляет игрок. Далее записываем константы, обозначающие ID требуемых характеристик. Теперь стоит задача получения этих характеристик из памяти игры. В исходном коде клиента для этого используется указатель на объект типа SGameAttr, связанный с персонажем:

	SGameAttr* pCChaAttr = pCha->getGameAttr();
	if (!pCChaAttr )	return;

Анализ ассемблерного листинга показывает, что этот указатель находится по смещению 0xD0 относительно указателя на персонажа:

.text:0047F1D2 mov     ebx, dword_668B6C
. . .
.text:0047F1E4 add     ebx, 0D0h

Посмотрим что представляет собой объект SGameAttr в исходном коде:

struct SGameAttr
{
    long lAttr[MAX_ATTR_CLIENT]; // MAX_ATTR_CLIENT = 74
	. . .
};

Это структура, которая содержит только одно поле - массив из 74 целых чисел по 4 байта каждое, индексы элементов которого соответствует ID характеристик персонажа (см. файл AttrType.lua из скриптов GameServer). Из курса программирования мы знаем, что все элементы массива хранятся в памяти друг за другом. Значит, чтобы получить адрес характеристики персонажа с определенным ID, мы можем воспользоваться следующей формулой: 

0xD0 + 4 * <ID> + <Адрес персонажа в памяти>

Осталось самое простое: вывести требуемые характеристики персонажа в новые текстовые метки с помощью метода void CLabel::SetCaption(const char * str).

 

5) Компилируем проект и присоединяем DLL библиотеку к процессу игры. В результате мы получим следующее:

screenshot_11.png

 

 

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

 

Отмечу, что в примере я использовал GAME_13X_2, в Вашем Game.exe найденные адреса могут отличаться. Если у Вас возникли вопросы, то смело задавайте их в этой теме. Ниже прикладываю проект DLL-библиотеки и DLL-библиотеку мода.

 

 

Скачать

 

Архив с проектом DLL библиотеки модификации для Visual Studio 2022 Community и DLL библиотекой модификации из примера (286 Кб)

 

Также на основе данной статьи был создан полноценный мод для вывода характеристик персонажа, который не ограничивается тремя характеристиками:

 

  • Thanks 1

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...