В данном планируемом цикле статей я постараюсь объяснить основные моменты написания своих дополнений для клиентской части 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 к следующему виду:
#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.
Зато загрузка игры нашем модулем, вроде как, должна под 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
#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
#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
То, что вы увидите будет выглядить примерно так:
Так как код hw.dll и hw.so большей частью одинаковый, то функциям в hw.dll можно задать нормальные имена, позаимствовав их из hw.so.
Посмотрите, по какому адресу находится 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/
Добавить комментарий