Добрый день.
В данном планируемом цикле статей я постараюсь объяснить основные моменты написания своих дополнений для клиентской части 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. Она описана в HLSDKengineAPIProxy.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.
Архив с проектом для Visual Studio 2010 можно скачать отсюда.
В настройках проекта во вкладках Отладка и События после построения замените D:SteamSteamAppscommonHalf-Life на путь, соответствующий вашим реалиям.
Если у вас нет Steam, то в качестве исполняемого файла вам следует указать Run_CS.exe, при этом, из-за особенности загрузки, вы не сможете сразу запустить отладку. С этой проблемой мы так же разберёмся в одной из следующих статей.
На этом пока всё. В следующей статье я расскажу про то, зачем в interface.cpp нужна функция Sys_GetFactory и что полезного можно получить с её помощью.
Автор: Chuvi