• Уважаемые гости и новички, приветствуем Вас на нашем форуме
    Здесь вы можете найти ответы практически на все свои вопросы о серии игр «Готика» (в том числе различных модах на нее), «Ведьмак», «Ризен», «Древние свитки», «Эра дракона» и о многих других играх. Можете также узнать свежие новости о разработке новых проектов, восхититься творчеством наших форумчан, либо самим показать, что вы умеете. Ну и наконец, можете обсудить общие увлечения или просто весело пообщаться с посетителями «Таверны».

    Чтобы получить возможность писать на форуме, оставьте сообщение в этой теме.
    Удачи!

6.2 Практические примеры перехвата функций

Gratt


Модостроитель
Регистрация
14 Ноя 2014
Сообщения
3.301
Благодарности
4.636
Баллы
625
* если после темы останутся вопросы - спрашиваем. Потом сделаем FAQ.

Классы перехватчики
CCallBack - весь процесс перехвата и управления хуками реализован в нем. Класс контролирует, чтобы программист не допустил критических ошибок, а также регулирует множественные перехваты.​
CInvoke - интерфейс перехвата. Чтобы не выполнять рутинную работу, этот класс внутри своих экземпляров строит запросы в CCallBack основываясь на базовых входных данных.​


Шаблон перехвата средствами CInvoke
Повторюсь, - CInvoke строит запросы за программиста. Экземпляру необходимо передать сведения об указателе на функцию (тип), адрес оригинальной функции и куда ее переадресовать.
Любой хук имеет вид:
C++:
CInvoke<pointer-type> Ivk_<hook-name>( original-pointer, redirect-pointer, hook-flags );
где:
Код:
pointer-type        - тип указателя на оригинальную функцию. Через него мы сможем обращаться к оригинальной функции. Как строить указатель см. в теме 6.1.
hook-name           - любое удобное имя хука. Я обычно предпочитаю вид Ivk_<class-name>_<method-name>, например Ivk_oCGame_Render.
original-pointer   - адрес на перехвачиваемую функцию или метод.
redirect-pointer   - адрес на переадресованную функцию.
hook-flags            - флаги перехвата. По умолчанию используется IVK_AUTO.
    IVK_DISABLED       Не дает хуку перехватить функцию. Делает экземпляр обычным указателем.
    IVK_READONLY      Аналогично предыдущему.
    IVK_NORMAL         Обычный хук. Если существует хотя бы одна перехваченная функция по тому же адресу - выскочит ошибка о невозможности перехвата.
    IVK_AUTO              Позволяет хукам создавать дерево перехвата по одному и тому же адресу. redirect-pointer будут вызываться поочереди, начиная с последнего за каждым return original-pointer.
    IVK_REDEFINE       Перезаписывает собой любой существующий по адресу хук, либо при отсутствии просто перехватывает функцию. Перезаписанный хук деактивируется.
    IVK_PROTECTED    Ставит защиту на хук. Рекомендуется использовать только в очень важных функциях, поскольку такой хук невозможно отключить, к нему невозможно подключиться, его невозможно перезаписать.





Шаблон перехвата с помощью макросов HOOK AS
Тут все еще проще. Это тот же CInvoke, только с двумя макросами сверху. За основу берется функция Intercept, в шаблоне которой автоматически определяется тип указателя.
C++:
HOOK Ivk_<hook-name> AS( original-pointer, redirect-pointer, hook-flags );
// расширяется до:
// auto Ivk_<hook-name> = InvokeAuto( original-pointer, redirect-pointer, hook-flags );





Перехват обычной функции
Открываем GothicNames и выбираем функцию. Пусть это будет событие, которое применяет выбранные настройки видео.
C++:
// 0x0042C450 int __cdecl Apply_Options_Video(void)
// прототип функции переадресации
int Apply_Options_Video();

// В <> указываем тип указателя оригинальной функции
CInvoke<int(*)()> Ivk_Apply_Options_Video( 0x0042C450, &Apply_Options_Video );

// реализация функции переадресации
int Apply_Options_Video() {
  Message::Info( "Попытка применить изменения . . ." );

  // Обращаемся к экземпляру хука как к функции.
  // Так будет вызвана оригинальная функция.
  // Оригинальная функция выполнит применение настроек видео и вернет значение в result.
  int Result = Ivk_Apply_Options_Video();

  Message::Info( "Изменения применены с кодом " + string::Hex32( Result ) );

  // Возвращаем значение в движок, полученное из оригинальной функции.
  return Result;
}




Перехват функции-члена
Рассмотрим вариант с функцией отрисовки воба. Покажу сразу несколько вариантов реализации.
Вариант #1
Переадресация метода в обычную функцию.
C++:
// 0x006015D0 public: virtual int __fastcall zCVob::Render(struct zTRenderContext &)
int __fastcall zCVob_Render( zCVob* _this, zTRenderContext& context );

