Skip to content

Instantly share code, notes, and snippets.

@nosoop
Last active August 6, 2023 18:10
Show Gist options
  • Save nosoop/df8f18de68c80004d2eb981ece1597d4 to your computer and use it in GitHub Desktop.
Save nosoop/df8f18de68c80004d2eb981ece1597d4 to your computer and use it in GitHub Desktop.
/**
* dhooks_gameconf_shim
* (c) 2023 nosoop
*
* DHooks stores the hook definitions in-extension. Because it's keyed globally, it has the
* side effect of conflicting if multiple game config files use the same name for different
* hook definitions, no matter which plugin is loading them.
*
* This issue is tracked here: https://github.com/alliedmodders/sourcemod/issues/1879
*
* This include file implements some functions to parse the gameconfig within the plugin and
* provide drop-in replacements for compatibility reasons.
*
* This currently does not cover full compatibility of the game config parsing logic, but should
* be sufficent for most use cases. Any unexpected values will raise errors, so they can be
* tracked down and fixed.
*
* To use this shim:
*
* 1. Call `ReadDHooksDefinitions` around the same time you call `LoadGameConfigFile` with the
* same file. You may also wish to rename the "Functions" section in the config file to a
* different name to prevent DHooks itself from processing the section;
* `ReadDHooksDefinitions` provides a parameter to customize the section read.
* 2. Replace every instance of `DHookCreateFromConf` with `GetDHooksDefinition`. If you are
* using the `FromConf` methods, replace such instances with `GetDHooksHookDefinition` and
* `GetDHooksDetourDefinition` depending on the type.
* 3. Call `ClearDHooksDefinitions` when you delete the gameconf handle and no longer need to
* call `GetDHooksDefinition`. This frees up the associated resources.
*
* Those are the only external-facing symbols.
*/
#include <dhooks>
/**
* Internal constants.
*/
#define MAX_DHOOKS_SDKCONF_NAME_LENGTH 128
#define MAX_DHOOKS_PARAM_NAME_LENGTH 64
/**
* Plugin-internal list of definitions.
*/
static StringMap s_DHooksDefinitions;
static char s_CurrentHookListSectionName[64];
enum DHookGameConfParseState {
GameConfParse_Root,
GameConfParse_GameList,
GameConfParse_Game,
GameConfParse_HookList,
GameConfParse_HookSetup,
GameConfParse_ParamList,
GameConfParse_ParamInfo,
}
/**
* Container for a DHooks argument definition (corresponding to an entry under the "arguments"
* section).
*/
enum struct DHookSetupParamDefinition {
char m_Name[MAX_DHOOKS_PARAM_NAME_LENGTH];
HookParamType m_ParamType;
int m_ParamSize;
DHookPassFlag m_PassFlags;
DHookRegister m_CustomRegister;
void Init(const char[] name) {
strcopy(this.m_Name, sizeof(DHookSetupParamDefinition::m_Name), name);
this.m_ParamSize = -1;
this.m_PassFlags = DHookPass_ByVal;
this.m_CustomRegister = DHookRegister_Default;
}
bool ParseKeyValue(const char[] key, const char[] value) {
if (StrEqual(key, "type")) {
return GetHookParamTypeFromString(value, this.m_ParamType);
} else if (StrEqual(key, "size")) {
int parsedSize = StringToInt(value, 0);
if (parsedSize < 1) {
return false;
}
this.m_ParamSize = parsedSize;
return true;
} else if (StrEqual(key, "flags")) {
return GetPassFlagsFromString(value, this.m_PassFlags);
} else if (StrEqual(key, "register")) {
return GetRegisterFromString(value, this.m_CustomRegister);
}
return false;
}
}
/**
* Container for a DHooks hook definition (corresponding to an entry under the "Functions"
* section).
*/
enum struct DHookSetupDefinition {
SDKFuncConfSource m_HookSource;
char m_ConfName[MAX_DHOOKS_SDKCONF_NAME_LENGTH];
HookType m_VirtualHookType;
CallingConvention m_DetourCallConv;
ThisPointerType m_ThisPtrType;
ReturnType m_ReturnType;
ArrayList m_ParamList;
void Init() {
this.m_ParamList = new ArrayList(sizeof(DHookSetupParamDefinition));
this.m_ConfName = "";
}
void Destroy() {
delete this.m_ParamList;
}
void AddParameter(const DHookSetupParamDefinition paramdef) {
// replace existing matching param by name or push it
for (int i, n = this.m_ParamList.Length; i < n; i++) {
DHookSetupParamDefinition internaldef;
this.m_ParamList.GetArray(i, internaldef, sizeof(internaldef));
if (StrEqual(internaldef.m_Name, paramdef.m_Name)) {
this.m_ParamList.SetArray(i, paramdef);
return;
}
}
this.m_ParamList.PushArray(paramdef);
}
bool ParseKeyValue(const char[] key, const char[] value) {
if (StrEqual(key, "signature")) {
this.m_HookSource = SDKConf_Signature;
strcopy(this.m_ConfName, sizeof(DHookSetupDefinition::m_ConfName), value);
return true;
} else if (StrEqual(key, "address")) {
this.m_HookSource = SDKConf_Address;
strcopy(this.m_ConfName, sizeof(DHookSetupDefinition::m_ConfName), value);
return true;
} else if (StrEqual(key, "offset")) {
this.m_HookSource = SDKConf_Virtual;
strcopy(this.m_ConfName, sizeof(DHookSetupDefinition::m_ConfName), value);
return true;
} else if (StrEqual(key, "callconv")) {
return GetCallConvFromString(value, this.m_DetourCallConv);
} else if (StrEqual(key, "hooktype")) {
return GetHookTypeFromString(value, this.m_VirtualHookType);
} else if (StrEqual(key, "return")) {
return GetReturnTypeFromString(value, this.m_ReturnType);
} else if (StrEqual(key, "this")) {
return GetThisPtrTypeFromString(value, this.m_ThisPtrType);
}
return false;
}
DHookSetup CreateHook(Handle gameconf) {
DHookSetup hook;
if (this.m_HookSource == SDKConf_Virtual) {
hook = DHookCreate(0, this.m_VirtualHookType, this.m_ReturnType,
this.m_ThisPtrType);
} else {
// address or signature
hook = DHookCreateDetour(Address_Null, this.m_DetourCallConv, this.m_ReturnType,
this.m_ThisPtrType);
}
if (!hook.SetFromConf(gameconf, this.m_HookSource, this.m_ConfName)) {
return null;
}
for (int i, n = this.m_ParamList.Length; i < n; i++) {
DHookSetupParamDefinition paramdef;
this.m_ParamList.GetArray(i, paramdef, sizeof(paramdef));
hook.AddParam(paramdef.m_ParamType, paramdef.m_ParamSize, paramdef.m_PassFlags,
paramdef.m_CustomRegister);
}
return hook;
}
}
/**
* Processes DHooks hook setup definitions from a game config file.
*
* @param file File to load. The path must be relative to the 'gamedata' folder and the
* extension should be omitted.
* @param section Section to read definitions from. By default, this reads from the same
* section as DHooks does, but may be changed to prevent DHooks from reading
* them itself.
* @return True if the file exists and was successfully parsed, false otherwise.
*/
stock bool ReadDHooksDefinitions(const char[] file, const char[] section = "Functions") {
if (!s_DHooksDefinitions) {
s_DHooksDefinitions = new StringMap();
}
// TODO create parser
SMCParser gameConfigParser = CreateGameConfParser();
char gameconf[PLATFORM_MAX_PATH];
BuildPath(Path_SM, gameconf, sizeof(gameconf), "gamedata/%s.txt", file);
if (!FileExists(gameconf)) {
LogError("Game config file %s not found", file);
return false;
}
strcopy(s_CurrentHookListSectionName, sizeof(s_CurrentHookListSectionName), section);
SMCError parseResult = gameConfigParser.ParseFile(gameconf);
delete gameConfigParser;
return parseResult == SMCError_Okay;
}
/**
* Constructs a DHookSetup instance from a parsed config file.
* Drop-in replacement for DHookCreateFromConf().
*/
stock DHookSetup GetDHooksDefinition(Handle gameconf, const char[] name) {
DHookSetupDefinition hookdef;
if (!s_DHooksDefinitions.GetArray(name, hookdef, sizeof(hookdef))) {
return null;
}
return hookdef.CreateHook(gameconf);
}
/**
* Constructs a DynamicHook instance from a parsed config file.
* Drop-in replacement for DynamicHook.FromConf(). Returns null if the game config entry was
* not specified as a virtual hook.
*/
stock DynamicHook GetDHooksHookDefinition(Handle gameconf, const char[] name) {
DHookSetupDefinition hookdef;
if (!s_DHooksDefinitions.GetArray(name, hookdef, sizeof(hookdef))) {
return null;
} else if (hookdef.m_HookSource != SDKConf_Virtual) {
return null;
}
return view_as<DynamicHook>(hookdef.CreateHook(gameconf));
}
/**
* Constructs a DynamicDetour instance from a parsed config file.
* Drop-in replacement for DynamicDetour.FromConf(). Returns null if the game config entry was
* not specified as a detour.
*/
stock DynamicDetour GetDHooksDetourDefinition(Handle gameconf, const char[] name) {
DHookSetupDefinition hookdef;
if (!s_DHooksDefinitions.GetArray(name, hookdef, sizeof(hookdef))) {
return null;
} else if (hookdef.m_HookSource != SDKConf_Address
&& hookdef.m_HookSource != SDKConf_Signature) {
return null;
}
return view_as<DynamicDetour>(hookdef.CreateHook(gameconf));
}
/**
* Clears the internal definition list. You should call this around the same time you close the
* corresponding gamedata handle.
*/
stock void ClearDHooksDefinitions() {
if (!s_DHooksDefinitions) {
return;
}
StringMapSnapshot definitionNames = s_DHooksDefinitions.Snapshot();
for (int i, n = definitionNames.Length; i < n; i++) {
char name[MAX_DHOOKS_SDKCONF_NAME_LENGTH];
definitionNames.GetKey(i, name, sizeof(name));
DHookSetupDefinition hookdef;
if (s_DHooksDefinitions.GetArray(name, hookdef, sizeof(hookdef))) {
hookdef.Destroy();
continue;
}
}
s_DHooksDefinitions.Clear();
}
/**
* Internal parsing logic.
*/
static SMCParser CreateGameConfParser() {
SMCParser gameConfigParser = new SMCParser();
gameConfigParser.OnStart = OnGameConfigStartParse;
gameConfigParser.OnEnd = OnGameConfigEndParse;
gameConfigParser.OnEnterSection = OnGameConfigEnterSection;
gameConfigParser.OnLeaveSection = OnGameConfigLeaveSection;
gameConfigParser.OnKeyValue = OnGameConfigKeyValue;
return gameConfigParser;
}
static DHookGameConfParseState s_ParseState = GameConfParse_Root;
static int s_nParseStateIgnoreNestedSections;
static char s_CurrentHookName[MAX_DHOOKS_SDKCONF_NAME_LENGTH];
static DHookSetupDefinition s_CurrentHookDefinition;
static DHookSetupParamDefinition s_CurrentParamDefinition;
static void OnGameConfigStartParse(SMCParser smc) {
s_ParseState = GameConfParse_Root;
s_nParseStateIgnoreNestedSections = 0;
s_CurrentHookName = "";
}
/**
* Push new parse state depending on the current section.
*/
static SMCResult OnGameConfigEnterSection(SMCParser smc, const char[] name, bool opt_quotes) {
/**
* If we're ignoring a parent section, increment and don't emit a change in parse state.
*/
if (s_nParseStateIgnoreNestedSections) {
s_nParseStateIgnoreNestedSections++;
return SMCParse_Continue;
}
switch (s_ParseState) {
case GameConfParse_Root: {
if (StrEqual(name, "Games")) {
s_ParseState = GameConfParse_GameList;
} else {
return ThrowSMCParseFailure("Entering unexpected section '%s'", name);
}
}
case GameConfParse_GameList: {
if (IsSupportedGameSection(name)) {
s_ParseState = GameConfParse_Game;
} else {
// ignore sections irrelevant to the game
s_nParseStateIgnoreNestedSections++;
}
}
case GameConfParse_Game: {
if (StrEqual(name, s_CurrentHookListSectionName)) {
s_ParseState = GameConfParse_HookList;
} else {
s_nParseStateIgnoreNestedSections++;
}
}
case GameConfParse_HookList: {
s_ParseState = GameConfParse_HookSetup;
// initialize current hook context and retrieve an existing definition if present
strcopy(s_CurrentHookName, sizeof(s_CurrentHookName), name);
s_CurrentHookDefinition.Init();
s_DHooksDefinitions.GetArray(name,
s_CurrentHookDefinition, sizeof(s_CurrentHookDefinition));
}
case GameConfParse_HookSetup: {
if (StrEqual(name, "arguments")) {
s_ParseState = GameConfParse_ParamList;
} else {
return ThrowSMCParseFailure("Entering unexpected section '%s'", name);
}
}
case GameConfParse_ParamList: {
s_ParseState = GameConfParse_ParamInfo;
s_CurrentParamDefinition.Init(name);
}
case GameConfParse_ParamInfo: {
// ignore further nested sections
s_nParseStateIgnoreNestedSections++;
}
default: {
return ThrowSMCParseFailure("Entering unexpected section '%s'", name);
}
}
return SMCParse_Continue;
}
/**
* Pop parse state and go back to previous one.
*/
static SMCResult OnGameConfigLeaveSection(SMCParser smc) {
/**
* If we're leaving an ignored section, decrement and don't emit a change in parse state.
*/
if (s_nParseStateIgnoreNestedSections) {
s_nParseStateIgnoreNestedSections--;
return SMCParse_Continue;
}
switch (s_ParseState) {
case GameConfParse_ParamInfo: {
s_ParseState = GameConfParse_ParamList;
s_CurrentHookDefinition.AddParameter(s_CurrentParamDefinition);
}
case GameConfParse_ParamList: {
s_ParseState = GameConfParse_HookSetup;
}
case GameConfParse_HookSetup: {
s_ParseState = GameConfParse_HookList;
// add current hook to internal list
s_DHooksDefinitions.SetArray(s_CurrentHookName,
s_CurrentHookDefinition, sizeof(s_CurrentHookDefinition));
}
case GameConfParse_HookList: {
s_ParseState = GameConfParse_Game;
}
case GameConfParse_Game: {
s_ParseState = GameConfParse_GameList;
}
case GameConfParse_GameList: {
s_ParseState = GameConfParse_Root;
}
default: {
return ThrowSMCParseFailure("Unhandled section leave");
}
}
return SMCParse_Continue;
}
static SMCResult OnGameConfigKeyValue(SMCParser smc, const char[] key, const char[] value,
bool key_quotes, bool value_quotes) {
/**
* Not in a section we care about.
*/
if (s_nParseStateIgnoreNestedSections) {
return SMCParse_Continue;
}
switch (s_ParseState) {
case GameConfParse_HookSetup: {
if (s_CurrentHookDefinition.ParseKeyValue(key, value)) {
return SMCParse_Continue;
}
}
case GameConfParse_ParamInfo: {
if (s_CurrentParamDefinition.ParseKeyValue(key, value)) {
return SMCParse_Continue;
}
}
}
return ThrowSMCParseFailure("Unexpected key / value pair '%s' / '%s'", key, value);
}
static void OnGameConfigEndParse(SMCParser smc, bool halted, bool failed) {
if (halted || failed) {
return;
}
if (s_ParseState != GameConfParse_Root) {
LogError("Parse state not at root on end parse");
}
}
static SMCResult ThrowSMCParseFailure(const char[] message, any ...) {
char buffer[512];
VFormat(buffer, sizeof(buffer), message, 2);
LogError("%s (parse state %d)", buffer, s_ParseState);
return SMCParse_HaltFail;
}
static bool IsSupportedGameSection(const char[] game) {
char gamedir[64];
if (GetGameFolderName(gamedir, sizeof(gamedir)) && StrEqual(game, gamedir)) {
return true;
}
return StrEqual(game, "#default");
}
static bool GetHookTypeFromString(const char[] value, HookType &type) {
if (StrEqual(value, "entity")) {
type = HookType_Entity;
} else if (StrEqual(value, "gamerules")) {
type = HookType_GameRules;
} else if (StrEqual(value, "raw")) {
type = HookType_Raw;
} else {
return false;
}
return true;
}
static bool GetThisPtrTypeFromString(const char[] value, ThisPointerType &type) {
if (StrEqual(value, "ignore")) {
type = ThisPointer_Ignore;
} else if (StrEqual(value, "entity")) {
type = ThisPointer_CBaseEntity;
} else if (StrEqual(value, "address")) {
type = ThisPointer_Address;
} else {
return false;
}
return true;
}
static bool GetCallConvFromString(const char[] value, CallingConvention &callconv) {
if (StrEqual(value, "cdecl")) {
callconv = CallConv_CDECL;
} else if (StrEqual(value, "thiscall")) {
callconv = CallConv_THISCALL;
} else if (StrEqual(value, "stdcall")) {
callconv = CallConv_STDCALL;
} else if (StrEqual(value, "fastcall")) {
callconv = CallConv_FASTCALL;
} else {
return false;
}
return true;
}
static bool GetReturnTypeFromString(const char[] value, ReturnType &type) {
if (StrEqual(value, "void")) {
type = ReturnType_Void;
} else if (StrEqual(value, "int")) {
type = ReturnType_Int;
} else if (StrEqual(value, "bool")) {
type = ReturnType_Bool;
} else if (StrEqual(value, "float")) {
type = ReturnType_Float;
} else if (StrEqual(value, "string")) {
type = ReturnType_String;
} else if (StrEqual(value, "stringptr")) {
type = ReturnType_StringPtr;
} else if (StrEqual(value, "charptr")) {
type = ReturnType_CharPtr;
} else if (StrEqual(value, "vector")) {
type = ReturnType_Vector;
} else if (StrEqual(value, "vectorptr")) {
type = ReturnType_VectorPtr;
} else if (StrEqual(value, "cbaseentity")) {
type = ReturnType_CBaseEntity;
} else if (StrEqual(value, "edict")) {
type = ReturnType_Edict;
} else {
type = ReturnType_Unknown;
}
return true;
}
static bool GetHookParamTypeFromString(const char[] value, HookParamType &type) {
if (StrEqual(value, "int")) {
type = HookParamType_Int;
} else if (StrEqual(value, "bool")) {
type = HookParamType_Bool;
} else if (StrEqual(value, "float")) {
type = HookParamType_Float;
} else if (StrEqual(value, "string")) {
type = HookParamType_String;
} else if (StrEqual(value, "stringptr")) {
type = HookParamType_StringPtr;
} else if (StrEqual(value, "charptr")) {
type = HookParamType_CharPtr;
} else if (StrEqual(value, "vectorptr")) {
type = HookParamType_VectorPtr;
} else if (StrEqual(value, "cbaseentity")) {
type = HookParamType_CBaseEntity;
} else if (StrEqual(value, "objectptr")) {
type = HookParamType_ObjectPtr;
} else if (StrEqual(value, "edict")) {
type = HookParamType_Edict;
} else if (StrEqual(value, "object")) {
type = HookParamType_Object;
} else {
type = HookParamType_Unknown;
}
return true;
}
static bool GetPassFlagsFromString(const char[] value, DHookPassFlag &flags) {
flags = view_as<DHookPassFlag>(0);
if (StrContains(value, "byval") != -1) {
flags |= DHookPass_ByVal;
}
if (StrContains(value, "byref") != -1) {
flags |= DHookPass_ByRef;
}
if (StrContains(value, "odtor") != -1) {
flags |= DHookPass_ODTOR;
}
if (StrContains(value, "octor") != -1) {
flags |= DHookPass_OCTOR;
}
if (StrContains(value, "oassignop") != -1) {
flags |= DHookPass_OASSIGNOP;
}
// "ocopyctor" and "ounalign" do not have flags defined in SM
return true;
}
static bool GetRegisterFromString(const char[] value, DHookRegister &register) {
if (StrEqual(value, "al")) {
register = DHookRegister_AL;
} else if (StrEqual(value, "cl")) {
register = DHookRegister_CL;
} else if (StrEqual(value, "dl")) {
register = DHookRegister_DL;
} else if (StrEqual(value, "bl")) {
register = DHookRegister_BL;
} else if (StrEqual(value, "ah")) {
register = DHookRegister_AH;
} else if (StrEqual(value, "ch")) {
register = DHookRegister_CH;
} else if (StrEqual(value, "dh")) {
register = DHookRegister_DH;
} else if (StrEqual(value, "bh")) {
register = DHookRegister_BH;
} else if (StrEqual(value, "eax")) {
register = DHookRegister_EAX;
} else if (StrEqual(value, "ecx")) {
register = DHookRegister_ECX;
} else if (StrEqual(value, "edx")) {
register = DHookRegister_EDX;
} else if (StrEqual(value, "ebx")) {
register = DHookRegister_EBX;
} else if (StrEqual(value, "esp")) {
register = DHookRegister_ESP;
} else if (StrEqual(value, "ebp")) {
register = DHookRegister_EBP;
} else if (StrEqual(value, "esi")) {
register = DHookRegister_ESI;
} else if (StrEqual(value, "edi")) {
register = DHookRegister_EDI;
} else if (StrEqual(value, "xmm0")) {
register = DHookRegister_XMM0;
} else if (StrEqual(value, "xmm1")) {
register = DHookRegister_XMM1;
} else if (StrEqual(value, "xmm2")) {
register = DHookRegister_XMM2;
} else if (StrEqual(value, "xmm3")) {
register = DHookRegister_XMM3;
} else if (StrEqual(value, "xmm4")) {
register = DHookRegister_XMM4;
} else if (StrEqual(value, "xmm5")) {
register = DHookRegister_XMM5;
} else if (StrEqual(value, "xmm6")) {
register = DHookRegister_XMM6;
} else if (StrEqual(value, "xmm7")) {
register = DHookRegister_XMM7;
} else if (StrEqual(value, "st0")) {
register = DHookRegister_ST0;
} else {
/**
* The following registers are processed by the extension, but don't have SP
* equivalents:
* - [asdb]x, sp, bp, si, di
* - mm[0-7]
* - [csdefg]s
*/
return false;
}
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment