From d356d8053716ec99bf74fad34e710da2b3011ce3 Mon Sep 17 00:00:00 2001 From: nosoop Date: Mon, 5 Sep 2022 15:44:58 -0700 Subject: [PATCH] Add functions for working with entity lumps (#1673) --- bridge/include/LogicProvider.h | 3 + core/logic/AMBuilder | 2 + core/logic/LumpManager.cpp | 133 ++++++++++ core/logic/LumpManager.h | 116 +++++++++ core/logic/common_logic.cpp | 35 +++ core/logic/smn_entitylump.cpp | 367 +++++++++++++++++++++++++++ core/logic/smn_entitylump.h | 44 ++++ core/sourcemod.cpp | 27 +- core/sourcemod.h | 1 + plugins/include/entitylump.inc | 157 ++++++++++++ plugins/include/sourcemod.inc | 1 + plugins/testsuite/entitylumptest.sp | 125 +++++++++ tools/entlumpparser/AMBuildScript | 224 ++++++++++++++++ tools/entlumpparser/AMBuilder | 16 ++ tools/entlumpparser/configure.py | 16 ++ tools/entlumpparser/console_main.cpp | 22 ++ 16 files changed, 1288 insertions(+), 1 deletion(-) create mode 100644 core/logic/LumpManager.cpp create mode 100644 core/logic/LumpManager.h create mode 100644 core/logic/smn_entitylump.cpp create mode 100644 core/logic/smn_entitylump.h create mode 100644 plugins/include/entitylump.inc create mode 100644 plugins/testsuite/entitylumptest.sp create mode 100644 tools/entlumpparser/AMBuildScript create mode 100644 tools/entlumpparser/AMBuilder create mode 100644 tools/entlumpparser/configure.py create mode 100644 tools/entlumpparser/console_main.cpp diff --git a/bridge/include/LogicProvider.h b/bridge/include/LogicProvider.h index 966e73cf..7c01a0f2 100644 --- a/bridge/include/LogicProvider.h +++ b/bridge/include/LogicProvider.h @@ -73,6 +73,9 @@ struct sm_logic_t void (*FreeCellArray)(ICellArray *arr); void * (*FromPseudoAddress)(uint32_t pseudoAddr); uint32_t (*ToPseudoAddress)(void *addr); + void (*SetEntityLumpWritable)(bool writable); + bool (*ParseEntityLumpString)(const char *entityString, int &status, size_t &position); + const char * (*GetEntityLumpString)(); IScriptManager *scripts; IShareSys *sharesys; IExtensionSys *extsys; diff --git a/core/logic/AMBuilder b/core/logic/AMBuilder index bbc69f00..3af40f19 100644 --- a/core/logic/AMBuilder +++ b/core/logic/AMBuilder @@ -84,6 +84,8 @@ for cxx in builder.targets: 'smn_halflife.cpp', 'FrameIterator.cpp', 'DatabaseConfBuilder.cpp', + 'LumpManager.cpp', + 'smn_entitylump.cpp', ] if binary.compiler.target.arch == 'x86_64': diff --git a/core/logic/LumpManager.cpp b/core/logic/LumpManager.cpp new file mode 100644 index 00000000..d6b1eb7f --- /dev/null +++ b/core/logic/LumpManager.cpp @@ -0,0 +1,133 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * Entity Lump Manager + * Copyright (C) 2021-2022 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$ + */ + +#include "LumpManager.h" + +#include +#include + +EntityLumpParseResult::operator bool() const { + return m_Status == Status_OK; +} + +EntityLumpParseResult EntityLumpManager::Parse(const char* pMapEntities) { + m_Entities.clear(); + + std::istringstream mapEntities(pMapEntities); + + for (;;) { + std::string token; + mapEntities >> std::ws >> token >> std::ws; + + // Assert that we're at the start of a new block, otherwise we're done parsing + if (token != "{") { + if (token == "\0") { + break; + } else { + return EntityLumpParseResult { + Status_UnexpectedChar, mapEntities.tellg() + }; + } + } + + /** + * Parse key / value pairs until we reach a closing brace. We currently assume there + * are only quoted keys / values up to the next closing brace. + * + * The SDK suggests that there are cases that could use non-quoted symbols and nested + * braces (`shared/mapentities_shared.cpp::MapEntity_ParseToken`), but I haven't seen + * those in practice. + */ + EntityLumpEntry entry; + while (mapEntities.peek() != '}') { + std::string key, value; + + if (mapEntities.peek() != '"') { + return EntityLumpParseResult { + Status_UnexpectedChar, mapEntities.tellg() + }; + } + mapEntities >> quoted(key) >> std::ws; + + if (mapEntities.peek() != '"') { + return EntityLumpParseResult { + Status_UnexpectedChar, mapEntities.tellg() + }; + } + mapEntities >> quoted(value) >> std::ws; + + entry.emplace_back(key, value); + } + mapEntities.get(); + m_Entities.push_back(std::make_shared(entry)); + } + + return EntityLumpParseResult{}; +} + +std::string EntityLumpManager::Dump() { + std::ostringstream stream; + for (const auto& entry : m_Entities) { + // ignore empty entries + if (entry->empty()) { + continue; + } + stream << "{\n"; + for (const auto& pair : *entry) { + stream << '"' << pair.first << "\" \"" << pair.second << '"' << '\n'; + } + stream << "}\n"; + } + return stream.str(); +} + +std::weak_ptr EntityLumpManager::Get(size_t index) { + return m_Entities[index]; +} + +void EntityLumpManager::Erase(size_t index) { + m_Entities.erase(m_Entities.begin() + index); +} + +void EntityLumpManager::Insert(size_t index) { + m_Entities.emplace(m_Entities.begin() + index, std::make_shared()); +} + +size_t EntityLumpManager::Append() { + return std::distance( + m_Entities.begin(), + m_Entities.emplace(m_Entities.end(), std::make_shared()) + ); +} + +size_t EntityLumpManager::Length() { + return m_Entities.size(); +} diff --git a/core/logic/LumpManager.h b/core/logic/LumpManager.h new file mode 100644 index 00000000..432c4985 --- /dev/null +++ b/core/logic/LumpManager.h @@ -0,0 +1,116 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * Entity Lump Manager + * Copyright (C) 2021-2022 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_LUMPMANAGER_H_ +#define _INCLUDE_LUMPMANAGER_H_ + +#include +#include +#include + +/** + * Entity lump manager. Provides a list that stores a list of key / value pairs and the + * functionality to (de)serialize it from / to an entity string. + * This file and its corresponding .cpp should be compilable independently of SourceMod; + * the SourceMod interop is located within smn_entitylump. + * + * @file lumpmanager.h + * @brief Class definition for object that parses lumps. + */ + +/** + * @brief A container of key / value pairs. + */ +using EntityLumpEntry = std::vector>; + +enum EntityLumpParseStatus { + Status_OK, + Status_UnexpectedChar, +}; + +/** + * @brief Result of parsing an entity lump. On a parse error, m_Status is not Status_OK and + * m_Position indicates the offset within the string that caused the parse error. + */ +struct EntityLumpParseResult { + EntityLumpParseStatus m_Status; + std::streamoff m_Position; + + operator bool() const; + const char* Description() const; +}; + +/** + * @brief Manages entity lump entries. + */ +class EntityLumpManager +{ +public: + /** + * @brief Parses the map entities string into an internal representation. + */ + EntityLumpParseResult Parse(const char* pMapEntities); + + /** + * @brief Dumps the current internal representation out to an std::string. + */ + std::string Dump(); + + /** + * @brief Returns a weak reference to an EntityLumpEntry. Used for handles on the scripting side. + */ + std::weak_ptr Get(size_t index); + + /** + * @brief Removes an EntityLumpEntry at the given index, shifting down all entries after it by one. + */ + void Erase(size_t index); + + /** + * @brief Inserts a new EntityLumpEntry at the given index, shifting up the entries previously at the index and after it up by one. + */ + void Insert(size_t index); + + /** + * @brief Adds a new EntityLumpEntry to the end. Returns the index of the entry. + */ + size_t Append(); + + /** + * @brief Returns the number of EntityLumpEntry items in the list. + */ + size_t Length(); + +private: + std::vector> m_Entities; +}; + +#endif // _INCLUDE_LUMPMANAGER_H_ diff --git a/core/logic/common_logic.cpp b/core/logic/common_logic.cpp index e82bc549..24e95092 100644 --- a/core/logic/common_logic.cpp +++ b/core/logic/common_logic.cpp @@ -56,6 +56,7 @@ #include "LibrarySys.h" #include "RootConsoleMenu.h" #include "CellArray.h" +#include "smn_entitylump.h" #include #include @@ -89,6 +90,8 @@ CNativeOwner g_CoreNatives; PseudoAddressManager pseudoAddr; #endif +EntityLumpParseResult lastParseResult; + static void AddCorePhraseFile(const char *filename) { g_pCorePhrases->AddPhraseFile(filename); @@ -135,6 +138,35 @@ static uint32_t ToPseudoAddress(void *addr) #endif } +static void SetEntityLumpWritable(bool writable) +{ + g_bLumpAvailableForWriting = writable; + + // write-lock causes the map entities to be serialized out to string + if (!writable) + { + g_strMapEntities = lumpmanager->Dump(); + } +} + +static bool ParseEntityLumpString(const char *pMapEntities, int &status, size_t &position) +{ + lastParseResult = lumpmanager->Parse(pMapEntities); + status = static_cast(lastParseResult.m_Status); + position = static_cast(lastParseResult.m_Position); + return lastParseResult; +} + +// returns nullptr if the original lump failed to parse +static const char* GetEntityLumpString() +{ + if (!lastParseResult) + { + return nullptr; + } + return g_strMapEntities.c_str(); +} + // Defined in smn_filesystem.cpp. extern bool OnLogPrint(const char *msg); @@ -170,6 +202,9 @@ static sm_logic_t logic = CellArray::Free, FromPseudoAddress, ToPseudoAddress, + SetEntityLumpWritable, + ParseEntityLumpString, + GetEntityLumpString, &g_PluginSys, &g_ShareSys, &g_Extensions, diff --git a/core/logic/smn_entitylump.cpp b/core/logic/smn_entitylump.cpp new file mode 100644 index 00000000..ee923235 --- /dev/null +++ b/core/logic/smn_entitylump.cpp @@ -0,0 +1,367 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * Entity Lump Manager + * Copyright (C) 2021-2022 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$ + */ + +#include "HandleSys.h" +#include "common_logic.h" + +#include "LumpManager.h" + +#include + +HandleType_t g_EntityLumpEntryType; + +std::string g_strMapEntities; +bool g_bLumpAvailableForWriting = false; + +static EntityLumpManager s_LumpManager; +EntityLumpManager *lumpmanager = &s_LumpManager; + +class LumpManagerNatives : + public IHandleTypeDispatch, + public SMGlobalClass +{ +public: //SMGlobalClass + void OnSourceModAllInitialized() + { + g_EntityLumpEntryType = handlesys->CreateType("EntityLumpEntry", this, 0, NULL, NULL, g_pCoreIdent, NULL); + } + void OnSourceModShutdown() + { + handlesys->RemoveType(g_EntityLumpEntryType, g_pCoreIdent); + } +public: //IHandleTypeDispatch + void OnHandleDestroy(HandleType_t type, void* object) + { + if (type == g_EntityLumpEntryType) + { + delete reinterpret_cast*>(object); + } + } +}; + +static LumpManagerNatives s_LumpManagerNatives; + +cell_t sm_LumpManagerGet(IPluginContext *pContext, const cell_t *params) { + int index = params[1]; + if (index < 0 || index >= static_cast(lumpmanager->Length())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + std::weak_ptr* pReference = new std::weak_ptr; + *pReference = lumpmanager->Get(index); + + return handlesys->CreateHandle(g_EntityLumpEntryType, pReference, + pContext->GetIdentity(), g_pCoreIdent, NULL); +} + +cell_t sm_LumpManagerErase(IPluginContext *pContext, const cell_t *params) { + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLump.Erase() outside of OnMapInit"); + } + + int index = params[1]; + if (index < 0 || index >= static_cast(lumpmanager->Length())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + lumpmanager->Erase(index); + return 0; +} + +cell_t sm_LumpManagerInsert(IPluginContext *pContext, const cell_t *params) { + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLump.Insert() outside of OnMapInit"); + } + + int index = params[1]; + if (index < 0 || index > static_cast(lumpmanager->Length())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + lumpmanager->Insert(index); + return 0; +} + +cell_t sm_LumpManagerAppend(IPluginContext *pContext, const cell_t *params) { + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLump.Append() outside of OnMapInit"); + } + return lumpmanager->Append(); +} + +cell_t sm_LumpManagerLength(IPluginContext *pContext, const cell_t *params) { + return lumpmanager->Length(); +} + +cell_t sm_LumpEntryGet(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + auto entry = entryref->lock(); + + int index = params[2]; + if (index < 0 || index >= static_cast(entry->size())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + const auto& pair = (*entry)[index]; + + size_t nBytes; + pContext->StringToLocalUTF8(params[3], params[4], pair.first.c_str(), &nBytes); + pContext->StringToLocalUTF8(params[5], params[6], pair.second.c_str(), &nBytes); + + return 0; +} + +cell_t sm_LumpEntryUpdate(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLumpEntry.Update() outside of OnMapInit"); + } + + auto entry = entryref->lock(); + + int index = params[2]; + if (index < 0 || index >= static_cast(entry->size())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + char *key, *value; + pContext->LocalToStringNULL(params[3], &key); + pContext->LocalToStringNULL(params[4], &value); + + auto& pair = (*entry)[index]; + if (key != nullptr) { + pair.first = key; + } + if (value != nullptr) { + pair.second = value; + } + + return 0; +} + +cell_t sm_LumpEntryInsert(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLumpEntry.Insert() outside of OnMapInit"); + } + + auto entry = entryref->lock(); + + int index = params[2]; + if (index < 0 || index > static_cast(entry->size())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + char *key, *value; + pContext->LocalToString(params[3], &key); + pContext->LocalToString(params[4], &value); + + entry->emplace(entry->begin() + index, key, value); + + return 0; +} + +cell_t sm_LumpEntryErase(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLumpEntry.Erase() outside of OnMapInit"); + } + + auto entry = entryref->lock(); + + int index = params[2]; + if (index < 0 || index >= static_cast(entry->size())) { + return pContext->ThrowNativeError("Invalid index %d", index); + } + + entry->erase(entry->begin() + index); + + return 0; +} + +cell_t sm_LumpEntryAppend(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + if (!g_bLumpAvailableForWriting) { + return pContext->ThrowNativeError("Cannot use EntityLumpEntry.Append() outside of OnMapInit"); + } + + auto entry = entryref->lock(); + + char *key, *value; + pContext->LocalToString(params[2], &key); + pContext->LocalToString(params[3], &value); + + entry->emplace_back(key, value); + + return 0; +} + +cell_t sm_LumpEntryFindKey(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + // start from the index after the current one + int start = params[3] + 1; + + auto entry = entryref->lock(); + + if (start < 0 || start >= static_cast(entry->size())) { + return -1; + } + + char *key; + pContext->LocalToString(params[2], &key); + + auto matches_key = [&key](std::pair pair) { + return pair.first == key; + }; + + auto result = std::find_if(entry->begin() + start, entry->end(), matches_key); + + if (result == entry->end()) { + return -1; + } + return std::distance(entry->begin(), result); +} + +cell_t sm_LumpEntryLength(IPluginContext *pContext, const cell_t *params) { + HandleSecurity sec(pContext->GetIdentity(), g_pCoreIdent); + HandleError err; + + Handle_t hndl = static_cast(params[1]); + + std::weak_ptr* entryref = nullptr; + if ((err = handlesys->ReadHandle(hndl, g_EntityLumpEntryType, &sec, (void**) &entryref)) != HandleError_None) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (error: %d)", hndl, err); + } + + if (entryref->expired()) { + return pContext->ThrowNativeError("Invalid EntityLumpEntry handle %x (reference expired)", hndl); + } + + auto entry = entryref->lock(); + return entry->size(); +} + +REGISTER_NATIVES(entityLumpNatives) +{ + { "EntityLump.Get", sm_LumpManagerGet }, + { "EntityLump.Erase", sm_LumpManagerErase }, + { "EntityLump.Insert", sm_LumpManagerInsert }, + { "EntityLump.Append", sm_LumpManagerAppend }, + { "EntityLump.Length", sm_LumpManagerLength }, + + { "EntityLumpEntry.Get", sm_LumpEntryGet }, + { "EntityLumpEntry.Update", sm_LumpEntryUpdate }, + { "EntityLumpEntry.Insert", sm_LumpEntryInsert }, + { "EntityLumpEntry.Erase", sm_LumpEntryErase }, + { "EntityLumpEntry.Append", sm_LumpEntryAppend }, + { "EntityLumpEntry.FindKey", sm_LumpEntryFindKey }, + { "EntityLumpEntry.Length.get", sm_LumpEntryLength }, + + {NULL, NULL} +}; diff --git a/core/logic/smn_entitylump.h b/core/logic/smn_entitylump.h new file mode 100644 index 00000000..4b42be56 --- /dev/null +++ b/core/logic/smn_entitylump.h @@ -0,0 +1,44 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod +* Copyright (C) 2022-2022 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_SOURCEMOD_ENTITYLUMP_H_ +#define _INCLUDE_SOURCEMOD_ENTITYLUMP_H_ + +#include +#include "LumpManager.h" + +using namespace SourceMod; + +extern std::string g_strMapEntities; +extern bool g_bLumpAvailableForWriting; +extern EntityLumpManager *lumpmanager; + +#endif // _INCLUDE_SOURCEMOD_ENTITYLUMP_H_ \ No newline at end of file diff --git a/core/sourcemod.cpp b/core/sourcemod.cpp index eb55ed75..e27dfbf3 100644 --- a/core/sourcemod.cpp +++ b/core/sourcemod.cpp @@ -54,6 +54,7 @@ SH_DECL_HOOK0_void(IServerGameDLL, LevelShutdown, SH_NOATTRIB, false); SH_DECL_HOOK1_void(IServerGameDLL, GameFrame, SH_NOATTRIB, false, bool); SH_DECL_HOOK1_void(IServerGameDLL, Think, SH_NOATTRIB, false, bool); SH_DECL_HOOK1_void(IVEngineServer, ServerCommand, SH_NOATTRIB, false, const char *); +SH_DECL_HOOK0(IVEngineServer, GetMapEntitiesString, SH_NOATTRIB, 0, const char *); SourceModBase g_SourceMod; @@ -278,6 +279,7 @@ bool SourceModBase::InitializeSourceMod(char *error, size_t maxlength, bool late /* Hook this now so we can detect startup without calling StartSourceMod() */ SH_ADD_HOOK(IServerGameDLL, LevelInit, gamedll, SH_MEMBER(this, &SourceModBase::LevelInit), false); + SH_ADD_HOOK(IVEngineServer, GetMapEntitiesString, engine, SH_MEMBER(this, &SourceModBase::GetMapEntitiesString), false); /* Only load if we're not late */ if (!late) @@ -416,10 +418,32 @@ bool SourceModBase::LevelInit(char const *pMapName, char const *pMapEntities, ch g_LevelEndBarrier = true; + int parseError; + size_t position; + bool success = logicore.ParseEntityLumpString(pMapEntities, parseError, position); + + logicore.SetEntityLumpWritable(true); g_pOnMapInit->PushString(pMapName); g_pOnMapInit->Execute(); + logicore.SetEntityLumpWritable(false); - RETURN_META_VALUE(MRES_IGNORED, true); + if (!success) + { + logger->LogError("Map entity lump parsing for %s failed with error code %d on position %d", pMapName, parseError, position); + RETURN_META_VALUE(MRES_IGNORED, true); + } + + RETURN_META_VALUE_NEWPARAMS(MRES_HANDLED, true, &IServerGameDLL::LevelInit, (pMapName, logicore.GetEntityLumpString(), pOldLevel, pLandmarkName, loadGame, background)); +} + +const char *SourceModBase::GetMapEntitiesString() +{ + const char *pNewMapEntities = logicore.GetEntityLumpString(); + if (pNewMapEntities != nullptr) + { + RETURN_META_VALUE(MRES_SUPERCEDE, pNewMapEntities); + } + RETURN_META_VALUE(MRES_IGNORED, NULL); } void SourceModBase::LevelShutdown() @@ -534,6 +558,7 @@ void SourceModBase::CloseSourceMod() return; SH_REMOVE_HOOK(IServerGameDLL, LevelInit, gamedll, SH_MEMBER(this, &SourceModBase::LevelInit), false); + SH_REMOVE_HOOK(IVEngineServer, GetMapEntitiesString, engine, SH_MEMBER(this, &SourceModBase::GetMapEntitiesString), false); if (g_Loaded) { diff --git a/core/sourcemod.h b/core/sourcemod.h index ee72872d..0b4eb23e 100644 --- a/core/sourcemod.h +++ b/core/sourcemod.h @@ -140,6 +140,7 @@ public: // ISourceMod private: void ShutdownServices(); private: + const char* GetMapEntitiesString(); char m_SMBaseDir[PLATFORM_MAX_PATH]; char m_SMRelDir[PLATFORM_MAX_PATH]; char m_ModDir[32]; diff --git a/plugins/include/entitylump.inc b/plugins/include/entitylump.inc new file mode 100644 index 00000000..9d269e60 --- /dev/null +++ b/plugins/include/entitylump.inc @@ -0,0 +1,157 @@ +#if defined _entitylump_included + #endinput +#endif + +#define _entitylump_included + +/** + * An ordered list of key / value pairs for a map entity. + * If the entry in the EntityLump is removed, the handle will error on all operations. + * (The handle will remain valid on the scripting side, and will still need to be deleted.) + * + * Write operations (update, insert, erase, append) are only allowed during OnMapInit. + */ +methodmap EntityLumpEntry < Handle { + /** + * Copies the key / value at the given index into buffers. + * + * @param index Position, starting from 0. + * @param keybuf Key name buffer. + * @param keylen Maximum length of the key name buffer. + * @param valbuf Value buffer. + * @param vallen Maximum length of the value buffer. + * @error Index is out of bounds. + */ + public native void Get(int index, char[] keybuf = "", int keylen = 0, char[] valbuf = "", int vallen = 0); + + /** + * Updates the key / value pair at the given index. + * + * @param index Position, starting from 0. + * @param key New key name, or NULL_STRING to preserve the existing key name. + * @param value New value, or NULL_STRING to preserve the existing value. + * @error Index is out of bounds or entity lump is read-only. + */ + public native void Update(int index, const char[] key = NULL_STRING, const char[] value = NULL_STRING); + + /** + * Inserts a new key / value pair at the given index, shifting the pair at that index and beyond up. + * If EntityLumpEntry.Length is passed in, this is an append operation. + * + * @param index Position, starting from 0. + * @param key New key name. + * @param value New value. + * @error Index is out of bounds or entity lump is read-only. + */ + public native void Insert(int index, const char[] key, const char[] value); + + /** + * Removes the key / value pair at the given index, shifting all entries past it down. + * + * @param index Position, starting from 0. + * @error Index is out of bounds or entity lump is read-only. + */ + public native void Erase(int index); + + /** + * Inserts a new key / value pair at the end of the entry's list. + * + * @param key New key name. + * @param value New value. + * @error Index is out of bounds or entity lump is read-only. + */ + public native void Append(const char[] key, const char[] value); + + /** + * Searches the entry list for an index matching a key starting from a position. + * + * @param key Key name to search. + * @param start A position after which to begin searching from. Use -1 to start from the + * first entry. + * @return Position after start with an entry matching the given key, or -1 if no + * match was found. + * @error Invalid start position; must be a value between -1 and one less than the + * length of the entry. + */ + public native int FindKey(const char[] key, int start = -1); + + /** + * Searches the entry list for an index matching a key starting from a position. + * This also copies the value from that index into the given buffer. + * + * This can be used to find the first / only value matching a key, or to iterate over all + * the values that match said key. + * + * @param key Key name to search. + * @param buffer Value buffer. This will contain the result of the next match, or empty + * if no match was found. + * @param maxlen Maximum length of the value buffer. + * @param start An index after which to begin searching from. Use -1 to start from the + * first entry. + * @return Position after start with an entry matching the given key, or -1 if no + * match was found. + * @error Invalid start position; must be a value between -1 and one less than the + * length of the entry. + */ + public int GetNextKey(const char[] key, char[] buffer, int maxlen, int start = -1) { + int result = this.FindKey(key, start); + if (result != -1) { + this.Get(result, .valbuf = buffer, .vallen = maxlen); + } else { + buffer[0] = '\0'; + } + return result; + } + + /** + * Retrieves the number of key / value pairs in the entry. + */ + property int Length { + public native get(); + } +}; + +/** + * A group of natives for a singleton entity lump, representing all the entities defined in the map. + * + * Write operations (insert, erase, append) are only allowed during OnMapInit. + */ +methodmap EntityLump { + /** + * Returns the EntityLumpEntry at the given index. + * This handle should be freed by the calling plugin. + * + * @param index Position, starting from 0. + * @error Index is out of bounds. + */ + public static native EntityLumpEntry Get(int index); + + /** + * Erases an EntityLumpEntry at the given index, shifting all entries past it down. + * Any handles referencing the erased EntityLumpEntry will throw on any operations aside from delete. + * + * @param index Position, starting from 0. + * @error Index is out of bounds or entity lump is read-only. + */ + public static native void Erase(int index); + + /** + * Inserts an empty EntityLumpEntry at the given index, shifting the existing entry and ones past it up. + * + * @param index Position, starting from 0. + * @error Index is out of bounds or entity lump is read-only. + */ + public static native void Insert(int index); + + /** + * Creates an empty EntityLumpEntry, returning its index. + * + * @error Entity lump is read-only. + */ + public static native int Append(); + + /** + * Returns the number of entities currently in the lump. + */ + public static native int Length(); +}; diff --git a/plugins/include/sourcemod.inc b/plugins/include/sourcemod.inc index 1a704bd9..6da1cf67 100644 --- a/plugins/include/sourcemod.inc +++ b/plugins/include/sourcemod.inc @@ -76,6 +76,7 @@ struct Plugin #include #include #include +#include enum APLRes { diff --git a/plugins/testsuite/entitylumptest.sp b/plugins/testsuite/entitylumptest.sp new file mode 100644 index 00000000..0f5457ef --- /dev/null +++ b/plugins/testsuite/entitylumptest.sp @@ -0,0 +1,125 @@ +#pragma semicolon 1 +#include + +#include +#include + +#pragma newdecls required + +#define PLUGIN_VERSION "0.0.0" +public Plugin myinfo = { + name = "Entity Lump Core Native Test", + author = "nosoop", + description = "A port of the Level KeyValues entity test to the Entity Lump implementation in core SourceMod", +} + +#define OUTPUT_NAME "OnCapTeam2" + +public void OnMapInit() { + // set every area_time_to_cap value to 30 + for (int i, n = EntityLump.Length(); i < n; i++) { + EntityLumpEntry entry = EntityLump.Get(i); + + int ttc = entry.FindKey("area_time_to_cap"); + if (ttc != -1) { + entry.Update(ttc, NULL_STRING, "30"); + PrintToServer("Set time to cap for item %d to 30", i); + } + + delete entry; + } +} + +public void OnMapStart() { + int captureArea = FindEntityByClassname(-1, "trigger_capture_area"); + + if (!IsValidEntity(captureArea)) { + LogMessage("---- %s", "No capture area"); + return; + } + + int hammerid = GetEntProp(captureArea, Prop_Data, "m_iHammerID"); + EntityLumpEntry entry = FindEntityLumpEntryByHammerID(hammerid); + + if (!entry) { + return; + } + + LogMessage("---- %s", "Found a trigger_capture_area with keys:"); + + for (int i, n = entry.Length; i < n; i++) { + char keyBuffer[128], valueBuffer[128]; + entry.Get(i, keyBuffer, sizeof(keyBuffer), valueBuffer, sizeof(valueBuffer)); + + LogMessage("%s -> %s", keyBuffer, valueBuffer); + } + + LogMessage("---- %s", "List of " ... OUTPUT_NAME ... " outputs:"); + char outputString[256]; + for (int k = -1; (k = entry.GetNextKey(OUTPUT_NAME, outputString, sizeof(outputString), k)) != -1;) { + char targetName[32], inputName[64], variantValue[32]; + float delay; + int nFireCount; + + ParseEntityOutputString(outputString, targetName, sizeof(targetName), + inputName, sizeof(inputName), variantValue, sizeof(variantValue), + delay, nFireCount); + + LogMessage("target %s -> input %s (value %s, delay %.2f, refire %d)", + targetName, inputName, variantValue, delay, nFireCount); + } + delete entry; +} + +/** + * Returns the first EntityLumpEntry with a matching hammerid. + */ +EntityLumpEntry FindEntityLumpEntryByHammerID(int hammerid) { + for (int i, n = EntityLump.Length(); i < n; i++) { + EntityLumpEntry entry = EntityLump.Get(i); + + char value[32]; + if (entry.GetNextKey("hammerid", value, sizeof(value)) != -1 + && StringToInt(value) == hammerid) { + return entry; + } + delete entry; + } + return null; +} + +/** + * Parses an entity's output value (as formatted in the entity string). + * Refer to https://developer.valvesoftware.com/wiki/AddOutput for the format. + * + * @return True if the output string was successfully parsed, false if not. + */ +stock bool ParseEntityOutputString(const char[] output, char[] targetName, int targetNameLength, + char[] inputName, int inputNameLength, char[] variantValue, int variantValueLength, + float &delay, int &nFireCount) { + int delimiter; + char buffer[32]; + + { + // validate that we have something resembling an output string (four commas) + int i, c, nDelim; + while ((c = FindCharInString(output[i], ',')) != -1) { + nDelim++; + i += c + 1; + } + if (nDelim < 4) { + return false; + } + } + + delimiter = SplitString(output, ",", targetName, targetNameLength); + delimiter += SplitString(output[delimiter], ",", inputName, inputNameLength); + delimiter += SplitString(output[delimiter], ",", variantValue, variantValueLength); + + delimiter += SplitString(output[delimiter], ",", buffer, sizeof(buffer)); + delay = StringToFloat(buffer); + + nFireCount = StringToInt(output[delimiter]); + + return true; +} diff --git a/tools/entlumpparser/AMBuildScript b/tools/entlumpparser/AMBuildScript new file mode 100644 index 00000000..a03b5f55 --- /dev/null +++ b/tools/entlumpparser/AMBuildScript @@ -0,0 +1,224 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: +import os, sys + +# Simple extensions do not need to modify this file. + +def ResolveEnvPath(env, folder): + if env in os.environ: + path = os.environ[env] + if os.path.isdir(path): + return path + return None + + head = os.getcwd() + oldhead = None + while head != None and head != oldhead: + path = os.path.join(head, folder) + if os.path.isdir(path): + return path + oldhead = head + head, tail = os.path.split(head) + + return None + +def Normalize(path): + return os.path.abspath(os.path.normpath(path)) + +class ProgramConfig(object): + def __init__(self): + self.binaries = [] + self.sm_root = None + + @property + def tag(self): + if builder.options.debug == '1': + return 'Debug' + return 'Release' + + def configure(self): + cxx = builder.DetectCompilers() + + if builder.options.sm_path: + self.sm_root = builder.options.sm_path + if not self.sm_root or not os.path.isdir(self.sm_root): + raise Exception('Could not find a source copy of SourceMod') + + if cxx.like('gcc'): + self.configure_gcc(cxx) + elif cxx.vendor == 'msvc': + self.configure_msvc(cxx) + + # Optimization + if builder.options.opt == '1': + cxx.defines += ['NDEBUG'] + + # Debugging + if builder.options.debug == '1': + cxx.defines += ['DEBUG', '_DEBUG'] + + # Platform-specifics + if builder.target_platform == 'linux': + self.configure_linux(cxx) + elif builder.target_platform == 'mac': + self.configure_mac(cxx) + elif builder.target_platform == 'windows': + self.configure_windows(cxx) + + # Finish up. + cxx.includes += [ + os.path.join(self.sm_root, 'public'), + ] + + def configure_gcc(self, cxx): + cxx.defines += [ + 'stricmp=strcasecmp', + '_stricmp=strcasecmp', + '_snprintf=snprintf', + '_vsnprintf=vsnprintf', + 'HAVE_STDINT_H', + 'GNUC', + ] + cxx.cflags += [ + '-pipe', + '-fno-strict-aliasing', + '-Wall', + '-Werror', + '-Wno-unused', + '-Wno-switch', + '-Wno-array-bounds', + '-msse', + '-m32', + '-fvisibility=hidden', + ] + cxx.cxxflags += [ + '-std=c++14', + '-fno-exceptions', + '-fno-threadsafe-statics', + '-Wno-non-virtual-dtor', + '-Wno-overloaded-virtual', + '-fvisibility-inlines-hidden', + ] + cxx.linkflags += ['-m32'] + + have_gcc = cxx.vendor == 'gcc' + have_clang = cxx.vendor == 'clang' + if cxx.version >= 'clang-3.9' or cxx.version == 'clang-3.4' or cxx.version > 'apple-clang-6.0': + cxx.cxxflags += ['-Wno-expansion-to-defined'] + if cxx.version >= 'clang-3.6': + cxx.cxxflags += ['-Wno-inconsistent-missing-override'] + if have_clang or (cxx.version >= 'gcc-4.6'): + cxx.cflags += ['-Wno-narrowing'] + if have_clang or (cxx.version >= 'gcc-4.7'): + cxx.cxxflags += ['-Wno-delete-non-virtual-dtor'] + if cxx.version >= 'gcc-4.8': + cxx.cflags += ['-Wno-unused-result'] + + if have_clang: + cxx.cxxflags += ['-Wno-implicit-exception-spec-mismatch'] + if cxx.version >= 'apple-clang-5.1' or cxx.version >= 'clang-3.4': + cxx.cxxflags += ['-Wno-deprecated-register'] + else: + cxx.cxxflags += ['-Wno-deprecated'] + cxx.cflags += ['-Wno-sometimes-uninitialized'] + + if have_gcc: + cxx.cflags += ['-mfpmath=sse'] + + if builder.options.opt == '1': + cxx.cflags += ['-O3'] + + def configure_msvc(self, cxx): + if builder.options.debug == '1': + cxx.cflags += ['/MTd'] + cxx.linkflags += ['/NODEFAULTLIB:libcmt'] + else: + cxx.cflags += ['/MT'] + cxx.defines += [ + '_CRT_SECURE_NO_DEPRECATE', + '_CRT_SECURE_NO_WARNINGS', + '_CRT_NONSTDC_NO_DEPRECATE', + '_ITERATOR_DEBUG_LEVEL=0', + ] + cxx.cflags += [ + '/W3', + ] + cxx.cxxflags += [ + '/EHsc', + '/GR-', + '/TP', + ] + cxx.linkflags += [ + '/MACHINE:X86', + 'kernel32.lib', + 'user32.lib', + 'gdi32.lib', + 'winspool.lib', + 'comdlg32.lib', + 'advapi32.lib', + 'shell32.lib', + 'ole32.lib', + 'oleaut32.lib', + 'uuid.lib', + 'odbc32.lib', + 'odbccp32.lib', + ] + + if builder.options.opt == '1': + cxx.cflags += ['/Ox', '/Zo'] + cxx.linkflags += ['/OPT:ICF', '/OPT:REF'] + + if builder.options.debug == '1': + cxx.cflags += ['/Od', '/RTC1'] + + # This needs to be after our optimization flags which could otherwise disable it. + # Don't omit the frame pointer. + cxx.cflags += ['/Oy-'] + + def configure_linux(self, cxx): + cxx.defines += ['_LINUX', 'POSIX'] + cxx.linkflags += ['-Wl,--exclude-libs,ALL', '-lm'] + if cxx.vendor == 'gcc': + cxx.linkflags += ['-static-libgcc'] + elif cxx.vendor == 'clang': + cxx.linkflags += ['-lgcc_eh'] + + def configure_mac(self, cxx): + cxx.defines += ['OSX', '_OSX', 'POSIX'] + cxx.cflags += ['-mmacosx-version-min=10.5'] + cxx.linkflags += [ + '-mmacosx-version-min=10.5', + '-arch', 'i386', + '-lstdc++', + '-stdlib=libstdc++', + ] + cxx.cxxflags += ['-stdlib=libstdc++'] + + def configure_windows(self, cxx): + cxx.defines += ['WIN32', '_WINDOWS'] + + def Program(self, context, name): + binary = context.compiler.Program(name) + if binary.compiler.like('msvc'): + binary.compiler.linkflags.append('/SUBSYSTEM:CONSOLE') + return binary + +Tool = ProgramConfig() +Tool.configure() + +# Add additional buildscripts here +BuildScripts = [ + 'AMBuilder', +] + +binary = Tool.Program(builder, 'entlump_parser') +binary.sources += [ + 'console_main.cpp', + os.path.join(builder.options.sm_path, 'core', 'logic', 'LumpManager.cpp'), +] + +binary.compiler.includes += [ + os.path.join(builder.sourcePath), + os.path.join(builder.options.sm_path, 'core', 'logic'), +] + +builder.Add(binary) diff --git a/tools/entlumpparser/AMBuilder b/tools/entlumpparser/AMBuilder new file mode 100644 index 00000000..54be892a --- /dev/null +++ b/tools/entlumpparser/AMBuilder @@ -0,0 +1,16 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: +import os + +binary = Tool.Program(builder, 'entlump_parser') + +binary.sources += [ + 'console_main.cpp', + os.path.join(builder.options.sm_path, 'core', 'logic', 'LumpManager.cpp'), +] + +binary.compiler.includes += [ + os.path.join(builder.sourcePath), + os.path.join(builder.options.sm_path, 'core', 'logic'), +] + +builder.Add(binary) diff --git a/tools/entlumpparser/configure.py b/tools/entlumpparser/configure.py new file mode 100644 index 00000000..76678d4b --- /dev/null +++ b/tools/entlumpparser/configure.py @@ -0,0 +1,16 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et: +import sys +from ambuild2 import run + +# Simple extensions do not need to modify this file. + +builder = run.PrepareBuild(sourcePath = sys.path[0]) + +builder.options.add_option('--sm-path', type=str, dest='sm_path', default=None, + help='Path to SourceMod') +builder.options.add_option('--enable-debug', action='store_const', const='1', dest='debug', + help='Enable debugging symbols') +builder.options.add_option('--enable-optimize', action='store_const', const='1', dest='opt', + help='Enable optimization') + +builder.Configure() diff --git a/tools/entlumpparser/console_main.cpp b/tools/entlumpparser/console_main.cpp new file mode 100644 index 00000000..4ca35457 --- /dev/null +++ b/tools/entlumpparser/console_main.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +#include "LumpManager.h" + +int main(int argc, char *argv[]) { + if (argc < 2) { + std::cout << "Missing input file\n"; + return 0; + } + + const char* filepath = argv[1]; + + std::ifstream input(filepath, std::ios_base::binary); + std::string data((std::istreambuf_iterator(input)), std::istreambuf_iterator()); + + EntityLumpManager lumpmgr; + lumpmgr.Parse(data.c_str()); + + std::cout << lumpmgr.Dump() << "\n"; +}