CInvoke<int(__fastcall *)( zCVob* _this, zTRenderContext& context )> Ivk_zCVob_Render( 0x006015D0, &zCVob_Render );

int __fastcall zCVob_Render( zCVob* _this, zTRenderContext& context ) {
  if( _this == player ) {
    // Если в данный момент рендерится модель игрока, то на экран
     // будет выведено соответствующее сообщение
    screen->PrintCX( 1000, "Сейчас на экран рисуется player." );
  }
  // Отрабатываем и возвращаем оригинальный результат
  return Ivk_zCVob_Render( _this, context );
}


Вариант #2
переадресация метода в метод.
Для данного примера идем в Gothic API и в классе zCVob создаем новую функцию:
int __fastcall RenderUnion(struct zTRenderContext &);
C++:
// 0x006015D0 public: virtual int __fastcall zCVob::Render(struct zTRenderContext &)
CInvoke<int(__fastcall *)( zCVob* _this, zTRenderContext& context )> Ivk_zCVob_Render( 0x006015D0, &zCVob::RenderUnion );

// Прототип уже указан в классе. __fastcall не обязателен,
// так как он уже определен в прототипе.
int zCVob::RenderUnion( zTRenderContext& context ) {
  if( this == player ) {
    // Если в данный момент рендерится модель игрока, то на экран
     // будет выведено соответствующее сообщение
    screen->PrintCX( 1000, "Сейчас на экран рисуется player." );
  }
  // Отрабатываем и возвращаем оригинальный результат
  return Ivk_zCVob_Render( this, context );
}


Вариант #3
переадресация метода в метод с указателем на конкретную функци-член.
Для данного примера идем в Gothic API и в классе zCVob создаем новую функцию:
int __fastcall RenderUnion( struct zTRenderContext & );
C++:
// 0x006015D0 public: virtual int __fastcall zCVob::Render(struct zTRenderContext &)
// this не указываем, он предполагается типом.
CInvoke<int(__fastcall zCVob::*)( zTRenderContext& context )> Ivk_zCVob_Render( 0x006015D0, &zCVob::RenderUnion );

int zCVob::RenderUnion( zTRenderContext& context ) {
  if( this == player ) {
    screen->PrintCX( 1000, "Сейчас на экран рисуется player." );
  }
  // this передается оператором -> на указатель. см тему 6.1.
  return ( this->*Ivk_zCVob_Render )( context );
}





Множественный перехват функции
Далее можно поэкспериментировать в нескольких плагинах. Я же покажу в рамках одного, разницы никакой.
Идем в zCVob и пишем еще одну функцию ниже предыдущей.
int __fastcall RenderUnion_2( struct zTRenderContext & );

Дополнительно покажу как хукать конструкцией HOOK AS
C++:
// 0x006015D0 public: virtual int __fastcall zCVob::Render(struct zTRenderContext &)
HOOK Ivk_zCVob_Render   AS( 0x006015D0, &zCVob::RenderUnion   ); // Этот хук переватывает первым
HOOK Ivk_zCVob_Render_2 AS( 0x006015D0, &zCVob::RenderUnion_2 ); // А это вторым. Но сначала отработает он.

int zCVob::RenderUnion( zTRenderContext& context ) {
  if( this == player ) {
    screen->PrintCX( 1000, "Сейчас на экран рисуется player." );
  }
  return ( this->*Ivk_zCVob_Render )( context );
}

int zCVob::RenderUnion_2( zTRenderContext& context ) {
  if( this == player ) {
    screen->PrintCX( 1200, "А это сообщение говорит о том, что два хука уживаются в одной функции." );
  }
  return ( this->*Ivk_zCVob_Render_2 )( context );
}





Защищенный хук
Вообще не рекомендую баловаться такими хуками, если только прям очень нужно.
За защиту хука отвечает флаг IVK_PROTECTED, он запретит любые манипуляции над всей веткой хуков.
Пусть в этот раз будет функция проигрывания видеоролика.
C++:
// 0x0042B940 public: int __thiscall CGameManager::PlayVideo(class zSTRING)
int __fastcall CGameManager_PlayVideo  ( CGameManager* _this, void* vtable, zSTRING );
int __fastcall CGameManager_PlayVideo_2( CGameManager* _this, void* vtable, zSTRING );

CInvoke<int(__thiscall*)( CGameManager* _this, zSTRING )> Ivk_CGameManager_PlayVideo  ( 0x0042B940, &CGameManager_PlayVideo, IVK_PROTECTED );
CInvoke<int(__thiscall*)( CGameManager* _this, zSTRING )> Ivk_CGameManager_PlayVideo_2( 0x0042B940, &CGameManager_PlayVideo_2 ); // Тут нам выбьет ошибку. Перехват невозможен.

int __fastcall CGameManager_PlayVideo( CGameManager* _this, void* vtable, zSTRING videoName ) {
  int Result = Ivk_CGameManager_PlayVideo( _this, videoName );
  Message::Info( "Функция (#1) завершена с кодом " + string::Hex32( Result ) );
  return Result;
}

