Пишем свой модуль для клиентской части Counter Strike 1.6. Часть 1-я. «Организационные моменты»

от автора

Добрый день.
В данном планируемом цикле статей я постараюсь объяснить основные моменты написания своих дополнений для клиентской части GoldSrc игр. В качестве «подопытного» будем использовать игру Counter Strike 1.6, хотя, этот модуль, по-идее, должен так же работать и в Half-Life и в других играх на этом же движке.

Что вам понадобится:

  • Сам клиент Counter Strike, желательно последних версий. Если у вас нет Steam, можно раздобыть здесь или купить здесь.
  • Желательно так же заполучить эту же версию клиента для Linux или MacOs (или попросить скинуть кого-нибудь hw.so или hw.dylib из неё. А лучше всю директорию Half-Life целиком)
  • HLSDK
  • Так же нам понадобится IDA PRO
  • Какая-нибудь среда разработки, например, Visual Studio.

Основные моменты

Создайте новый проект Win32->dll, подключите к этому проекту следующие директории из HLSDK:

  • cl_dll
  • common
  • dlls
  • engine
  • game_shared
  • pm_shared
  • public

Советую создать в проекте директорию /include/HLSDK и скопировать эти директории туда.
Чуть не забыл. Пройдитесь массовым поиском по HLSDK (например, с помощью Notepad++), и замените HSPRITE в SptiteHandle_t, ибо 10-я студия на HSPRITE ругается. При замене не забудьте поставить чекбокс «Учитывать регистр».

Приведите stdafx.h к следующему виду:

stdafx.h

#pragma once #ifdef WIN32 #define WIN32_LEAN_AND_MEAN  #include <Windows.h> #else #ifndef LINUX #define LINUX #endif #ifndef linux #define linux #endif #endif  #ifdef _WIN32 // Used for dll exporting and importing #define  DLLEXPORT   extern "C" __declspec( dllexport )  #define  DLLIMPORT   extern "C" __declspec( dllimport )  // Can't use extern "C" when DLL exporting a class #define  DLL_CLASS_EXPORT   __declspec( dllexport )  #define  DLL_CLASS_IMPORT   __declspec( dllimport )  // Can't use extern "C" when DLL exporting a global #define  DLL_GLOBAL_EXPORT   extern __declspec( dllexport )  #define  DLL_GLOBAL_IMPORT   extern __declspec( dllimport ) #elif defined _LINUX  // Used for dll exporting and importing #define  DLLEXPORT   extern "C"  #define  DLLIMPORT   extern "C"   // Can't use extern "C" when DLL exporting a class #define  DLL_CLASS_EXPORT    #define  DLL_CLASS_IMPORT    // Can't use extern "C" when DLL exporting a global #define  DLL_GLOBAL_EXPORT   extern #define  DLL_GLOBAL_IMPORT   extern  #else #error "Unsupported Platform." #endif  #include <wrect.h> #include <cl_dll.h> #include <in_defs.h>  #include <cdll_int.h> #include <cl_entity.h> #include <com_model.h> #include <cvardef.h> #include <entity_state.h> #include <entity_types.h> #include <event_args.h> #include <net_api.h> #include <r_studioint.h> #include <pm_defs.h> #include <r_efx.h> #include <com_model.h> #include <ref_params.h> #include <studio_event.h> #include <event_api.h> #include <screenfade.h> #include <demo_api.h> #include <triangleapi.h> #include <ivoicetweak.h> #include <con_nprint.h> //Interfaces #include <interface.h> 

Попробуйте это безобразие скомпилировать. Если скомпилировалось — идём дальше.
Со временем эта «основа» будет «обрастать» различными дополнениями и изменениями, но пока оставим всё как есть.
Совет скопировать всё в /include/HLSDK был дан не случайно. В следующей статье нам понадобятся заголовки metamod-a, и было бы неплохо их поместить в /include/metamod/

Загрузка модуля

Как наш модуль будет загружаться в игру?

  • Вариант первый, суровый — иньекция DLL. Не рассматриваем ввиду чрезмерной суровости.
  • Вариант второй, более простой — игрушка сама подцепит наш модуль.
  • Вариант третий, о котором я планирую рассказать несколько позже — наш модуль сам запустит игру, заменив собой hl.exe

