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

в 7:24, , рубрики: game development, Программирование, метки:

Добрый день.
В данном планируемом цикле статей я постараюсь объяснить основные моменты написания своих дополнений для клиентской части 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. Она описана в HLSDKengineAPIProxy.h и в ней есть много чего полезного.
Для начала нужно её найти. По-хорошему, поиск нужных элементов нужно как-то автоматизировать. Однако, я пока не представляю, как искать структуру по сигнатуре или по каким-нибудь другим параметрам. Если мне кто-нибудь это объяснит, буду признателен. :)
Для поиска cl_enginefuncs_t воспользуемся IDA Pro, причём, желательно, сразу двумя.
Откройте hw.dll, который вы найдёте в своей директории Half-Life. По окончании декомпиляции перебазируйте модуль на 0x40000000. Это нужно для более удобного поиска адреса структуры. Для этого откройте Edit->Segments->Rebase Program, убедитесь что поставлены оба чекбокса и переключатель стоит на ImageBase и впишите в Value 0x40000000.
Теперь откройте hw.so, которую вы можете скачать отсюда.
И там и там найдите строку ScreenShake
То, что вы увидите будет выглядить примерно так:

Картинка 275КБ

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

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

Картинка 193КБ

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

Посмотрите, по какому адресу находится 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js