int __fastcall CGameManager_PlayVideo_2( CGameManager* _this, void* vtable, zSTRING videoName ) {
  int Result = Ivk_CGameManager_PlayVideo( _this, videoName );
  Message::Info( "Функция (#2) завершена с кодом " + string::Hex32( Result ) );
  return Result;
}





Замена хука
Все тот же пример, но без защиты. Второй хук будет иметь флаг перезаписи.
Такая конструкция выключит первый хук, а на его место поставит второй.
Выполнится функция CGameManager_PlayVideo_2.
C++:
// 0x006660E0 public: virtual void __thiscall oCNpc::OnDamage(struct oCNpc::oSDamageDescriptor &)
void __fastcall oCNpc_OnDamage_1( oCNpc*, void*, oCNpc::oSDamageDescriptor& );
void __fastcall oCNpc_OnDamage_2( oCNpc*, void*, oCNpc::oSDamageDescriptor& );

CInvoke<void( __thiscall* )( oCNpc*, oCNpc::oSDamageDescriptor& )> Ivk_oCNpc_OnDamage_1( 0x006660E0, &oCNpc_OnDamage_1 );
CInvoke<void( __thiscall* )( oCNpc*, oCNpc::oSDamageDescriptor& )> Ivk_oCNpc_OnDamage_2( 0x006660E0, &oCNpc_OnDamage_2, IVK_REDEFINE ); // Этот хук заменит первый

void __fastcall oCNpc_OnDamage_1( oCNpc* _this, void* vtable, oCNpc::oSDamageDescriptor& damdesc ) {
  Message::Info( "Сообщение об уроне #1" );
  Ivk_oCNpc_OnDamage_1( _this, damdesc );
}
void __fastcall oCNpc_OnDamage_2( oCNpc* _this, void* vtable, oCNpc::oSDamageDescriptor& damdesc ) { // В игре будет выведено только это сообщение
  Message::Info( "Сообщение об уроне #2" );
  Ivk_oCNpc_OnDamage_2( _this, damdesc );
}





Неактивный хук
Хук можно быстро сделать неактивным еще на этапе его построения.
Для этого указываем флаг IVK_DISABLED (или IVK_READONLY)
В примере ниже ни один из хуков не будет выполнен.
C++:
// 0x0042D710 int __cdecl Apply_Options_Game(void)
int Apply_Options_Game();

CInvoke<int( *)( )> Ivk_Apply_Options_Game( 0x0042D710, Apply_Options_Game, IVK_DISABLED );

int Apply_Options_Game() {
  Message::Info( "Применение игровых настроек . . ." );
  int Result = Ivk_Apply_Options_Game();
  Message::Box( "Функция завершилась с кодом " + string::Hex32( Result ) );
  return Result;
}





Комбинирование флагов
Флаги можно комбинировать. например сделать перезапись хука и моментальную его защиту.
В примере ниже второй хук перезапишет первый. Далее его будет невозможно ни изменить, ни удалить, ни подключиться к нему.
C++:
// 0x007323C0 public: void __thiscall oCNpc::EquipItem(class oCItem *)
void __fastcall oCNpc_EquipItem_1( oCNpc*, void*, oCItem* );
void __fastcall oCNpc_EquipItem_2( oCNpc*, void*, oCItem* );
void __fastcall oCNpc_EquipItem_3( oCNpc*, void*, oCItem* );

CInvoke<void( __thiscall* )( oCNpc*, oCItem* )> Ivk_oCNpc_EquipItem_1( 0x007323C0, oCNpc_EquipItem_1 );
CInvoke<void( __thiscall* )( oCNpc*, oCItem* )> Ivk_oCNpc_EquipItem_2( 0x007323C0, oCNpc_EquipItem_2, IVK_REDEFINE | IVK_PROTECTED ); // хук заменит первый
CInvoke<void( __thiscall* )( oCNpc*, oCItem* )> Ivk_oCNpc_EquipItem_3( 0x007323C0, oCNpc_EquipItem_3, IVK_REDEFINE ); // доступ будет невозмжен, так как предыдущий защищен

void __fastcall oCNpc_EquipItem_1( oCNpc* _this, void* vtable, oCItem* item ) {
  Message::Info( "OnEquip #1" );
  Ivk_oCNpc_EquipItem_1( _this, item );
}
void __fastcall oCNpc_EquipItem_2( oCNpc* _this, void* vtable, oCItem* item ) {
  Message::Info( "OnEquip #2" );
  Ivk_oCNpc_EquipItem_2( _this, item );
}
void __fastcall oCNpc_EquipItem_3( oCNpc* _this, void* vtable, oCItem* item ) {
  Message::Info( "OnEquip #3" );
  Ivk_oCNpc_EquipItem_3( _this, item );
}
 
Последнее редактирование:
Сверху Снизу