Как наш модуль будет загружаться в игру? Всё просто, GoldSrc использует библиотеку mss32.dll, которая подгружает все .asi-файлы, находящиеся в корневой директории игры в качестве дополнительных модулей. Эти .asi-файлы, по факту, ни что иное как обычные .dll-ки.
Поэтому, в настройках проекта, в качестве конечного расширения поставьте не .dll, а .asi.

.asi под Linux

.asi-модуль под линуксом это нечто странное и не совсем понятное (они там тоже есть и тоже работают, но у них в заголовках не ELF а MZ….PE. Кто-нибудь, объясните пожалуйста, как такое возможно?), поэтому вариант "asi под Linux" я предпочитаю оставить в покое.
Зато загрузка игры нашем модулем, вроде как, должна под Linux заработать. Поэтому, по возможности, старайтесь делать код кроссплатформенным.

Если вы на данном этапе попробуете скомпилировать модуль и закинуть его в директорию Half-Life, поставив в DllMain MessageBox-ы на загрузку и на выгрузку, вы увидите, что модуль выгрузится сразу после загрузки. Причина заключается в том, что mss32.dll выгружает модуль, если в нём нет экспортируемой функции RIB_Main.
Если честно, то asi-модули для более старых версий GoldSrc, например, у версии 4554, спокойно себе грузились через DllMain, но в версии 6027 (эта та, с которой я начал эти «копания»), уже использовалась функция Rib_Main

Создайте в проекте 2 файла: AsiMain.cpp и AsiMain.h
В функцию RibMain передаются 5 параметров, из них 3- указатели на функции, использующиеся для регистрации провайдеров, которых у нас не будет, поэтому, по большому счёту, их можно заменить на void*. Однако я не оставляю надежды когда-нибудь разобраться с использованием этих модулей «по назначению», поэтому, давайте объявим функцию так, как она должна объявляться.
Для начала, заполните AsiMain.h

AsiMain.h

#ifdef _WIN32 #define AILCALL        __stdcall #else #define AILCALL #endif  #ifndef C8 #define C8 char #endif  #ifndef U32 #define U32 unsigned int #endif  #ifndef S32 #define S32 signed int #endif  #ifndef UINTa #define UINTa unsigned int #endif  typedef U32 HPROVIDER; typedef S32 RIBRESULT;  typedef enum {    RIB_NONE = 0, // No type    RIB_CUSTOM,   // Used for pointers to application-specific structures    RIB_DEC,      // Used for 32-bit integer values to be reported in decimal    RIB_HEX,      // Used for 32-bit integer values to be reported in hex    RIB_FLOAT,    // Used for 32-bit single-precision FP values    RIB_PERCENT,  // Used for 32-bit single-precision FP values to be reported as percentages    RIB_BOOL,     // Used for Boolean-constrained integer values to be reported as TRUE or FALSE    RIB_STRING,   // Used for pointers to null-terminated ASCII strings    RIB_READONLY = 0x80000000  // Property is read-only } RIB_DATA_SUBTYPE;  typedef enum {    RIB_FUNCTION = 0,    RIB_PROPERTY       // Property: read-only or read-write data type } RIB_ENTRY_TYPE;  typedef struct {    RIB_ENTRY_TYPE   type;        // See list above    C8 FAR          *entry_name;  // Name of desired function or property    UINTa            token;       // Function pointer or property token    RIB_DATA_SUBTYPE subtype;     // Property subtype } RIB_INTERFACE_ENTRY;   typedef HPROVIDER	(*RIB_alloc_provider_handle_ptr) (long module); typedef RIBRESULT	(*RIB_register_interface_ptr) (HPROVIDER  provider, C8 const FAR *interface_name, S32 entry_count, RIB_INTERFACE_ENTRY const FAR *rlist); typedef RIBRESULT	(*RIB_unregister_interface_ptr)  (HPROVIDER  provider, C8 const FAR *interface_name, S32 entry_count, RIB_INTERFACE_ENTRY const FAR *rlist);  EXTERN_C DLLEXPORT S32 AILCALL RIB_Main(HPROVIDER provider_handle, 										U32 up_down, 										RIB_alloc_provider_handle_ptr RIB_alloc_provider_handle, 										RIB_register_interface_ptr RIB_register_interface, 										RIB_unregister_interface_ptr RIB_unregister_interface 										); 

