- PVSM.RU - https://www.pvsm.ru -
Добрый день.
В данном планируемом цикле статей я постараюсь объяснить основные моменты написания своих дополнений для клиентской части GoldSrc игр. В качестве «подопытного» будем использовать игру Counter Strike 1.6, хотя, этот модуль, по-идее, должен так же работать и в Half-Life и в других играх на этом же движке.
Что вам понадобится:
Создайте новый проект Win32->dll, подключите к этому проекту следующие директории из HLSDK:
Советую создать в проекте директорию /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/
Как наш модуль будет загружаться в игру?
Как наш модуль будет загружаться в игру? Всё просто, GoldSrc использует библиотеку mss32.dll, которая подгружает все .asi-файлы, находящиеся в корневой директории игры в качестве дополнительных модулей. Эти .asi-файлы, по факту, ни что иное как обычные .dll-ки.
Поэтому, в настройках проекта, в качестве конечного расширения поставьте не .dll, а .asi.
Если вы на данном этапе попробуете скомпилировать модуль и закинуть его в директорию 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, которую вы можете скачать отсюда [4].
И там и там найдите строку 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 можно скачать отсюда [4].
В настройках проекта во вкладках Отладка и События после построения замените D:SteamSteamAppscommonHalf-Life на путь, соответствующий вашим реалиям.
Если у вас нет Steam, то в качестве исполняемого файла вам следует указать Run_CS.exe, при этом, из-за особенности загрузки, вы не сможете сразу запустить отладку. С этой проблемой мы так же разберёмся в одной из следующих статей.
На этом пока всё. В следующей статье я расскажу про то, зачем в interface.cpp нужна функция Sys_GetFactory и что полезного можно получить с её помощью.
Автор: Chuvi
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/46170
Ссылки в тексте:
[1] здесь: http://forum.csmania.ru/viewtopic.php?f=102&t=38443&start=45
[2] здесь: http://store.steampowered.com/app/10/?l=russian
[3] HLSDK: https://github.com/ValveSoftware/halflife
[4] отсюда: http://cheatmakers.ru/habrahabr/part1/hw_6153.so.zip
[5] Источник: http://habrahabr.ru/post/198334/
Нажмите здесь для печати.