From 4b2ba10300f878470187854786f2d16fbe43c0bd Mon Sep 17 00:00:00 2001 From: Arkshine Date: Tue, 23 Jun 2015 19:39:01 +0200 Subject: [PATCH 1/3] Gameconfig: Add game configs parser --- amxmodx/AMBuilder | 2 + amxmodx/CGameConfigs.cpp | 894 ++++++++++++++++++++++ amxmodx/CGameConfigs.h | 167 ++++ amxmodx/CLibrarySys.cpp | 26 + amxmodx/CLibrarySys.h | 11 + amxmodx/msvc12/amxmodx_mm.vcxproj | 4 + amxmodx/msvc12/amxmodx_mm.vcxproj.filters | 18 +- public/IGameConfigs.h | 139 ++++ public/memtools/MemoryUtils.cpp | 9 +- 9 files changed, 1266 insertions(+), 4 deletions(-) create mode 100644 amxmodx/CGameConfigs.cpp create mode 100644 amxmodx/CGameConfigs.h create mode 100644 public/IGameConfigs.h diff --git a/amxmodx/AMBuilder b/amxmodx/AMBuilder index 6f1bd3b0..53075490 100644 --- a/amxmodx/AMBuilder +++ b/amxmodx/AMBuilder @@ -91,9 +91,11 @@ binary.sources = [ 'textparse.cpp', 'CvarManager.cpp', 'cvars.cpp', + '../public/memtools/MemoryUtils.cpp', '../public/memtools/CDetour/detours.cpp', '../public/memtools/CDetour/asm/asm.c', 'CLibrarySys.cpp', + 'CGameConfigs.cpp', ] if builder.target_platform == 'windows': diff --git a/amxmodx/CGameConfigs.cpp b/amxmodx/CGameConfigs.cpp new file mode 100644 index 00000000..2b0e9421 --- /dev/null +++ b/amxmodx/CGameConfigs.cpp @@ -0,0 +1,894 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +#include "CGameConfigs.h" +#include +#include + +CGameConfigManager ConfigManager; +static CGameMasterReader MasterReader; + +// +// GAME CONFIG +// + +enum +{ + PSTATE_NONE, + PSTATE_GAMES, + PSTATE_GAMEDEFS, + PSTATE_GAMEDEFS_OFFSETS, + PSTATE_GAMEDEFS_OFFSETS_OFFSET, + PSTATE_GAMEDEFS_KEYS, + PSTATE_GAMEDEFS_SUPPORTED, + PSTATE_GAMEDEFS_SIGNATURES, + PSTATE_GAMEDEFS_SIGNATURES_SIG, + PSTATE_GAMEDEFS_CUSTOM, + PSTATE_GAMEDEFS_ADDRESSES, + PSTATE_GAMEDEFS_ADDRESSES_ADDRESS, + PSTATE_GAMEDEFS_ADDRESSES_ADDRESS_READ, +}; + +struct TempSigInfo +{ + void Reset() + { + library[0] = '\0'; + signature[0] = '\0'; + } + + char signature[1024]; + char library[64]; + +} TempSig; + +static char ParseEngine[32]; + +static bool DoesGameMatch(const char *value) +{ + return g_mod_name.compare(value) == 0; +} + +static bool DoesEngineMatch(const char* value) +{ + return strcmp(ParseEngine, value) == 0; +} + +CGameConfig::CGameConfig(const char *path) +{ + strncopy(m_File, path, sizeof(m_File)); + strncopy(ParseEngine, IS_DEDICATED_SERVER() ? "engine_ds" : "engine_ls", sizeof(ParseEngine)); + + m_CustomLevel = 0; + m_CustomHandler = nullptr; +} + +CGameConfig::~CGameConfig() +{ + ConfigManager.RemoveCachedConfig(this); +} + +SMCResult CGameConfig::ReadSMC_NewSection(const SMCStates *states, const char *name) +{ + if (m_IgnoreLevel) + { + m_IgnoreLevel++; + return SMCResult_Continue; + } + + switch (m_ParseState) + { + case PSTATE_NONE: + { + if (strcmp(name, "Games") == 0) + { + m_ParseState = PSTATE_GAMES; + } + else + { + m_IgnoreLevel++; + } + break; + } + case PSTATE_GAMES: + { + if (strcmp(name, "*") == 0 || strcmp(name, "#default") == 0 || DoesGameMatch(name)) + { + m_ShouldBeReadingDefault = true; + strncopy(m_Game, name, sizeof(m_Game)); + + m_ParseState = PSTATE_GAMEDEFS; + } + else + { + m_IgnoreLevel++; + } + break; + } + case PSTATE_GAMEDEFS: + { + if (strcmp(name, "Offsets") == 0) + { + m_ParseState = PSTATE_GAMEDEFS_OFFSETS; + } + else if (strcmp(name, "Keys") == 0) + { + m_ParseState = PSTATE_GAMEDEFS_KEYS; + } + else if (strcmp(name, "#supported") == 0 && strcmp(m_Game, "#default") == 0) + { + m_ParseState = PSTATE_GAMEDEFS_SUPPORTED; + + m_ShouldBeReadingDefault = false; + m_HadGame = false; + m_MatchedGame = false; + m_HadEngine = false; + m_MatchedEngine = false; + } + else if (strcmp(name, "Signatures") == 0) + { + m_ParseState = PSTATE_GAMEDEFS_SIGNATURES; + } + else if (strcmp(name, "Addresses") == 0) + { + m_ParseState = PSTATE_GAMEDEFS_ADDRESSES; + } + else + { + if (ConfigManager.m_customHandlers.retrieve(name, &m_CustomHandler)) + { + m_CustomLevel = 0; + m_ParseState = PSTATE_GAMEDEFS_CUSTOM; + m_CustomHandler->ReadSMC_ParseStart(); + break; + } + + m_IgnoreLevel++; + } + break; + } + case PSTATE_GAMEDEFS_OFFSETS: + { + m_Class[0] = '\0'; + + strncopy(m_Offset, name, sizeof(m_Offset)); + + m_ParseState = PSTATE_GAMEDEFS_OFFSETS_OFFSET; + m_MatchedPlatform = false; + break; + } + case PSTATE_GAMEDEFS_SIGNATURES: + { + strncopy(m_Offset, name, sizeof(m_Offset)); + TempSig.Reset(); + + m_ParseState = PSTATE_GAMEDEFS_SIGNATURES_SIG; + m_MatchedPlatform = false; + break; + } + case PSTATE_GAMEDEFS_CUSTOM: + { + m_CustomLevel++; + return m_CustomHandler->ReadSMC_NewSection(states, name); + break; + } + case PSTATE_GAMEDEFS_ADDRESSES: + { + m_Address[0] = '\0'; + m_AddressSignature[0] = '\0'; + m_AddressReadCount = 0; + + strncopy(m_Address, name, sizeof(m_Address)); + + m_ParseState = PSTATE_GAMEDEFS_ADDRESSES_ADDRESS; + break; + } + case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS: + { + if (g_LibSys.DoesPlatformMatch(name)) + { + m_ParseState = PSTATE_GAMEDEFS_ADDRESSES_ADDRESS_READ; + } + else + { + if (strcmp(name, PLATFORM_LINUX_NAME) != 0 && strcmp(name, PLATFORM_WINDOWNS_NAME) != 0 && strcmp(name, PLATFORM_MAC_NAME) != 0) + { + AMXXLOG_Error("Error while parsing Address section for \"%s\" (%s):", m_Address, m_CurrentPath); + AMXXLOG_Error("Unrecognized platform \"%s\"", name); + } + + m_IgnoreLevel = 1; + } + break; + } + default: + { + m_IgnoreLevel++; + break; + } + } + + return SMCResult_Continue; +} + +SMCResult CGameConfig::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) +{ + if (m_IgnoreLevel) + { + return SMCResult_Continue; + } + + switch (m_ParseState) + { + case PSTATE_GAMEDEFS_OFFSETS_OFFSET: + { + if (strcmp(key, "class") == 0) + { + strncopy(m_Class, value, sizeof(m_Class)); + } + else if (g_LibSys.IsPlatformCompatible(key, &m_MatchedPlatform)) + { + if (m_Class[0]) + { + auto ic = m_OffsetsByClass.findForAdd(m_Class); + + if (ic.found()) + { + ic->value->list.replace(m_Offset, atoi(value)); + } + else if (m_OffsetsByClass.add(ic, m_Class)) + { + ic->value = new OffsetClass; + ic->value->list.insert(m_Offset, atoi(value)); + } + } + else + { + m_Offsets.replace(m_Offset, atoi(value)); + } + } + break; + } + case PSTATE_GAMEDEFS_KEYS: + { + ke::AString vstr(value); + m_Keys.replace(key, ke::Move(vstr)); + break; + } + case PSTATE_GAMEDEFS_SUPPORTED: + { + if (strcmp(key, "game") == 0) + { + m_HadGame = true; + + if (DoesGameMatch(value)) + { + m_MatchedGame = true; + } + + if ((!m_HadEngine && m_MatchedGame) || (m_MatchedEngine && m_MatchedGame)) + { + m_ShouldBeReadingDefault = true; + } + } + else if (strcmp(key, "engine") == 0) + { + m_HadEngine = true; + + if (DoesEngineMatch(value)) + { + m_MatchedEngine = true; + } + + if ((!m_HadGame && m_MatchedEngine) || (m_MatchedGame && m_MatchedEngine)) + { + m_ShouldBeReadingDefault = true; + } + } + break; + } + case PSTATE_GAMEDEFS_SIGNATURES_SIG: + { + if (g_LibSys.IsPlatformCompatible(key, &m_MatchedPlatform)) + { + strncopy(TempSig.signature, value, sizeof(TempSig.signature)); + } + else if (strcmp(key, "library") == 0) + { + strncopy(TempSig.library, value, sizeof(TempSig.library)); + } + break; + } + case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS: + case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS_READ: + { + if (strcmp(key, "read") == 0) + { + int limit = sizeof(m_AddressRead) / sizeof(m_AddressRead[0]); + + if (m_AddressReadCount < limit) + { + m_AddressRead[m_AddressReadCount] = atoi(value); + m_AddressReadCount++; + } + else + { + AMXXLOG_Error("[SM] Error parsing Address \"%s\", does not support more than %d read offsets (gameconf \"%s\")", + m_Address, limit, m_CurrentPath); + } + } + else if (strcmp(key, "signature") == 0) + { + strncopy(m_AddressSignature, value, sizeof(m_AddressSignature)); + } + break; + } + case PSTATE_GAMEDEFS_CUSTOM: + { + return m_CustomHandler->ReadSMC_KeyValue(states, key, value); + } + } + + return SMCResult_Continue; +} + +SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) +{ + if (m_IgnoreLevel) + { + m_IgnoreLevel--; + return SMCResult_Continue; + } + + if (m_CustomLevel) + { + m_CustomLevel--; + m_CustomHandler->ReadSMC_LeavingSection(states); + return SMCResult_Continue; + } + + switch (m_ParseState) + { + case PSTATE_GAMES: + { + m_ParseState = PSTATE_NONE; + break; + } + case PSTATE_GAMEDEFS: + { + m_ParseState = PSTATE_GAMES; + break; + } + case PSTATE_GAMEDEFS_CUSTOM: + { + m_ParseState = PSTATE_GAMEDEFS; + m_CustomHandler->ReadSMC_ParseEnd(false, false); + break; + } + case PSTATE_GAMEDEFS_KEYS: + case PSTATE_GAMEDEFS_OFFSETS: + { + m_ParseState = PSTATE_GAMEDEFS; + break; + } + case PSTATE_GAMEDEFS_OFFSETS_OFFSET: + { + m_ParseState = PSTATE_GAMEDEFS_OFFSETS; + break; + } + case PSTATE_GAMEDEFS_SUPPORTED: + { + if (!m_ShouldBeReadingDefault) + { + m_IgnoreLevel = 1; + m_ParseState = PSTATE_GAMES; + } + else + { + m_ParseState = PSTATE_GAMEDEFS; + } + break; + } + case PSTATE_GAMEDEFS_SIGNATURES: + { + m_ParseState = PSTATE_GAMEDEFS; + break; + } + case PSTATE_GAMEDEFS_SIGNATURES_SIG: + { + if (TempSig.library[0] == '\0') + { + strncopy(TempSig.library, "server", sizeof(TempSig.library)); + } + + void *addressInBase = nullptr; + + if (strcmp(TempSig.library, "server") == 0) + { + addressInBase = reinterpret_cast(MDLL_Spawn); + } + else if (strcmp(TempSig.library, "engine") == 0) + { + addressInBase = reinterpret_cast(gpGlobals); + } + + void *finalAddress = nullptr; + + if (!addressInBase) + { + AMXXLOG_Error("Unrecognized library \"%s\" (gameconf \"%s\")", TempSig.library, m_CurrentPath); + } + else if (TempSig.signature[0]) + { + if (TempSig.signature[0] == '@') + { +#if defined PLATFORM_WINDOWS + MEMORY_BASIC_INFORMATION mem; + + if (VirtualQuery(addressInBase, &mem, sizeof(mem))) + { + finalAddress = g_MemUtils.ResolveSymbol(mem.AllocationBase, &TempSig.signature[1]); + } + else + { + AMXXLOG_Error("Unable to find library \"%s\" in memory (gameconf \"%s\")", TempSig.library, m_File); + } + +#elif defined PLATFORM_POSIX + Dl_info info; + + if (dladdr(addressInBase, &info) != 0) + { + void *handle = dlopen(info.dli_fname, RTLD_NOW); + + if (handle) + { + finalAddress = g_MemUtils.ResolveSymbol(handle, &TempSig.signature[1]); + dlclose(handle); + } + else + { + AMXXLOG_Error("Unable to load library \"%s\" (gameconf \"%s\")", TempSig.library, m_File); + } + } + else + { + AMXXLOG_Error("Unable to find library \"%s\" in memory (gameconf \"%s\")", TempSig.library, m_File); + } +#endif + } + + if (!finalAddress) + { + finalAddress = g_MemUtils.DecodeAndFindPattern(addressInBase, TempSig.signature); + } + + m_Sigs.replace(m_Offset, finalAddress); + } + + m_ParseState = PSTATE_GAMEDEFS_SIGNATURES; + break; + } + case PSTATE_GAMEDEFS_ADDRESSES: + { + m_ParseState = PSTATE_GAMEDEFS; + break; + } + case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS: + { + m_ParseState = PSTATE_GAMEDEFS_ADDRESSES; + + if (m_Address[0] != '\0' && m_AddressSignature[0] != '\0') + { + AddressConf addrConf(m_AddressSignature, sizeof(m_AddressSignature), m_AddressReadCount, m_AddressRead); + m_Addresses.replace(m_Address, addrConf); + } + + break; + } + case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS_READ: + { + m_ParseState = PSTATE_GAMEDEFS_ADDRESSES_ADDRESS; + break; + } + } + + return SMCResult_Continue; +} + +bool CGameConfig::Reparse(char *error, size_t maxlength) +{ + m_Offsets.clear(); + m_OffsetsByClass.clear(); + m_Keys.clear(); + m_Addresses.clear(); + + char path[PLATFORM_MAX_PATH]; + const char *dataDir = get_localinfo("amxx_datadir", "addons/amxmodx/data"); + + build_pathname_r(path, sizeof(path), "%s/gamedata/%s/master.games.txt", dataDir, m_File); + + if (!g_LibSys.PathExists(path)) + { + return false; + } + + SMCError err; + SMCStates state = { 0, 0 }; + + ke::Vector fileList; + MasterReader.m_FileList = &fileList; + + err = textparsers->ParseSMCFile(path, &MasterReader, &state, error, maxlength); + + if (err != SMCError_Okay) + { + const char *msg = textparsers->GetSMCErrorString(err); + + AMXXLOG_Error("Error parsing master gameconf file \"%s\":", path); + AMXXLOG_Error("Error %d on line %d, col %d: %s", err, state.line, state.col, msg ? msg : "Unknown error"); + + return false; + } + + for (size_t i = 0; i < fileList.length(); ++i) + { + g_LibSys.PathFormat(path, sizeof(path), "%s/%s", m_File, fileList[i].chars()); + + if (!EnterFile(path, error, maxlength)) + { + return false; + } + } + + build_pathname_r(path, sizeof(path), "%s/gamedata/%s/custom", dataDir, m_File); + CDirectory *customDir = g_LibSys.OpenDirectory(path); + + if (!customDir) + { + return true; + } + + while (customDir->MoreFiles()) + { + if (!customDir->IsEntryFile()) + { + customDir->NextEntry(); + continue; + } + + const char *currentFile = customDir->GetEntryName(); + + size_t length = strlen(currentFile); + + if (length > 4 && strcmp(¤tFile[length - 4], ".txt") != 0) + { + customDir->NextEntry(); + continue; + } + + g_LibSys.PathFormat(path, sizeof(path), "%s/custom/%s", m_File, currentFile); + + if (!EnterFile(path, error, maxlength)) + { + g_LibSys.CloseDirectory(customDir); + return false; + } + + customDir->NextEntry(); + } + + g_LibSys.CloseDirectory(customDir); + + return true; +} + +bool CGameConfig::EnterFile(const char *file, char *error, size_t maxlength) +{ + char path[PLATFORM_MAX_PATH]; + build_pathname_r(path, sizeof(path), "%s/gamedata/%s", get_localinfo("amxx_datadir", "addons/amxmodx/data"), file); + + strncopy(m_CurrentPath, path, sizeof(m_CurrentPath)); + + m_IgnoreLevel = 0; + m_ShouldBeReadingDefault = true; + m_ParseState = PSTATE_NONE; + + SMCError err; + SMCStates state = { 0, 0 }; + + if ((err = textparsers->ParseSMCFile(m_CurrentPath, this, &state, error, maxlength)) != SMCError_Okay) + { + const char *msg = textparsers->GetSMCErrorString(err); + + AMXXLOG_Error("Error parsing gameconfig file \"%s\":", m_CurrentPath); + AMXXLOG_Error("Error %d on line %d, col %d: %s", err, state.line, state.col, msg ? msg : "Unknown error"); + + if (m_ParseState == PSTATE_GAMEDEFS_CUSTOM) + { + m_CustomHandler->ReadSMC_ParseEnd(true, true); + m_CustomHandler = nullptr; + m_CustomLevel = 0; + } + + return false; + } + + return true; +} + +bool CGameConfig::GetOffset(const char *key, int *value) +{ + return m_Offsets.retrieve(key, value); +} + +bool CGameConfig::GetOffsetByClass(const char *classname, const char *key, int *value) +{ + auto r = m_OffsetsByClass.find(classname); + + if (!r.found()) + { + return false; + } + + return r->value->list.retrieve(key, value); +} + +const char *CGameConfig::GetKeyValue(const char *key) +{ + auto r = m_Keys.find(key); + + if (!r.found()) + { + return nullptr; + } + + return r->value.chars(); +} + +//memory addresses below 0x10000 are automatically considered invalid for dereferencing +#define VALID_MINIMUM_MEMORY_ADDRESS 0x10000 + +bool CGameConfig::GetAddress(const char *key, void **retaddr) +{ + auto r = m_Addresses.find(key); + + if (!r.found()) + { + *retaddr = nullptr; + return false; + } + + AddressConf &addrConf = r->value; + + void *address; + + if (!GetMemSig(addrConf.m_SignatureName, &address)) + { + *retaddr = nullptr; + return false; + } + + for (size_t i = 0; i < addrConf.m_ReadCount; ++i) + { + int offset = addrConf.m_ReadBytes[i]; + + if (!address || reinterpret_cast(address) < VALID_MINIMUM_MEMORY_ADDRESS) + { + *retaddr = nullptr; + return false; + } + address = *(reinterpret_cast(reinterpret_cast(address) + offset)); + } + + *retaddr = address; + + return true; +} + +CGameConfig::AddressConf::AddressConf(const char *sigName, size_t sigLength, size_t readCount, int *read) +{ + size_t limit = sizeof(m_ReadBytes) / sizeof(m_ReadBytes[0]); + size_t readLimit = (readCount <= limit) ? readCount : limit; + + strncopy(m_SignatureName, sigName, sizeof(m_SignatureName)); + + m_ReadCount = readLimit; + memcpy(&this->m_ReadBytes[0], read, sizeof(this->m_ReadBytes[0]) * readLimit); +} + +bool CGameConfig::GetMemSig(const char *key, void **addr) +{ + return m_Sigs.retrieve(key, addr); +} + + +// +// CONFIG MASTER READER +// + +#define MSTATE_NONE 0 +#define MSTATE_MAIN 1 +#define MSTATE_FILE 2 + +void CGameMasterReader::ReadSMC_ParseStart() +{ + m_State = MSTATE_NONE; + m_IgnoreLevel = 0; +} + +SMCResult CGameMasterReader::ReadSMC_NewSection(const SMCStates *states, const char *name) +{ + if (m_IgnoreLevel) + { + return SMCResult_Continue; + } + + if (m_State == MSTATE_NONE) + { + if (strcmp(name, "Game Master") == 0) + { + m_State = MSTATE_MAIN; + } + else + { + m_IgnoreLevel++; + } + } + else if (m_State == MSTATE_MAIN) + { + strncopy(m_CurrentPath, name, sizeof(m_CurrentPath)); + + m_HadEngine = false; + m_MatchedEngine = false; + m_HadGame = false; + m_MatchedGame = false; + + m_State = MSTATE_FILE; + } + else if (m_State == MSTATE_FILE) + { + m_IgnoreLevel++; + } + + return SMCResult_Continue; +} + +SMCResult CGameMasterReader::ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) +{ + if (m_IgnoreLevel || m_State != MSTATE_FILE) + { + return SMCResult_Continue; + } + + if (strcmp(key, "engine") == 0) + { + m_HadEngine = true; + + if (DoesEngineMatch(value)) + { + m_MatchedEngine = true; + } + } + else if (strcmp(key, "game") == 0) + { + m_HadGame = true; + + if (DoesGameMatch(value)) + { + m_MatchedGame = true; + } + } + + return SMCResult_Continue; +} + +SMCResult CGameMasterReader::ReadSMC_LeavingSection(const SMCStates *states) +{ + if (m_IgnoreLevel) + { + m_IgnoreLevel--; + return SMCResult_Continue; + } + + if (m_State == MSTATE_FILE) + { + // The four success conditions: + // 1. Needed nothing. + // 2. Needed game only. + // 3. Needed engine only. + // 4. Needed both engine and game. + // Final result is minimized via k-map. + + if ((!m_HadEngine && !m_HadGame) || + (!m_HadEngine && m_MatchedGame) || + (!m_HadGame && m_MatchedEngine) || + (m_MatchedEngine && m_MatchedEngine)) + { + m_FileList->append(m_CurrentPath); + } + + m_State = MSTATE_MAIN; + } + else if (m_State == MSTATE_MAIN) + { + m_State = MSTATE_NONE; + } + + return SMCResult_Continue; +} + + +// +// CONFIG MANAGER +// + +CGameConfigManager::CGameConfigManager() +{ +} + +CGameConfigManager::~CGameConfigManager() +{ +} + +bool CGameConfigManager::LoadGameConfigFile(const char *file, IGameConfig **config, char *error, size_t maxlength) +{ + CGameConfig *configFromCache; + + if (m_Lookup.retrieve(file, &configFromCache)) + { + configFromCache->AddRef(); + *config = configFromCache; + + return true; + } + + configFromCache = new CGameConfig(file); + configFromCache->AddRef(); + + bool returnValue = returnValue = configFromCache->Reparse(error, maxlength); + + m_Lookup.insert(file, configFromCache); + *config = configFromCache; + + return returnValue; +} + +void CGameConfigManager::CloseGameConfigFile(IGameConfig *config) +{ + CGameConfig *currentConfig = static_cast(config); + currentConfig->Release(); +} + +void CGameConfigManager::AddUserConfigHook(const char *sectionName, ITextListener_SMC *listener) +{ + m_customHandlers.insert(sectionName, listener); +} + +void CGameConfigManager::RemoveUserConfigHook(const char *sectionName, ITextListener_SMC *listener) +{ + ITextListener_SMC *listenerFromCache; + + if (!m_customHandlers.retrieve(sectionName, &listenerFromCache)) + { + return; + } + + if (listenerFromCache != listener) + { + return; + } + + m_customHandlers.remove(sectionName); +} + +void CGameConfigManager::RemoveCachedConfig(CGameConfig *config) +{ + m_Lookup.remove(config->m_File); +} diff --git a/amxmodx/CGameConfigs.h b/amxmodx/CGameConfigs.h new file mode 100644 index 00000000..01ab72db --- /dev/null +++ b/amxmodx/CGameConfigs.h @@ -0,0 +1,167 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +#ifndef _INCLUDE_GAMECONFIG_H_ +#define _INCLUDE_GAMECONFIG_H_ + +#include +#include "CLibrarySys.h" +#include +#include +#include +#include +#include + +class CGameConfig + : + public ITextListener_SMC, + public IGameConfig, + public ke::Refcounted +{ + friend class CGameConfigManager; + + public: + + CGameConfig(const char *file); + ~CGameConfig(); + + public: + + bool Reparse(char *error, size_t maxlength); + bool EnterFile(const char *file, char *error, size_t maxlength); + + public: // ITextListener_SMC + + SMCResult ReadSMC_NewSection(const SMCStates *states, const char *name); + SMCResult ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value); + SMCResult ReadSMC_LeavingSection(const SMCStates *states); + + public: // IGameConfig + + const char* GetKeyValue(const char *key); + bool GetOffset(const char *key, int *value); + bool GetOffsetByClass(const char *classname, const char *key, int *value); + bool GetMemSig(const char *key, void **addr); + bool GetAddress(const char *key, void **addr); + + public: // NameHashSet + + static inline bool matches(const char *key, const CGameConfig *value) + { + return strcmp(key, value->m_File) == 0; + } + + private: + + struct OffsetClass + { + StringHashMap list; + }; + + typedef StringHashMap> OffsetClassMap; + + char m_File[PLATFORM_MAX_PATH]; + char m_CurrentPath[PLATFORM_MAX_PATH]; + + StringHashMap m_Offsets; + OffsetClassMap m_OffsetsByClass; + StringHashMap m_Keys; + StringHashMap m_Sigs; + + int m_ParseState; + unsigned int m_IgnoreLevel; + + char m_Class[64]; + char m_Offset[64]; + char m_Game[256]; + + bool m_ShouldBeReadingDefault; + bool m_HadGame; + bool m_MatchedGame; + bool m_HadEngine; + bool m_MatchedEngine; + bool m_MatchedPlatform; + + unsigned int m_CustomLevel; + ITextListener_SMC* m_CustomHandler; + + struct AddressConf + { + char m_SignatureName[64]; + size_t m_ReadCount; + int m_ReadBytes[8]; + + AddressConf(const char *sigName, size_t sigLength, size_t readCount, int *read); + AddressConf() {} + }; + + char m_Address[64]; + char m_AddressSignature[64]; + int m_AddressReadCount; + int m_AddressRead[8]; + StringHashMap m_Addresses; + + char m_pEngine[64]; +}; + +class CGameMasterReader : public ITextListener_SMC +{ + public: + + void ReadSMC_ParseStart(); + + SMCResult ReadSMC_NewSection(const SMCStates *states, const char *name); + SMCResult ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value); + SMCResult ReadSMC_LeavingSection(const SMCStates *states); + + public: + + ke::Vector* m_FileList; + + unsigned int m_State; + unsigned int m_IgnoreLevel; + + char m_CurrentPath[PLATFORM_MAX_PATH]; + + bool m_HadEngine; + bool m_MatchedEngine; + bool m_HadGame; + bool m_MatchedGame; +}; + +class CGameConfigManager : public IGameConfigManager +{ + public: + + CGameConfigManager(); + ~CGameConfigManager(); + + public: // IGameConfigManager + + bool LoadGameConfigFile(const char *file, IGameConfig **pConfig, char *error, size_t maxlength); + void CloseGameConfigFile(IGameConfig *cfg); + void AddUserConfigHook(const char *sectionname, ITextListener_SMC *listener); + void RemoveUserConfigHook(const char *sectionname, ITextListener_SMC *listener); + + public: + + void RemoveCachedConfig(CGameConfig *config); + + private: + + NameHashSet m_Lookup; + + public: + + StringHashMap m_customHandlers; +}; + +extern CGameConfigManager ConfigManager; + +#endif // _INCLUDE_GAMECONFIG_H_ diff --git a/amxmodx/CLibrarySys.cpp b/amxmodx/CLibrarySys.cpp index 689b789d..fca84a3d 100644 --- a/amxmodx/CLibrarySys.cpp +++ b/amxmodx/CLibrarySys.cpp @@ -530,3 +530,29 @@ bool LibrarySystem::FileTime(const char* path, FileTimeType type, time_t* pTime) return true; } + +bool LibrarySystem::DoesPlatformMatch(const char *platform) +{ + return strcmp(platform, PLATFORM_NAME) == 0; +} + +bool LibrarySystem::IsPlatformCompatible(const char *platform, bool *hadPrimaryMatch) +{ + if (DoesPlatformMatch(platform)) + { +#if defined PLATFORM_COMPAT_ALT + *hadPrimaryMatch = true; +#endif + return true; + } + +#if defined PLATFORM_COMPAT_ALT + /* If entry hasn't been found for the primary platform name, check for compatible alternate */ + if (!*hadPrimaryMatch) + { + return strcmp(platform, PLATFORM_COMPAT_ALT) == 0; + } +#endif + + return false; +} \ No newline at end of file diff --git a/amxmodx/CLibrarySys.h b/amxmodx/CLibrarySys.h index d57063b4..935cac6c 100644 --- a/amxmodx/CLibrarySys.h +++ b/amxmodx/CLibrarySys.h @@ -14,6 +14,9 @@ #include // Interface (HLSDK) #include // AutoPtr +#define PLATFORM_WINDOWNS_NAME "windows" +#define PLATFORM_LINUX_NAME "linux" +#define PLATFORM_MAC_NAME "mac" #if defined(WIN32) # ifndef PLATFORM_WINDOWS # define PLATFORM_WINDOWS 1 @@ -25,6 +28,7 @@ # include # include # define PLATFORM_LIB_EXT "dll" +# define PLATFORM_NAME PLATFORM_WINDOWNS_NAME # define PLATFORM_SEP_CHAR '\\' # define PLATFORM_SEP_ALTCHAR '/' # define PLATFORM_EXTERN_C extern "C" __declspec(dllexport) @@ -32,9 +36,13 @@ # if defined(__linux__) # define PLATFORM_LINUX 1 # define PLATFORM_LIB_EXT "so" +# define PLATFORM_NAME PLATFORM_LINUX_NAME +# define PLATFORM_COMPAT_ALT PLATFORM_MAC_NAME # elif defined(__APPLE__) # define PLATFORM_APPLE 1 # define PLATFORM_LIB_EXT "dylib" +# define PLATFORM_NAME PLATFORM_MAC_NAME +# define PLATFORM_COMPAT_ALT PLATFORM_LINUX_NAME # endif # ifndef PLATFORM_POSIX # define PLATFORM_POSIX 1 @@ -153,6 +161,9 @@ class LibrarySystem bool FileTime(const char* path, FileTimeType type, time_t* pTime); void GetLoaderError(char* buffer, size_t maxlength); + + bool DoesPlatformMatch(const char* platform); + bool IsPlatformCompatible(const char *platform, bool *hadPrimaryMatch); }; extern LibrarySystem g_LibSys; diff --git a/amxmodx/msvc12/amxmodx_mm.vcxproj b/amxmodx/msvc12/amxmodx_mm.vcxproj index 4cdb7d2d..bd63e4a9 100644 --- a/amxmodx/msvc12/amxmodx_mm.vcxproj +++ b/amxmodx/msvc12/amxmodx_mm.vcxproj @@ -150,6 +150,7 @@ + $(IntDir)hashing\ $(IntDir)hashing\ @@ -255,6 +256,7 @@ + @@ -307,6 +309,7 @@ + @@ -338,6 +341,7 @@ + diff --git a/amxmodx/msvc12/amxmodx_mm.vcxproj.filters b/amxmodx/msvc12/amxmodx_mm.vcxproj.filters index 7270a8b4..ef5c2f69 100644 --- a/amxmodx/msvc12/amxmodx_mm.vcxproj.filters +++ b/amxmodx/msvc12/amxmodx_mm.vcxproj.filters @@ -277,11 +277,17 @@ Third Party\Hashing\hashers - Source Files - + Source Files + Source Files + + Source Files + + + Memtools + @@ -478,13 +484,19 @@ Third Party\Hashing\hashers - + Header Files Header Files + + Header Files + + + Memtools + diff --git a/public/IGameConfigs.h b/public/IGameConfigs.h new file mode 100644 index 00000000..1360e371 --- /dev/null +++ b/public/IGameConfigs.h @@ -0,0 +1,139 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * SourceMod + * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + * + * Version: $Id$ + */ + +#ifndef _INCLUDE_IGAMECONFIG_H_ +#define _INCLUDE_IGAMECONFIG_H_ + +#include + +/** + * @brief Describes a game private data config file + */ +class IGameConfig +{ +public: + /** + * @brief Returns an offset value. + * + * @param key Key to retrieve from the offset section. + * @param value Pointer to store the offset value in. + * @return True if found, false otherwise. + */ + virtual bool GetOffset(const char *key, int *value) = 0; + + /** + * @brief Returns an offset value from given class. + * + * @param classname class name to match from the offset section. + * @param key Key to retrieve from the offset section. + * @param value Pointer to store the offset value in. + * @return True if found, false otherwise. + */ + virtual bool GetOffsetByClass(const char *classname, const char *key, int *value) = 0; + + /** + * @brief Returns the value of a key from the "Keys" section. + * + * @param key Key to retrieve from the Keys section. + * @return String containing the value, or NULL if not found. + */ + virtual const char *GetKeyValue(const char *key) = 0; + + /** + * @brief Retrieves a cached memory signature. + * + * @param key Name of the signature. + * @param addr Pointer to store the memory address in. + * (NULL is copied if signature is not found in binary). + * @return True if the section exists and key for current + * platform was found, false otherwise. + */ + virtual bool GetMemSig(const char *key, void **addr) = 0; + + /** + * @brief Retrieves the value of an address from the "Address" section. + * + * @param key Key to retrieve from the Address section. + * @param addr Pointer to store the memory address. + * @return True on success, false on failure. + */ + virtual bool GetAddress(const char *key, void **addr) = 0; +}; + +/** + * @brief Manages game config files + */ +class IGameConfigManager +{ +public: + /** + * @brief Loads or finds an already loaded game config file. + * + * @param file File to load. The path must be relative to the + * 'gamedata' folder and the extension should be + * omitted. + * @param pConfig Pointer to store the game config pointer. Pointer + * will be valid even on failure. + * @param error Optional error message buffer. + * @param maxlength Maximum length of the error buffer. + * @return True on success, false if the file failed. + */ + virtual bool LoadGameConfigFile(const char *file, IGameConfig **pConfig, char *error, size_t maxlength) = 0; + + /** + * @brief Closes an IGameConfig pointer. Since a file can be loaded + * more than once, the file will not actually be removed from memory + * until it is closed once for each call to LoadGameConfigfile(). + * + * @param cfg Pointer to the IGameConfig to close. + */ + virtual void CloseGameConfigFile(IGameConfig *cfg) = 0; + + /** + * @brief Adds a custom gamedata section hook. + * + * @param sectionname Section name to hook. + * @param listener Listener callback. + * @noreturn + */ + virtual void AddUserConfigHook(const char *sectionname, ITextListener_SMC *listener) = 0; + + /** + * @brief Removes a custom gamedata section hook. + * + * @param sectionname Section name to unhook. + * @param listener Listener callback. + * @noreturn + */ + virtual void RemoveUserConfigHook(const char *sectionname, ITextListener_SMC *listener) = 0; +}; + +#endif //_INCLUDE_IGAMECONFIG_H_ diff --git a/public/memtools/MemoryUtils.cpp b/public/memtools/MemoryUtils.cpp index 65e38636..99ec9a94 100644 --- a/public/memtools/MemoryUtils.cpp +++ b/public/memtools/MemoryUtils.cpp @@ -28,7 +28,7 @@ */ #include "MemoryUtils.h" -#include "amxxmodule.h" +#include // sscanf #if defined(__linux__) #include @@ -159,6 +159,13 @@ void *MemoryUtils::ResolveSymbol(void *handle, const char *symbol) #elif defined(__linux__) + void *addr = dlsym(handle, symbol); + + if (addr) + { + return addr; + } + struct link_map *dlmap; struct stat dlstat; int dlfile; From 14513e6f701572d758fdaa1c0f10a28023e41209 Mon Sep 17 00:00:00 2001 From: Arkshine Date: Tue, 23 Jun 2015 20:04:17 +0200 Subject: [PATCH 2/3] Gameconfig: Export game config manager to AMXX API --- amxmodx/CGameConfigs.cpp | 16 +++++++++++++++- amxmodx/modules.cpp | 7 +++++++ public/sdk/amxxmodule.cpp | 3 +++ public/sdk/amxxmodule.h | 5 +++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/amxmodx/CGameConfigs.cpp b/amxmodx/CGameConfigs.cpp index 2b0e9421..35c07655 100644 --- a/amxmodx/CGameConfigs.cpp +++ b/amxmodx/CGameConfigs.cpp @@ -516,7 +516,21 @@ bool CGameConfig::Reparse(char *error, size_t maxlength) if (!g_LibSys.PathExists(path)) { - return false; + g_LibSys.PathFormat(path, sizeof(path), "%s.txt", m_File); + + if (!EnterFile(path, error, maxlength)) + { + return false; + } + + build_pathname_r(path, sizeof(path), "%s/gamedata/custom/%s.txt", dataDir, m_File); + + if (g_LibSys.PathExists(path)) + { + g_LibSys.PathFormat(path, sizeof(path), "custom/%s.txt", m_File); + return EnterFile(path, error, maxlength); + } + return true; } SMCError err; diff --git a/amxmodx/modules.cpp b/amxmodx/modules.cpp index f68cbcbf..5d32c809 100755 --- a/amxmodx/modules.cpp +++ b/amxmodx/modules.cpp @@ -31,6 +31,7 @@ #include "messages.h" #include "trie_natives.h" #include "CDataPack.h" +#include "CGameConfigs.h" CList g_modules; CList g_loadedscripts; @@ -1810,6 +1811,11 @@ int amx_Execv() return AMX_ERR_NOTFOUND; } +IGameConfigManager *MNF_GetConfigManager() +{ + return &ConfigManager; +} + void Module_CacheFunctions() { func_s *pFunc; @@ -1824,6 +1830,7 @@ void Module_CacheFunctions() REGISTER_FUNC("Format", MNF_Format) REGISTER_FUNC("RegisterFunction", MNF_RegisterFunction); REGISTER_FUNC("RegisterFunctionEx", MNF_RegisterFunctionEx); + REGISTER_FUNC("GetConfigManager", MNF_GetConfigManager); // Amx scripts loading / unloading / managing REGISTER_FUNC("GetAmxScript", MNF_GetAmxScript) diff --git a/public/sdk/amxxmodule.cpp b/public/sdk/amxxmodule.cpp index 332a98df..ea4049b0 100644 --- a/public/sdk/amxxmodule.cpp +++ b/public/sdk/amxxmodule.cpp @@ -2501,6 +2501,7 @@ PFN_GETLOCALINFO g_fn_GetLocalInfo; PFN_AMX_REREGISTER g_fn_AmxReRegister; PFN_REGISTERFUNCTIONEX g_fn_RegisterFunctionEx; PFN_MESSAGE_BLOCK g_fn_MessageBlock; +PFN_GET_CONFIG_MANAGER g_fn_GetConfigManager; // *** Exports *** C_DLLEXPORT int AMXX_Query(int *interfaceVersion, amxx_module_info_s *moduleInfo) @@ -2560,6 +2561,7 @@ C_DLLEXPORT int AMXX_Attach(PFN_REQ_FNPTR reqFnptrFunc) REQFUNC("Format", g_fn_Format, PFN_FORMAT); REQFUNC("RegisterFunction", g_fn_RegisterFunction, PFN_REGISTERFUNCTION); REQFUNC("RegisterFunctionEx", g_fn_RegisterFunctionEx, PFN_REGISTERFUNCTIONEX); + REQFUNC("GetConfigManager", g_fn_GetConfigManager, PFN_GET_CONFIG_MANAGER); // Amx scripts REQFUNC("GetAmxScript", g_fn_GetAmxScript, PFN_GET_AMXSCRIPT); @@ -2787,6 +2789,7 @@ void ValidateMacros_DontCallThis_Smiley() MF_RemoveLibraries(NULL); MF_OverrideNatives(NULL, NULL); MF_MessageBlock(0, 0, NULL); + MF_GetConfigManager(); } #endif diff --git a/public/sdk/amxxmodule.h b/public/sdk/amxxmodule.h index 4f4cd705..5f0feab5 100644 --- a/public/sdk/amxxmodule.h +++ b/public/sdk/amxxmodule.h @@ -18,6 +18,7 @@ // config #include "moduleconfig.h" +#include #include // size_t // metamod include files @@ -2217,6 +2218,7 @@ typedef const char * (*PFN_GETLOCALINFO) (const char * /*name*/, const char * typedef int (*PFN_AMX_REREGISTER) (AMX * /*amx*/, AMX_NATIVE_INFO * /*list*/, int /*list*/); typedef void * (*PFN_REGISTERFUNCTIONEX) (void * /*pfn*/, const char * /*desc*/); typedef void (*PFN_MESSAGE_BLOCK) (int /* mode */, int /* message */, int * /* opt */); +typedef IGameConfigManager* (*PFN_GET_CONFIG_MANAGER) (); extern PFN_ADD_NATIVES g_fn_AddNatives; extern PFN_ADD_NEW_NATIVES g_fn_AddNewNatives; @@ -2297,6 +2299,7 @@ extern PFN_GETLOCALINFO g_fn_GetLocalInfo; extern PFN_AMX_REREGISTER g_fn_AmxReRegister; extern PFN_REGISTERFUNCTIONEX g_fn_RegisterFunctionEx; extern PFN_MESSAGE_BLOCK g_fn_MessageBlock; +extern PFN_GET_CONFIG_MANAGER g_fn_GetConfigManager; #ifdef MAY_NEVER_BE_DEFINED // Function prototypes for intellisense and similar systems @@ -2374,6 +2377,7 @@ const char * MF_GetLocalInfo (const char *name, const char *def) { } int MF_AmxReRegister (AMX *amx, AMX_NATIVE_INFO *list, int number) { return 0; } void * MF_RegisterFunctionEx (void *pfn, const char *description) { } void * MF_MessageBlock (int mode, int msg, int *opt) { } +IGameConfigManager* MF_MessageBlock () { } #endif // MAY_NEVER_BE_DEFINED #define MF_AddNatives g_fn_AddNatives @@ -2456,6 +2460,7 @@ void MF_LogError(AMX *amx, int err, const char *fmt, ...); #define MF_AmxReRegister g_fn_AmxReRegister #define MF_RegisterFunctionEx g_fn_RegisterFunctionEx #define MF_MessageBlock g_fn_MessageBlock +#define MF_GetConfigManager g_fn_GetConfigManager #ifdef MEMORY_TEST /*** Memory ***/ From f38c726faf5a5677198210b66b6f17eaf974b5d6 Mon Sep 17 00:00:00 2001 From: Arkshine Date: Wed, 24 Jun 2015 17:45:12 +0200 Subject: [PATCH 3/3] Gameconfig: Add game configs natives --- amxmodx/AMBuilder | 1 + amxmodx/CGameConfigs.cpp | 91 +++++++---- amxmodx/CGameConfigs.h | 1 + amxmodx/amxmodx.h | 1 + amxmodx/gameconfigs.cpp | 174 ++++++++++++++++++++++ amxmodx/gameconfigs.h | 18 +++ amxmodx/meta_api.cpp | 2 + amxmodx/modules.cpp | 1 + amxmodx/msvc12/amxmodx_mm.vcxproj | 4 + amxmodx/msvc12/amxmodx_mm.vcxproj.filters | 12 ++ amxmodx/natives_handles.h | 88 +++++++++++ plugins/include/amxmodx.inc | 1 + plugins/include/gameconfig.inc | 93 ++++++++++++ public/ITextParsers.h | 2 + public/memtools/MemoryUtils.cpp | 18 ++- public/memtools/MemoryUtils.h | 1 + public/sdk/amxxmodule.h | 2 +- support/PackageScript | 1 + 18 files changed, 481 insertions(+), 30 deletions(-) create mode 100644 amxmodx/gameconfigs.cpp create mode 100644 amxmodx/gameconfigs.h create mode 100644 amxmodx/natives_handles.h create mode 100644 plugins/include/gameconfig.inc diff --git a/amxmodx/AMBuilder b/amxmodx/AMBuilder index 53075490..bc954f51 100644 --- a/amxmodx/AMBuilder +++ b/amxmodx/AMBuilder @@ -96,6 +96,7 @@ binary.sources = [ '../public/memtools/CDetour/asm/asm.c', 'CLibrarySys.cpp', 'CGameConfigs.cpp', + 'gameconfigs.cpp', ] if builder.target_platform == 'windows': diff --git a/amxmodx/CGameConfigs.cpp b/amxmodx/CGameConfigs.cpp index 35c07655..8678d5c6 100644 --- a/amxmodx/CGameConfigs.cpp +++ b/amxmodx/CGameConfigs.cpp @@ -14,7 +14,7 @@ CGameConfigManager ConfigManager; static CGameMasterReader MasterReader; -// +// // GAME CONFIG // @@ -23,6 +23,8 @@ enum PSTATE_NONE, PSTATE_GAMES, PSTATE_GAMEDEFS, + PSTATE_GAMEDEFS_CLASSES, + PSTATE_GAMEDEFS_CLASSES_CLASS, PSTATE_GAMEDEFS_OFFSETS, PSTATE_GAMEDEFS_OFFSETS_OFFSET, PSTATE_GAMEDEFS_KEYS, @@ -103,6 +105,8 @@ SMCResult CGameConfig::ReadSMC_NewSection(const SMCStates *states, const char *n m_ShouldBeReadingDefault = true; strncopy(m_Game, name, sizeof(m_Game)); + m_Class[0] = '\0'; + m_MatchedClasses = false; m_ParseState = PSTATE_GAMEDEFS; } else @@ -112,8 +116,22 @@ SMCResult CGameConfig::ReadSMC_NewSection(const SMCStates *states, const char *n break; } case PSTATE_GAMEDEFS: + case PSTATE_GAMEDEFS_CLASSES_CLASS: { - if (strcmp(name, "Offsets") == 0) + if (strcmp(name, "Classes") == 0) + { + if (!m_Class[0]) + { + m_ParseState = PSTATE_GAMEDEFS_CLASSES; + m_MatchedClasses = true; + } + else + { + ++m_IgnoreLevel; + } + break; + } + else if (strcmp(name, "Offsets") == 0) { m_ParseState = PSTATE_GAMEDEFS_OFFSETS; } @@ -149,14 +167,18 @@ SMCResult CGameConfig::ReadSMC_NewSection(const SMCStates *states, const char *n break; } - m_IgnoreLevel++; + ++m_IgnoreLevel; } break; } + case PSTATE_GAMEDEFS_CLASSES: + { + strncopy(m_Class, name, sizeof(m_Class)); + m_ParseState = PSTATE_GAMEDEFS_CLASSES_CLASS; + break; + } case PSTATE_GAMEDEFS_OFFSETS: { - m_Class[0] = '\0'; - strncopy(m_Offset, name, sizeof(m_Offset)); m_ParseState = PSTATE_GAMEDEFS_OFFSETS_OFFSET; @@ -174,7 +196,7 @@ SMCResult CGameConfig::ReadSMC_NewSection(const SMCStates *states, const char *n } case PSTATE_GAMEDEFS_CUSTOM: { - m_CustomLevel++; + ++m_CustomLevel; return m_CustomHandler->ReadSMC_NewSection(states, name); break; } @@ -228,11 +250,7 @@ SMCResult CGameConfig::ReadSMC_KeyValue(const SMCStates *states, const char *key { case PSTATE_GAMEDEFS_OFFSETS_OFFSET: { - if (strcmp(key, "class") == 0) - { - strncopy(m_Class, value, sizeof(m_Class)); - } - else if (g_LibSys.IsPlatformCompatible(key, &m_MatchedPlatform)) + if (g_LibSys.IsPlatformCompatible(key, &m_MatchedPlatform)) { if (m_Class[0]) { @@ -246,7 +264,7 @@ SMCResult CGameConfig::ReadSMC_KeyValue(const SMCStates *states, const char *key { ic->value = new OffsetClass; ic->value->list.insert(m_Offset, atoi(value)); - } + } } else { @@ -319,7 +337,7 @@ SMCResult CGameConfig::ReadSMC_KeyValue(const SMCStates *states, const char *key } else { - AMXXLOG_Error("[SM] Error parsing Address \"%s\", does not support more than %d read offsets (gameconf \"%s\")", + AMXXLOG_Error("[SM] Error parsing Address \"%s\", does not support more than %d read offsets (gameconf \"%s\")", m_Address, limit, m_CurrentPath); } } @@ -365,6 +383,18 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) m_ParseState = PSTATE_GAMES; break; } + case PSTATE_GAMEDEFS_CLASSES: + { + m_MatchedClasses = false; + m_ParseState = PSTATE_GAMEDEFS; + break; + } + case PSTATE_GAMEDEFS_CLASSES_CLASS: + { + m_ParseState = PSTATE_GAMEDEFS_CLASSES; + m_Class[0] = '\0'; + break; + } case PSTATE_GAMEDEFS_CUSTOM: { m_ParseState = PSTATE_GAMEDEFS; @@ -372,9 +402,13 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) break; } case PSTATE_GAMEDEFS_KEYS: + { + m_ParseState = m_MatchedClasses ? PSTATE_GAMEDEFS_CLASSES_CLASS : PSTATE_GAMEDEFS; + break; + } case PSTATE_GAMEDEFS_OFFSETS: { - m_ParseState = PSTATE_GAMEDEFS; + m_ParseState = m_MatchedClasses ? PSTATE_GAMEDEFS_CLASSES_CLASS : PSTATE_GAMEDEFS; break; } case PSTATE_GAMEDEFS_OFFSETS_OFFSET: @@ -389,7 +423,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) m_IgnoreLevel = 1; m_ParseState = PSTATE_GAMES; } - else + else { m_ParseState = PSTATE_GAMEDEFS; } @@ -397,7 +431,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) } case PSTATE_GAMEDEFS_SIGNATURES: { - m_ParseState = PSTATE_GAMEDEFS; + m_ParseState = m_MatchedClasses ? PSTATE_GAMEDEFS_CLASSES_CLASS : PSTATE_GAMEDEFS; break; } case PSTATE_GAMEDEFS_SIGNATURES_SIG: @@ -413,7 +447,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) { addressInBase = reinterpret_cast(MDLL_Spawn); } - else if (strcmp(TempSig.library, "engine") == 0) + else if (strcmp(TempSig.library, "engine") == 0) { addressInBase = reinterpret_cast(gpGlobals); } @@ -442,7 +476,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) #elif defined PLATFORM_POSIX Dl_info info; - + if (dladdr(addressInBase, &info) != 0) { void *handle = dlopen(info.dli_fname, RTLD_NOW); @@ -457,7 +491,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) AMXXLOG_Error("Unable to load library \"%s\" (gameconf \"%s\")", TempSig.library, m_File); } } - else + else { AMXXLOG_Error("Unable to find library \"%s\" in memory (gameconf \"%s\")", TempSig.library, m_File); } @@ -477,7 +511,7 @@ SMCResult CGameConfig::ReadSMC_LeavingSection(const SMCStates *states) } case PSTATE_GAMEDEFS_ADDRESSES: { - m_ParseState = PSTATE_GAMEDEFS; + m_ParseState = m_MatchedClasses ? PSTATE_GAMEDEFS_CLASSES_CLASS : PSTATE_GAMEDEFS; break; } case PSTATE_GAMEDEFS_ADDRESSES_ADDRESS: @@ -516,13 +550,16 @@ bool CGameConfig::Reparse(char *error, size_t maxlength) if (!g_LibSys.PathExists(path)) { +#if 0 + // Single config file without master g_LibSys.PathFormat(path, sizeof(path), "%s.txt", m_File); if (!EnterFile(path, error, maxlength)) { return false; } - +#endif + // Allow customizations of default gamedata files build_pathname_r(path, sizeof(path), "%s/gamedata/custom/%s.txt", dataDir, m_File); if (g_LibSys.PathExists(path)) @@ -605,10 +642,7 @@ bool CGameConfig::Reparse(char *error, size_t maxlength) bool CGameConfig::EnterFile(const char *file, char *error, size_t maxlength) { - char path[PLATFORM_MAX_PATH]; - build_pathname_r(path, sizeof(path), "%s/gamedata/%s", get_localinfo("amxx_datadir", "addons/amxmodx/data"), file); - - strncopy(m_CurrentPath, path, sizeof(m_CurrentPath)); + build_pathname_r(m_CurrentPath, sizeof(m_CurrentPath), "%s/gamedata/%s", get_localinfo("amxx_datadir", "addons/amxmodx/data"), file); m_IgnoreLevel = 0; m_ShouldBeReadingDefault = true; @@ -698,7 +732,8 @@ bool CGameConfig::GetAddress(const char *key, void **retaddr) *retaddr = nullptr; return false; } - address = *(reinterpret_cast(reinterpret_cast(address) + offset)); + + address = reinterpret_cast(reinterpret_cast(address) + offset); } *retaddr = address; @@ -723,7 +758,7 @@ bool CGameConfig::GetMemSig(const char *key, void **addr) } -// +// // CONFIG MASTER READER // @@ -839,7 +874,7 @@ SMCResult CGameMasterReader::ReadSMC_LeavingSection(const SMCStates *states) } -// +// // CONFIG MANAGER // diff --git a/amxmodx/CGameConfigs.h b/amxmodx/CGameConfigs.h index 01ab72db..e908bdb7 100644 --- a/amxmodx/CGameConfigs.h +++ b/amxmodx/CGameConfigs.h @@ -81,6 +81,7 @@ class CGameConfig char m_Offset[64]; char m_Game[256]; + bool m_MatchedClasses; bool m_ShouldBeReadingDefault; bool m_HadGame; bool m_MatchedGame; diff --git a/amxmodx/amxmodx.h b/amxmodx/amxmodx.h index af60eaa9..d0cf8e24 100755 --- a/amxmodx/amxmodx.h +++ b/amxmodx/amxmodx.h @@ -67,6 +67,7 @@ extern AMX_NATIVE_INFO g_DataStructNatives[]; extern AMX_NATIVE_INFO g_StackNatives[]; extern AMX_NATIVE_INFO g_TextParserNatives[]; extern AMX_NATIVE_INFO g_CvarNatives[]; +extern AMX_NATIVE_INFO g_GameConfigNatives[]; #if defined(_WIN32) #define DLLOAD(path) (DLHANDLE)LoadLibrary(path) diff --git a/amxmodx/gameconfigs.cpp b/amxmodx/gameconfigs.cpp new file mode 100644 index 00000000..b8768c82 --- /dev/null +++ b/amxmodx/gameconfigs.cpp @@ -0,0 +1,174 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +#include "gameconfigs.h" +#include "amxmodx.h" +#include "CGameConfigs.h" + +Handle GameConfigHandle; + +// native GameConfig:LoadGameConfigFile(const file[]); +static cell AMX_NATIVE_CALL LoadGameConfigFile(AMX *amx, cell *params) +{ + int length; + const char *filename = get_amxstring(amx, params[1], 0, length); + + IGameConfig *config = nullptr; + char error[128]; + + if (!ConfigManager.LoadGameConfigFile(filename, &config, error, sizeof(error))) + { + LogError(amx, AMX_ERR_NATIVE, "Unable to open %s: %s", filename, error); + return 0; + } + + int handle = GameConfigHandle.create(); + + auto configHandle = GameConfigHandle.lookup(handle); + + if (!configHandle) + { + return 0; + } + + configHandle->m_config = config; + + return handle; +} + +// native GameConfGetOffset(GameConfig:handle, const key[]); +static cell AMX_NATIVE_CALL GameConfGetOffset(AMX *amx, cell *params) +{ + GameConfigNative *handle = GameConfigHandle.lookup(params[1]); + + if (!handle) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid game config handle %d", params[1]); + return 0; + } + + int length; + int value; + + const char *key = get_amxstring(amx, params[2], 0, length); + + if (!handle->m_config->GetOffset(key, &value)) + { + return -1; + } + + return value; +} + +// native GameConfGetClassOffset(GameConfig:handle, const classname[], const key[]); +static cell AMX_NATIVE_CALL GameConfGetClassOffset(AMX *amx, cell *params) +{ + GameConfigNative *handle = GameConfigHandle.lookup(params[1]); + + if (!handle) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid game config handle %d", params[1]); + return 0; + } + + int length; + int value; + + const char *classname = get_amxstring(amx, params[2], 0, length); + const char *key = get_amxstring(amx, params[3], 1, length); + + if (!handle->m_config->GetOffsetByClass(classname, key, &value)) + { + return -1; + } + + return value; +} + +// native bool:GameConfGetKeyValue(GameConfig:handle, const key[], buffer[], maxlen); +static cell AMX_NATIVE_CALL GameConfGetKeyValue(AMX *amx, cell *params) +{ + GameConfigNative *handle = GameConfigHandle.lookup(params[1]); + + if (!handle) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid game config handle %d", params[1]); + return 0; + } + + int length; + const char *value; + const char *key = get_amxstring(amx, params[2], 0, length); + + if (!(value = handle->m_config->GetKeyValue(key))) + { + return 0; + } + + set_amxstring_utf8(amx, params[3], value, strlen(value), params[4]); + + return 1; +} + +// native GameConfGetAddress(GameConfig:handle, const name[]); +static cell AMX_NATIVE_CALL GameConfGetAddress(AMX *amx, cell *params) +{ + GameConfigNative *handle = GameConfigHandle.lookup(params[1]); + + if (!handle) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid game config handle %d", params[1]); + return 0; + } + + int length; + void *value; + + const char *key = get_amxstring(amx, params[2], 0, length); + + if (!handle->m_config->GetAddress(key, &value)) + { + return 0; + } + + return reinterpret_cast(value); +} + +// native CloseGameConfigFile(&GameConfig:handle); +static cell AMX_NATIVE_CALL CloseGameConfigFile(AMX *amx, cell *params) +{ + cell *address = get_amxaddr(amx, params[1]); + + GameConfigNative *handle = GameConfigHandle.lookup(*address); + + if (!handle) + { + return 0; + } + + if (GameConfigHandle.destroy(*address)) + { + *address = 0; + return 1; + } + + return 0; +} + + +AMX_NATIVE_INFO g_GameConfigNatives[] = +{ + { "LoadGameConfigFile" , LoadGameConfigFile }, + { "GameConfGetOffset" , GameConfGetOffset }, + { "GameConfGetClassOffset", GameConfGetClassOffset }, + { "GameConfGetKeyValue" , GameConfGetKeyValue }, + { "GameConfGetAddress" , GameConfGetAddress }, + { "CloseGameConfigFile" , CloseGameConfigFile }, + { nullptr , nullptr } +}; diff --git a/amxmodx/gameconfigs.h b/amxmodx/gameconfigs.h new file mode 100644 index 00000000..aed9434f --- /dev/null +++ b/amxmodx/gameconfigs.h @@ -0,0 +1,18 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +#include "IGameConfigs.h" +#include "natives_handles.h" + +struct GameConfigNative +{ + IGameConfig *m_config; +}; + +extern Handle GameConfigHandle; diff --git a/amxmodx/meta_api.cpp b/amxmodx/meta_api.cpp index 7108b397..b56ab7c8 100755 --- a/amxmodx/meta_api.cpp +++ b/amxmodx/meta_api.cpp @@ -27,6 +27,7 @@ #include "CvarManager.h" #include "CLibrarySys.h" #include "CFileSystem.h" +#include "gameconfigs.h" plugin_info_t Plugin_info = { @@ -408,6 +409,7 @@ int C_Spawn(edict_t *pent) g_TrieSnapshotHandles.clear(); g_DataPackHandles.clear(); g_TextParsersHandles.clear(); + GameConfigHandle.clear(); char map_pluginsfile_path[256]; char prefixed_map_pluginsfile[256]; diff --git a/amxmodx/modules.cpp b/amxmodx/modules.cpp index 5d32c809..b935a459 100755 --- a/amxmodx/modules.cpp +++ b/amxmodx/modules.cpp @@ -531,6 +531,7 @@ int set_amxnatives(AMX* amx, char error[128]) amx_Register(amx, g_StackNatives, -1); amx_Register(amx, g_TextParserNatives, -1); amx_Register(amx, g_CvarNatives, -1); + amx_Register(amx, g_GameConfigNatives, -1); //we're not actually gonna check these here anymore amx->flags |= AMX_FLAG_PRENIT; diff --git a/amxmodx/msvc12/amxmodx_mm.vcxproj b/amxmodx/msvc12/amxmodx_mm.vcxproj index bd63e4a9..87d7ded9 100644 --- a/amxmodx/msvc12/amxmodx_mm.vcxproj +++ b/amxmodx/msvc12/amxmodx_mm.vcxproj @@ -278,6 +278,7 @@ AssemblyAndSourceCode + @@ -361,10 +362,12 @@ + + @@ -383,6 +386,7 @@ + diff --git a/amxmodx/msvc12/amxmodx_mm.vcxproj.filters b/amxmodx/msvc12/amxmodx_mm.vcxproj.filters index ef5c2f69..c789aa43 100644 --- a/amxmodx/msvc12/amxmodx_mm.vcxproj.filters +++ b/amxmodx/msvc12/amxmodx_mm.vcxproj.filters @@ -288,6 +288,9 @@ Memtools + + Source Files + @@ -497,6 +500,12 @@ Memtools + + Header Files + + + Header Files + @@ -594,6 +603,9 @@ Pawn Includes + + Pawn Includes + diff --git a/amxmodx/natives_handles.h b/amxmodx/natives_handles.h new file mode 100644 index 00000000..5e0b3b84 --- /dev/null +++ b/amxmodx/natives_handles.h @@ -0,0 +1,88 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +#include + +template +class Handle +{ + private: + + ke::Vector m_handles; + + public: + + Handle() {} + ~Handle() + { + this->clear(); + } + + void clear() + { + for (size_t i = 0; i < m_handles.length(); ++i) + { + if (m_handles[i]) + { + delete m_handles[i]; + } + } + + m_handles.clear(); + } + + T *lookup(int handle) + { + --handle; + + if (handle < 0 || handle >= static_cast(m_handles.length())) + { + return nullptr; + } + + return m_handles[handle]; + } + + int create() + { + for (size_t i = 0; i < m_handles.length(); ++i) + { + if (!m_handles[i]) + { + m_handles[i] = new T; + + return static_cast(i) + 1; + } + } + + m_handles.append(new T); + + return m_handles.length(); + } + + bool destroy(int handle) + { + handle--; + + if (handle < 0 || handle >= static_cast(m_handles.length())) + { + return false; + } + + if (!m_handles[handle]) + { + return false; + } + + delete m_handles[handle]; + m_handles[handle] = nullptr; + + return true; + } +}; \ No newline at end of file diff --git a/plugins/include/amxmodx.inc b/plugins/include/amxmodx.inc index b58239de..becae711 100755 --- a/plugins/include/amxmodx.inc +++ b/plugins/include/amxmodx.inc @@ -30,6 +30,7 @@ #include #include #include +#include /** * Called just after server activation. diff --git a/plugins/include/gameconfig.inc b/plugins/include/gameconfig.inc new file mode 100644 index 00000000..d3724e45 --- /dev/null +++ b/plugins/include/gameconfig.inc @@ -0,0 +1,93 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +// +// Game Config Functions +// + +#if defined _gameconfigs_included + #endinput +#endif +#define _gameconfigs_included + +enum GameConfig +{ + Invalid_GameConfig = 0 +}; + +/** + * Loads a game config file. + * + * @note The file path must be relative to the 'gamedata' folder under the data folder + * and the extension should be omitted. + * + * @param file File to load + * + * @return A handle to the game config file + */ +native GameConfig:LoadGameConfigFile(const file[]); + +/** + * Returns an offset value. + * + * @param handle Game config handle + * @param key Key to retrieve from the offset section + * + * @return An offset, or -1 on failure + * @error Invalid game config handle + */ +native GameConfGetOffset(GameConfig:handle, const key[]); + +/** + * Returns an offset value given a classname. + * + * @param handle Game config handle + * @param classname Class name to match from the offset section + * @param key Key to retrieve from the offset section + * + * @return An offset, or -1 on failure + * @error Invalid game config handle + */ +native GameConfGetClassOffset(GameConfig:handle, const classname[], const key[]); + +/** + * Gets the value of a key from the "Keys" section. + * + * @param handle Game config handle + * @param key Key to retrieve from the Keys section + * @param buffer Destination string buffer + * @param maxlen Maximum length of output string buffer + * + * @return True if key existed, false otherwise + * @error Invalid game config handle + */ +native bool:GameConfGetKeyValue(GameConfig:handle, const key[], buffer[], maxlen); + +/** + * Finds an address calculation in a GameConfig file. + * + * @param handle Game config handle + * @param name Name of the property to find + * + * @return An address calculated on success, otherwise 0 on failure. + * @error Invalid game config handle + */ +native GameConfGetAddress(GameConfig:handle, const name[]); + +/** + * Destroys a game config and frees its memory. + * + * @note The function automatically sets the variable passed to it to 0 to aid + * in preventing accidental usage after destroy. + * + * @param handle Game config handle + * + * @return 1 on success, 0 if an invalid handle was passed in + */ +native CloseGameConfigFile(&GameConfig:handle); \ No newline at end of file diff --git a/public/ITextParsers.h b/public/ITextParsers.h index 2a643d32..67e27a46 100644 --- a/public/ITextParsers.h +++ b/public/ITextParsers.h @@ -32,6 +32,8 @@ #ifndef _INCLUDE_SOURCEMOD_TEXTPARSERS_INTERFACE_H_ #define _INCLUDE_SOURCEMOD_TEXTPARSERS_INTERFACE_H_ +#include // size_t + /** * @file ITextParsers.h * @brief Defines various text/file parsing functions, as well as UTF-8 support code. diff --git a/public/memtools/MemoryUtils.cpp b/public/memtools/MemoryUtils.cpp index 99ec9a94..d99c1241 100644 --- a/public/memtools/MemoryUtils.cpp +++ b/public/memtools/MemoryUtils.cpp @@ -644,7 +644,7 @@ bool MemoryUtils::GetLibraryOfAddress(const void *libPtr, char *buffer, size_t m return false; } const char *dllpath = info.dli_fname; - UTIL_Format(buffer, maxlength, "%s", dllpath); + Format(buffer, maxlength, "%s", dllpath); if (base) { *base = (uintptr_t)info.dli_fbase; @@ -708,4 +708,20 @@ size_t MemoryUtils::DecodeHexString(unsigned char *buffer, size_t maxlength, con } return written; +} + +size_t MemoryUtils::Format(char *buffer, size_t maxlength, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + size_t len = vsnprintf(buffer, maxlength, fmt, ap); + va_end(ap); + + if (len >= maxlength) + { + buffer[maxlength - 1] = '\0'; + return (maxlength - 1); + } + + return len; } \ No newline at end of file diff --git a/public/memtools/MemoryUtils.h b/public/memtools/MemoryUtils.h index b7b49662..2f22255f 100644 --- a/public/memtools/MemoryUtils.h +++ b/public/memtools/MemoryUtils.h @@ -80,6 +80,7 @@ class MemoryUtils public: size_t DecodeHexString(unsigned char *buffer, size_t maxlength, const char *hexstr); + size_t Format(char *buffer, size_t maxlength, const char *fmt, ...); #if defined(__linux__) || defined(__APPLE__) private: diff --git a/public/sdk/amxxmodule.h b/public/sdk/amxxmodule.h index 5f0feab5..c9f9e75f 100644 --- a/public/sdk/amxxmodule.h +++ b/public/sdk/amxxmodule.h @@ -2377,7 +2377,7 @@ const char * MF_GetLocalInfo (const char *name, const char *def) { } int MF_AmxReRegister (AMX *amx, AMX_NATIVE_INFO *list, int number) { return 0; } void * MF_RegisterFunctionEx (void *pfn, const char *description) { } void * MF_MessageBlock (int mode, int msg, int *opt) { } -IGameConfigManager* MF_MessageBlock () { } +IGameConfigManager* MF_GetConfigManager (void) { } #endif // MAY_NEVER_BE_DEFINED #define MF_AddNatives g_fn_AddNatives diff --git a/support/PackageScript b/support/PackageScript index 826371de..692c043b 100644 --- a/support/PackageScript +++ b/support/PackageScript @@ -264,6 +264,7 @@ scripting_files = [ 'include/file.inc', 'include/float.inc', 'include/fun.inc', + 'include/gameconfig.inc', 'include/geoip.inc', 'include/lang.inc', 'include/ns.inc',