По факту, в RibMain нас интересует только 1 параметр- up_down. Эта функция вызывается 2 раза: при загрузке игры и при штатном завершении её работы.
Если up_down равен нулю, то модуль выгружается. Иначе — загружается.
Небольшой хинт: Если DllMain говорит о том, что библиотека выгружается, но RibMain с параметром up_down равным нулю не был вызван, значит игра завершилась нештатным способом. Тобишь, скорее всего, вылетела из-за какой-нибудь ошибки.

Теперь нужно заполнить AsiMain.cpp

AsiMain.cpp

#include "stdafx.h" #include "AsiMain.h"  EXTERN_C DLLEXPORT S32 AILCALL RIB_Main(HPROVIDER provider_handle, 										U32 up_down, 										RIB_alloc_provider_handle_ptr RIB_alloc_provider_handle, 										RIB_register_interface_ptr RIB_register_interface, 										RIB_unregister_interface_ptr RIB_unregister_interface 										) {     if(up_down)     { 		//эта часть кода вызывается при загрузке модуля. 	} 	else 	{ 		//Эта часть кода выполняется при завершении работы модуля. 	} 	return 1; } 

Ура. Asi-модуль, который ничего не делает, готов.
Но хотелось бы, чтобы он что-то делал.
Давайте попробуем воспользоваться структурой cl_enginefuncs_t. Она описана в HLSDK\engine\APIProxy.h и в ней есть много чего полезного.
Для начала нужно её найти. По-хорошему, поиск нужных элементов нужно как-то автоматизировать. Однако, я пока не представляю, как искать структуру по сигнатуре или по каким-нибудь другим параметрам. Если мне кто-нибудь это объяснит, буду признателен. 🙂
Для поиска cl_enginefuncs_t воспользуемся IDA Pro, причём, желательно, сразу двумя.
Откройте hw.dll, который вы найдёте в своей директории Half-Life. По окончании декомпиляции перебазируйте модуль на 0x40000000. Это нужно для более удобного поиска адреса структуры. Для этого откройте Edit->Segments->Rebase Program, убедитесь что поставлены оба чекбокса и переключатель стоит на ImageBase и впишите в Value 0x40000000.
Теперь откройте hw.so, которую вы можете скачать отсюда.
И там и там найдите строку ScreenShake
То, что вы увидите будет выглядить примерно так:

Картинка 275КБ

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

Картинка 193КБ

Посмотрите, по какому адресу находится cl_enginefuncs в hw.dll.
У меня это 0x40134260. Так как мы базировали модуль по адресу 0x40000000, значит смещение этой структуры будет 0x134260

Вот теперь можно что-нибудь сделать.
Объявите в AsiMain.cpp, в глобальной области

cl_enginefunc_t *cl_enginefuncs; 

Там же, перед RibMain создайте функцию

void HabraHello() { 	cl_enginefuncs->Con_Printf("Hello, Habrahabr!\n"); } 

В код RibMain, который выполняется при запуске допишите

		HANDLE hw=LoadLibraryA("hw.dll"); 		cl_enginefuncs=(cl_enginefunc_t*)((unsigned long)hw+0x134260); 		cl_enginefuncs->pfnAddCommand("SayHello",HabraHello); //Так делать не совсем правильно и совсем не кроссплатформенно, но в качестве "Hello World-a", пожалуй, сойдёт. //В дальнейшем, для загрузки библиотек, мы будем использовать функции, реализованные в interface.cpp, предварительно немного их переделав. 

Теперь при вводе в консоль команды SayHello будет выводиться Hello, Habrahabr.

Архив с проектом можно скачать отсюда.
В настройках проекта во вкладках Отладка и События после построения замените D:\Steam\SteamApps\common\Half-Life на путь, соответствующий вашим реалиям.
Если у вас нет Steam, то в качестве исполняемого файла вам следует указать Run_CS.exe, при этом, из-за особенности загрузки, вы не сможете сразу запустить отладку. С этой проблемой мы так же разберёмся в одной из следующих статей.

На этом пока всё. В следующей статье я расскажу про то, зачем в interface.cpp нужна функция Sys_GetFactory и что полезного можно получить с её помощью.

ссылка на оригинал статьи http://habrahabr.ru/post/198334/