From b6ce1e732beb4d9ab17ccbe5a4c72562f220f5d5 Mon Sep 17 00:00:00 2001 From: Krixx Date: Mon, 6 Oct 2025 03:44:10 +0200 Subject: [PATCH 1/3] wip: name resolver --- KX-Vision.vcxproj | 2 + src/Core/Config.h | 4 + src/Game/AddressManager.cpp | 24 +++ src/Game/AddressManager.h | 3 + src/Game/NameResolver.cpp | 185 ++++++++++++++++++ src/Game/NameResolver.h | 51 +++++ src/Hooking/Hooks.cpp | 87 ++++++-- src/Rendering/Extractors/EntityExtractor.cpp | 7 + .../Utils/ESPEntityDetailsBuilder.cpp | 7 +- 9 files changed, 354 insertions(+), 16 deletions(-) create mode 100644 src/Game/NameResolver.cpp create mode 100644 src/Game/NameResolver.h diff --git a/KX-Vision.vcxproj b/KX-Vision.vcxproj index 7f4190b..50d755b 100644 --- a/KX-Vision.vcxproj +++ b/KX-Vision.vcxproj @@ -100,6 +100,7 @@ + @@ -162,6 +163,7 @@ + diff --git a/src/Core/Config.h b/src/Core/Config.h index e3f24ff..2625b32 100644 --- a/src/Core/Config.h +++ b/src/Core/Config.h @@ -22,4 +22,8 @@ namespace kx { constexpr std::string_view CONTEXT_COLLECTION_FUNC_PATTERN = "8B ? ? ? ? ? 65 ? ? ? ? ? ? ? ? BA ? ? ? ? 48 ? ? ? 48 ? ? ? C3"; constexpr std::string_view ALERT_CONTEXT_LOCATOR_PATTERN = "48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 41 0F 28 CA 48 8B 08 48 8B 51 58"; // "ViewAdvanceAlert" + // This pattern needs to be found for the string \"resultFunc\" to locate the DecodeText function. + // It is required for NPC/Object name decoding. + constexpr std::string_view DECODE_TEXT_PATTERN = "? ? 48 8B F2 48 8B F9 48 85 C9 ? ? 41 B8 D7"; + } // namespace kx \ No newline at end of file diff --git a/src/Game/AddressManager.cpp b/src/Game/AddressManager.cpp index e1808ca..e51b1e6 100644 --- a/src/Game/AddressManager.cpp +++ b/src/Game/AddressManager.cpp @@ -196,6 +196,7 @@ void AddressManager::Scan() { ScanModuleInformation(); ScanContextCollectionFunc(); ScanGameThreadUpdateFunc(); + ScanDecodeTextFunc(); // currently unused //ScanAgentArray(); @@ -203,6 +204,29 @@ void AddressManager::Scan() { //ScanBgfxContextFunc(); } +void AddressManager::ScanDecodeTextFunc() { + if (kx::DECODE_TEXT_PATTERN.empty()) { + LOG_WARN("[AddressManager] DecodeText pattern is empty. Name resolution for NPCs/Objects will fail."); + s_pointers.decodeTextFunc = 0; + return; + } + + std::optional patternMatch = kx::PatternScanner::FindPattern( + std::string(kx::DECODE_TEXT_PATTERN), + std::string(kx::TARGET_PROCESS_NAME) + ); + + if (!patternMatch) { + LOG_ERROR("[AddressManager] DecodeText pattern not found. Name resolution for NPCs/Objects will fail."); + s_pointers.decodeTextFunc = 0; + return; + } + + // The signature is assumed to start at the function entry point. + s_pointers.decodeTextFunc = *patternMatch - 16; + LOG_INFO("[AddressManager] -> SUCCESS: DecodeText function resolved to: 0x%p", (void*)s_pointers.decodeTextFunc); +} + void AddressManager::Initialize() { Scan(); } diff --git a/src/Game/AddressManager.h b/src/Game/AddressManager.h index c09161b..4f62aeb 100644 --- a/src/Game/AddressManager.h +++ b/src/Game/AddressManager.h @@ -42,6 +42,7 @@ struct GamePointers { uintptr_t bgfxContextFunc = 0; uintptr_t contextCollectionFunc = 0; uintptr_t gameThreadUpdateFunc = 0; + uintptr_t decodeTextFunc = 0; void* pContextCollection = nullptr; // Module information for VTable validation @@ -62,6 +63,7 @@ class AddressManager { static uintptr_t GetBgfxContextFunc() { return s_pointers.bgfxContextFunc; } static uintptr_t GetContextCollectionFunc() { return s_pointers.contextCollectionFunc; } static uintptr_t GetGameThreadUpdateFunc() { return s_pointers.gameThreadUpdateFunc; } + static uintptr_t GetDecodeTextFunc() { return s_pointers.decodeTextFunc; } static void* GetContextCollectionPtr() { return s_pointers.pContextCollection; } // Module information getters for VTable validation @@ -80,6 +82,7 @@ class AddressManager { static void ScanBgfxContextFunc(); static void ScanContextCollectionFunc(); static void ScanGameThreadUpdateFunc(); + static void ScanDecodeTextFunc(); // Single static struct instance holding all pointers. static GamePointers s_pointers; diff --git a/src/Game/NameResolver.cpp b/src/Game/NameResolver.cpp new file mode 100644 index 0000000..66a4e7b --- /dev/null +++ b/src/Game/NameResolver.cpp @@ -0,0 +1,185 @@ +#include "NameResolver.h" +#include "../Utils/MemorySafety.h" +#include "../Utils/StringHelpers.h" +#include "../Game/AddressManager.h" +#include +#include +#include + +namespace kx { + namespace NameResolver { + + // Thread-safe cache for agent names + static std::unordered_map s_nameCache; + static std::mutex s_nameCacheMutex; + + // A POD struct to pass as context to the callback. + struct DecodedNameContext { + wchar_t buffer[1024]; + bool success; + }; + + // CORRECT VTable function signature: takes a 'this' pointer, returns a pointer to the coded name structure. + typedef void* (__fastcall* GetCodedName_t)(void* this_ptr); + + // The game's text decoding function. + typedef void(__fastcall* DecodeGameText_t)(void* codedTxt, void* callback, void* ctx); + + // The callback type that DecodeGameText expects + typedef void(__fastcall* DecodeCallback_t)(void* ctx, wchar_t* decodedText); + + + // SEH-wrapped helper to safely copy the decoded string. + static void SafeCopyDecodedString(wchar_t* dest, size_t destSize, const wchar_t* src) { + __try { + if (src && src[0] != L'\0') { + wcsncpy_s(dest, destSize, src, _TRUNCATE); + } + } + __except (EXCEPTION_EXECUTE_HANDLER) { + // Ensure buffer is null-terminated on failure. + if (destSize > 0) { + dest[0] = L'\0'; + } + } + } + + // The callback for DecodeGameText. This itself is fine. + static void __fastcall DecodeNameCallback(DecodedNameContext* ctx, wchar_t* decodedText) { + if (!ctx) { + return; + } + ctx->success = false; + ctx->buffer[0] = L'\0'; + + SafeCopyDecodedString(ctx->buffer, _countof(ctx->buffer), decodedText); + + if (ctx->buffer[0] != L'\0') { + ctx->success = true; + } + } + + // Helper function to get the pointer to the coded name structure from the VTable. + static void* GetCodedNamePointerSEH(void* agent_ptr) { + __try { + uintptr_t* vtable = *reinterpret_cast(agent_ptr); + if (!SafeAccess::IsMemorySafe(vtable)) return nullptr; + + GetCodedName_t pGetCodedName = reinterpret_cast(vtable[0]); + if (!SafeAccess::IsMemorySafe((void*)pGetCodedName)) return nullptr; + + // Call the function and get the pointer to the game's coded name structure + return pGetCodedName(agent_ptr); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + return nullptr; + } + } + + // Helper function to validate the coded name structure, based on hacklib/decode.c + static bool IsCodedNameValid(void* pCodedName) { + __try { + if (!pCodedName || !SafeAccess::IsMemorySafe(pCodedName, sizeof(uint16_t))) return false; + + // The decompiled code checks if the first ushort is non-zero and passes other checks + uint16_t firstWord = *reinterpret_cast(pCodedName); + if (firstWord == 0) return false; + if ((firstWord & 0x7fff) <= 0xff) return false; + + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } + } + + // Helper function to isolate the SEH block for the DecodeText call. + static bool DecodeNameSEH(DecodeGameText_t pDecodeGameText, void* pCodedName, DecodedNameContext* pod_ctx) { + __try { + // Validate the pointer before passing it to the game's function + if (!IsCodedNameValid(pCodedName)) { + return false; + } + + DecodeCallback_t callbackPtr = reinterpret_cast(&DecodeNameCallback); + pDecodeGameText(pCodedName, reinterpret_cast(callbackPtr), pod_ctx); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) { + return false; + } + } + + std::string GetNameFromAgent(void* agent_ptr) { + if (!SafeAccess::IsVTablePointerValid(agent_ptr)) { + return ""; + } + + if (!AddressManager::GetContextCollectionPtr()) { + return ""; + } + + // Step 1: Get the POINTER to the coded name from the game's VTable function. + void* pCodedName = GetCodedNamePointerSEH(agent_ptr); + if (!pCodedName) { + return ""; // The VTable call failed or returned null. + } + + // Step 2: Get the DecodeText function pointer. + auto pDecodeGameText = reinterpret_cast(AddressManager::GetDecodeTextFunc()); + if (!pDecodeGameText) { + return ""; + } + + // Step 3: Call DecodeNameSEH, passing the pointer we received from the game. + DecodedNameContext name_ctx = {}; + if (!DecodeNameSEH(pDecodeGameText, pCodedName, &name_ctx)) { + return ""; // Decode failed. + } + + // Step 4: Convert the result. + if (name_ctx.success) { + return StringHelpers::WCharToUTF8String(name_ctx.buffer); + } + + return ""; + } + + void CacheNamesForAgents(const std::vector& agentPointers) { + std::unordered_map newNames; + + for (void* agentPtr : agentPointers) { + if (!agentPtr) continue; + + std::string name = GetNameFromAgent(agentPtr); + if (!name.empty()) { + newNames[agentPtr] = std::move(name); + } + } + + if (!newNames.empty()) { + std::lock_guard lock(s_nameCacheMutex); + for (auto& pair : newNames) { + s_nameCache[pair.first] = std::move(pair.second); + } + } + } + + std::string GetCachedName(void* agent_ptr) { + if (!agent_ptr) return ""; + + std::lock_guard lock(s_nameCacheMutex); + auto it = s_nameCache.find(agent_ptr); + if (it != s_nameCache.end()) { + return it->second; + } + return ""; + } + + void ClearNameCache() { + std::lock_guard lock(s_nameCacheMutex); + s_nameCache.clear(); + } + + } // namespace NameResolver +} // namespace kx \ No newline at end of file diff --git a/src/Game/NameResolver.h b/src/Game/NameResolver.h new file mode 100644 index 0000000..761ab79 --- /dev/null +++ b/src/Game/NameResolver.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +namespace kx { + namespace NameResolver { + /** + * @brief Retrieves the name of a generic game agent using its VTable. + * + * IMPORTANT: This function requires the game's TLS context to be valid. + * It should ONLY be called from the game thread where DecodeText can safely execute. + * For render thread usage, use GetCachedName() instead. + * + * @param agent_ptr A pointer to the agent's instance in memory. + * @return The decoded name as a std::string, or an empty string if retrieval fails. + */ + std::string GetNameFromAgent(void* agent_ptr); + + /** + * @brief Resolves and caches names for a batch of agent pointers. + * + * This function should be called from the GAME THREAD (e.g., in DetourGameThread) + * where the TLS context is valid. It will resolve names for all provided agents + * and store them in the cache for safe access from other threads. + * + * @param agentPointers Vector of agent pointers to resolve names for + */ + void CacheNamesForAgents(const std::vector& agentPointers); + + /** + * @brief Retrieves a cached name for an agent pointer. + * + * This function is THREAD-SAFE and can be called from any thread (e.g., render thread). + * It returns the cached name if available, or an empty string if not found. + * + * @param agent_ptr The agent pointer to look up + * @return The cached name, or empty string if not found + */ + std::string GetCachedName(void* agent_ptr); + + /** + * @brief Clears old entries from the name cache. + * + * Should be called periodically to prevent the cache from growing indefinitely + * as agents are destroyed and new ones are created. + */ + void ClearNameCache(); + + } // namespace NameResolver +} // namespace kx diff --git a/src/Hooking/Hooks.cpp b/src/Hooking/Hooks.cpp index 6a261cc..bd3f9bb 100644 --- a/src/Hooking/Hooks.cpp +++ b/src/Hooking/Hooks.cpp @@ -1,10 +1,15 @@ #include "Hooks.h" #include // For __try/__except +#include #include "../Core/Config.h" // For GW2AL_BUILD define #include "../Utils/DebugLogger.h" -#include "AddressManager.h" +#include "../Utils/SafeIterators.h" +#include "../Utils/MemorySafety.h" +#include "../Game/AddressManager.h" +#include "../Game/NameResolver.h" +#include "../Game/ReClassStructs.h" #include "AppState.h" #include "D3DRenderHook.h" #include "HookManager.h" @@ -17,25 +22,77 @@ namespace kx { typedef void(__fastcall* GameThreadUpdateFunc)(void*, int); GameThreadUpdateFunc pOriginalGameThreadUpdate = nullptr; - // This is our detour function. It will be executed on the GAME'S LOGIC THREAD. - void __fastcall DetourGameThread(void* pInst, int frame_time) { + // Helper function to call the game's GetContextCollection within an SEH block. + // This isolates the unsafe call and prevents C2712 errors. + void* GetContextCollection_SEH() { // Define the type for GetContextCollection using GetContextCollectionFn = void* (*)(); // Get the function pointer from our AddressManager uintptr_t funcAddr = AddressManager::GetContextCollectionFunc(); - if (funcAddr) { - auto getContextCollection = reinterpret_cast(funcAddr); - - // CAPTURE the pointer and store it in our shared static variable. - // This is a call into game code, so we wrap it in a __try/__except block - // to prevent a crash in the game's function from crashing our tool. - __try { - AddressManager::SetContextCollectionPtr(getContextCollection()); + if (!funcAddr) { + return nullptr; + } + + auto getContextCollection = reinterpret_cast(funcAddr); + + __try { + return getContextCollection(); + } + __except (EXCEPTION_EXECUTE_HANDLER) { + return nullptr; + } + } + + // This is our detour function. It will be executed on the GAME'S LOGIC THREAD. + void __fastcall DetourGameThread(void* pInst, int frame_time) { + // Periodically clear old cache entries (every ~300 frames / ~5 seconds at 60fps) + static int frameCounter = 0; + if (++frameCounter >= 300) { + frameCounter = 0; + NameResolver::ClearNameCache(); + } + + // Safely get the context collection pointer using the SEH-wrapped helper. + void* pContextCollection = GetContextCollection_SEH(); + AddressManager::SetContextCollectionPtr(pContextCollection); + + // NOW we're on the game thread with valid TLS context! + // We can safely use C++ objects outside of the __try block. + if (pContextCollection && kx::SafeAccess::IsMemorySafe(pContextCollection)) { + std::vector agentPointers; + agentPointers.reserve(512); // Reserve space for typical agent count + + kx::ReClass::ContextCollection ctxCollection(pContextCollection); + + // Collect character agents + kx::ReClass::ChCliContext charContext = ctxCollection.GetChCliContext(); + if (charContext.data()) { + kx::SafeAccess::CharacterList charList(charContext); + for (const auto& character : charList) { + auto agent = character.GetAgent(); + if (agent.data()) { + agentPointers.push_back(agent.data()); + } + } + } + + // Collect gadget agents + kx::ReClass::GdCliContext gadgetContext = ctxCollection.GetGdCliContext(); + if (gadgetContext.data()) { + kx::SafeAccess::GadgetList gadgetList(gadgetContext); + for (const auto& gadget : gadgetList) { + // *** THIS IS THE FIX *** + auto agent = gadget.GetAgKeyFramed(); // Corrected from GetAgKeyframed + if (agent.data()) { + agentPointers.push_back(agent.data()); + } + } } - __except (EXCEPTION_EXECUTE_HANDLER) { - // If the game function crashes, we'll just get a nullptr this frame. - AddressManager::SetContextCollectionPtr(nullptr); + + // Resolve and cache all names (this is safe here on game thread) + if (!agentPointers.empty()) { + NameResolver::CacheNamesForAgents(agentPointers); } } @@ -114,4 +171,4 @@ namespace kx { LOG_INFO("[Hooks] Cleanup finished."); } -} // namespace kx +} // namespace kx \ No newline at end of file diff --git a/src/Rendering/Extractors/EntityExtractor.cpp b/src/Rendering/Extractors/EntityExtractor.cpp index 338347a..0898509 100644 --- a/src/Rendering/Extractors/EntityExtractor.cpp +++ b/src/Rendering/Extractors/EntityExtractor.cpp @@ -3,6 +3,7 @@ #include "Utils/ESPFormatting.h" #include "../../Game/GameEnums.h" #include "../../Utils/StringHelpers.h" +#include "../../Game/NameResolver.h" #include namespace kx { @@ -87,6 +88,9 @@ namespace kx { ); outNpc.isValid = true; outNpc.address = inCharacter.data(); + // Extract name using cached name resolution (thread-safe) + // Names are resolved on the game thread and cached for render thread access + outNpc.name = NameResolver::GetCachedName(agent.data()); // --- Health --- ReClass::ChCliHealth health = inCharacter.GetHealth(); @@ -126,6 +130,9 @@ namespace kx { ); outGadget.isValid = true; outGadget.address = inGadget.data(); + // Extract name using cached name resolution (thread-safe) + // Names are resolved on the game thread and cached for render thread access + outGadget.name = NameResolver::GetCachedName(agKeyFramed.data()); outGadget.type = inGadget.GetGadgetType(); outGadget.isGatherable = inGadget.IsGatherable(); diff --git a/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp b/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp index f03952d..831a67b 100644 --- a/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp +++ b/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp @@ -50,7 +50,12 @@ std::vector ESPEntityDetailsBuilder::BuildGadgetDetails(const Ren return details; } - details.reserve(8); // Future-proof: generous reserve for adding new fields + details.reserve(8); + + // Show the specific object name if it exists + if (!gadget->name.empty()) { + details.push_back({ gadget->name, ESPColors::DEFAULT_TEXT }); + } const char* gadgetName = ESPFormatting::GetGadgetTypeName(gadget->type); details.push_back({ "Type: " + (gadgetName ? std::string(gadgetName) : "ID: " + std::to_string(static_cast(gadget->type))), ESPColors::DEFAULT_TEXT }); From c80ddd1be3ce599f31204afc4a909f222e382bd0 Mon Sep 17 00:00:00 2001 From: Krixx Date: Mon, 6 Oct 2025 04:02:26 +0200 Subject: [PATCH 2/3] Fixes? --- src/Game/NameResolver.cpp | 226 ++++++++++++++++++++++---------------- 1 file changed, 132 insertions(+), 94 deletions(-) diff --git a/src/Game/NameResolver.cpp b/src/Game/NameResolver.cpp index 66a4e7b..c5d2e5a 100644 --- a/src/Game/NameResolver.cpp +++ b/src/Game/NameResolver.cpp @@ -5,61 +5,61 @@ #include #include #include +#include namespace kx { namespace NameResolver { - // Thread-safe cache for agent names - static std::unordered_map s_nameCache; - static std::mutex s_nameCacheMutex; + // --- Asynchronous Request Management --- - // A POD struct to pass as context to the callback. - struct DecodedNameContext { - wchar_t buffer[1024]; - bool success; - }; + // A unique ID for each name request + static std::atomic s_nextRequestId = 1; - // CORRECT VTable function signature: takes a 'this' pointer, returns a pointer to the coded name structure. - typedef void* (__fastcall* GetCodedName_t)(void* this_ptr); + // Stores the agent pointer and the resulting name for a pending request + struct PendingRequest { + void* agentPtr; + std::string result; + }; - // The game's text decoding function. - typedef void(__fastcall* DecodeGameText_t)(void* codedTxt, void* callback, void* ctx); + // Thread-safe map of pending requests + static std::unordered_map s_pendingRequests; + static std::mutex s_requestsMutex; - // The callback type that DecodeGameText expects - typedef void(__fastcall* DecodeCallback_t)(void* ctx, wchar_t* decodedText); + // --- Caching --- + static std::unordered_map s_nameCache; + static std::mutex s_nameCacheMutex; + // --- Game Function Signatures --- + typedef void* (__fastcall* GetCodedName_t)(void* this_ptr); + typedef void(__fastcall* DecodeGameText_t)(void* codedTxt, void* callback, void* ctx); - // SEH-wrapped helper to safely copy the decoded string. - static void SafeCopyDecodedString(wchar_t* dest, size_t destSize, const wchar_t* src) { - __try { - if (src && src[0] != L'\0') { - wcsncpy_s(dest, destSize, src, _TRUNCATE); - } - } - __except (EXCEPTION_EXECUTE_HANDLER) { - // Ensure buffer is null-terminated on failure. - if (destSize > 0) { - dest[0] = L'\0'; - } + // The callback from the game. 'ctx' will be our request ID. + void __fastcall DecodeNameCallback(void* ctx, wchar_t* decodedText) { + if (!ctx || !decodedText || decodedText[0] == L'\0') { + return; } - } - // The callback for DecodeGameText. This itself is fine. - static void __fastcall DecodeNameCallback(DecodedNameContext* ctx, wchar_t* decodedText) { - if (!ctx) { + // --- FIX: Immediately copy the temporary game buffer into a stable wstring --- + // The 'decodedText' pointer is only guaranteed to be valid during this function call. + // By copying it instantly, we protect against the original buffer being overwritten. + std::wstring safeDecodedText(decodedText); + + // Now, perform the conversion using our safe, local copy. + std::string utf8Name = StringHelpers::WCharToUTF8String(safeDecodedText.c_str()); + if (utf8Name.empty()) { return; } - ctx->success = false; - ctx->buffer[0] = L'\0'; - - SafeCopyDecodedString(ctx->buffer, _countof(ctx->buffer), decodedText); - if (ctx->buffer[0] != L'\0') { - ctx->success = true; + // Lock the mutex to safely update the pending request map + std::lock_guard lock(s_requestsMutex); + uint64_t requestId = reinterpret_cast(ctx); + auto it = s_pendingRequests.find(requestId); + if (it != s_pendingRequests.end()) { + it->second.result = std::move(utf8Name); } } - // Helper function to get the pointer to the coded name structure from the VTable. + // Helper to get the coded name pointer static void* GetCodedNamePointerSEH(void* agent_ptr) { __try { uintptr_t* vtable = *reinterpret_cast(agent_ptr); @@ -68,7 +68,6 @@ namespace kx { GetCodedName_t pGetCodedName = reinterpret_cast(vtable[0]); if (!SafeAccess::IsMemorySafe((void*)pGetCodedName)) return nullptr; - // Call the function and get the pointer to the game's coded name structure return pGetCodedName(agent_ptr); } __except (EXCEPTION_EXECUTE_HANDLER) { @@ -76,91 +75,119 @@ namespace kx { } } - // Helper function to validate the coded name structure, based on hacklib/decode.c - static bool IsCodedNameValid(void* pCodedName) { - __try { - if (!pCodedName || !SafeAccess::IsMemorySafe(pCodedName, sizeof(uint16_t))) return false; - - // The decompiled code checks if the first ushort is non-zero and passes other checks - uint16_t firstWord = *reinterpret_cast(pCodedName); - if (firstWord == 0) return false; - if ((firstWord & 0x7fff) <= 0xff) return false; - - return true; - } - __except (EXCEPTION_EXECUTE_HANDLER) { - return false; - } - } - - // Helper function to isolate the SEH block for the DecodeText call. - static bool DecodeNameSEH(DecodeGameText_t pDecodeGameText, void* pCodedName, DecodedNameContext* pod_ctx) { + // Helper function to isolate the __try block + static bool CallDecodeTextSEH(DecodeGameText_t pDecodeGameText, void* pCodedName, void* callback, void* ctx) { __try { - // Validate the pointer before passing it to the game's function - if (!IsCodedNameValid(pCodedName)) { - return false; - } - - DecodeCallback_t callbackPtr = reinterpret_cast(&DecodeNameCallback); - pDecodeGameText(pCodedName, reinterpret_cast(callbackPtr), pod_ctx); - return true; + pDecodeGameText(pCodedName, callback, ctx); + return true; // Indicate success } __except (EXCEPTION_EXECUTE_HANDLER) { - return false; + return false; // Indicate failure } } - std::string GetNameFromAgent(void* agent_ptr) { - if (!SafeAccess::IsVTablePointerValid(agent_ptr)) { - return ""; + // This function NO LONGER returns a name. It just starts the decoding process. + void RequestNameForAgent(void* agent_ptr) { + if (!SafeAccess::IsVTablePointerValid(agent_ptr) || !AddressManager::GetContextCollectionPtr()) { + return; } - if (!AddressManager::GetContextCollectionPtr()) { - return ""; + auto pDecodeGameText = reinterpret_cast(AddressManager::GetDecodeTextFunc()); + if (!pDecodeGameText) { + return; } - // Step 1: Get the POINTER to the coded name from the game's VTable function. void* pCodedName = GetCodedNamePointerSEH(agent_ptr); if (!pCodedName) { - return ""; // The VTable call failed or returned null. + return; } - // Step 2: Get the DecodeText function pointer. - auto pDecodeGameText = reinterpret_cast(AddressManager::GetDecodeTextFunc()); - if (!pDecodeGameText) { - return ""; + // Generate a unique ID for this request + uint64_t requestId = s_nextRequestId++; + + { + // Store the agent pointer so we know who this request is for + std::lock_guard lock(s_requestsMutex); + s_pendingRequests[requestId] = { agent_ptr, "" }; } - // Step 3: Call DecodeNameSEH, passing the pointer we received from the game. - DecodedNameContext name_ctx = {}; - if (!DecodeNameSEH(pDecodeGameText, pCodedName, &name_ctx)) { - return ""; // Decode failed. + // Call the game function via our safe helper + bool success = CallDecodeTextSEH( + pDecodeGameText, + pCodedName, + reinterpret_cast(&DecodeNameCallback), + reinterpret_cast(requestId) + ); + + // If the call failed, remove the pending request to prevent it from sitting there forever + if (!success) { + std::lock_guard lock(s_requestsMutex); + s_pendingRequests.erase(requestId); } + } - // Step 4: Convert the result. - if (name_ctx.success) { - return StringHelpers::WCharToUTF8String(name_ctx.buffer); + // This function processes completed requests and moves them to the main cache. + void ProcessCompletedNameRequests() { + std::vector> completed; + + // Safely find and remove completed requests + { + std::lock_guard lock(s_requestsMutex); + for (auto it = s_pendingRequests.begin(); it != s_pendingRequests.end(); ) { + if (!it->second.result.empty()) { + completed.push_back({ it->second.agentPtr, std::move(it->second.result) }); + it = s_pendingRequests.erase(it); + } + else { + ++it; + } + } } - return ""; + // Add completed names to the main cache + if (!completed.empty()) { + std::lock_guard lock(s_nameCacheMutex); + for (const auto& pair : completed) { + s_nameCache[pair.first] = std::move(pair.second); + } + } } void CacheNamesForAgents(const std::vector& agentPointers) { - std::unordered_map newNames; + // 1. Process any requests that were completed since the last frame + ProcessCompletedNameRequests(); + // 2. Request names for any new agents for (void* agentPtr : agentPointers) { if (!agentPtr) continue; - std::string name = GetNameFromAgent(agentPtr); - if (!name.empty()) { - newNames[agentPtr] = std::move(name); + // --- FIX: Check both the main cache AND pending requests --- + bool alreadyProcessed = false; + { + // Check if it's already in the final cache + std::lock_guard lock(s_nameCacheMutex); + if (s_nameCache.count(agentPtr)) { + alreadyProcessed = true; + } } - } - if (!newNames.empty()) { - std::lock_guard lock(s_nameCacheMutex); - for (auto& pair : newNames) { - s_nameCache[pair.first] = std::move(pair.second); + if (alreadyProcessed) { + continue; // Skip if we have the name + } + + { + // Check if a request is already pending for this agent + std::lock_guard lock(s_requestsMutex); + for (const auto& pair : s_pendingRequests) { + if (pair.second.agentPtr == agentPtr) { + alreadyProcessed = true; + break; + } + } + } + + if (!alreadyProcessed) { + RequestNameForAgent(agentPtr); // Only request if not cached and not pending } } } @@ -179,6 +206,17 @@ namespace kx { void ClearNameCache() { std::lock_guard lock(s_nameCacheMutex); s_nameCache.clear(); + + // Also clear any pending requests that might now be stale + std::lock_guard req_lock(s_requestsMutex); + s_pendingRequests.clear(); + } + + // This function is no longer used by the main loop but is kept for reference. + std::string GetNameFromAgent(void* agent_ptr) { + // The process is now asynchronous, so we can't get the name immediately. + // We can only request it and check the cache later. + return GetCachedName(agent_ptr); } } // namespace NameResolver From 144cb645fe2c882ce9e407a81ee59e123bdff6f3 Mon Sep 17 00:00:00 2001 From: anonre Date: Sun, 9 Nov 2025 01:28:38 +0100 Subject: [PATCH 3/3] Fix GetCodedName vtable indices for GdCliGadget & ChCliCharacter --- src/Game/NameResolver.cpp | 14 +++++------ src/Game/NameResolver.h | 3 ++- src/Hooking/Hooks.cpp | 13 ++++------ src/Rendering/Core/ESPStageRenderer.cpp | 24 +++++++++---------- src/Rendering/Extractors/EntityExtractor.cpp | 4 ++-- src/Rendering/Renderers/ESPContextFactory.cpp | 10 ++++---- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/Game/NameResolver.cpp b/src/Game/NameResolver.cpp index c5d2e5a..8eb9726 100644 --- a/src/Game/NameResolver.cpp +++ b/src/Game/NameResolver.cpp @@ -60,12 +60,12 @@ namespace kx { } // Helper to get the coded name pointer - static void* GetCodedNamePointerSEH(void* agent_ptr) { + static void* GetCodedNamePointerSEH(void* agent_ptr, uint8_t type) { __try { uintptr_t* vtable = *reinterpret_cast(agent_ptr); if (!SafeAccess::IsMemorySafe(vtable)) return nullptr; - GetCodedName_t pGetCodedName = reinterpret_cast(vtable[0]); + GetCodedName_t pGetCodedName = reinterpret_cast(type == 0 ? vtable[57] : vtable[8]); if (!SafeAccess::IsMemorySafe((void*)pGetCodedName)) return nullptr; return pGetCodedName(agent_ptr); @@ -87,7 +87,7 @@ namespace kx { } // This function NO LONGER returns a name. It just starts the decoding process. - void RequestNameForAgent(void* agent_ptr) { + void RequestNameForAgent(void* agent_ptr, uint8_t type) { if (!SafeAccess::IsVTablePointerValid(agent_ptr) || !AddressManager::GetContextCollectionPtr()) { return; } @@ -97,7 +97,7 @@ namespace kx { return; } - void* pCodedName = GetCodedNamePointerSEH(agent_ptr); + void* pCodedName = GetCodedNamePointerSEH(agent_ptr, type); if (!pCodedName) { return; } @@ -153,12 +153,12 @@ namespace kx { } } - void CacheNamesForAgents(const std::vector& agentPointers) { + void CacheNamesForAgents(const std::unordered_map& agentPointers) { // 1. Process any requests that were completed since the last frame ProcessCompletedNameRequests(); // 2. Request names for any new agents - for (void* agentPtr : agentPointers) { + for (auto [agentPtr, type] : agentPointers) { if (!agentPtr) continue; // --- FIX: Check both the main cache AND pending requests --- @@ -187,7 +187,7 @@ namespace kx { } if (!alreadyProcessed) { - RequestNameForAgent(agentPtr); // Only request if not cached and not pending + RequestNameForAgent(agentPtr, type); // Only request if not cached and not pending } } } diff --git a/src/Game/NameResolver.h b/src/Game/NameResolver.h index 761ab79..3ea9ec4 100644 --- a/src/Game/NameResolver.h +++ b/src/Game/NameResolver.h @@ -2,6 +2,7 @@ #include #include +#include namespace kx { namespace NameResolver { @@ -26,7 +27,7 @@ namespace kx { * * @param agentPointers Vector of agent pointers to resolve names for */ - void CacheNamesForAgents(const std::vector& agentPointers); + void CacheNamesForAgents(const std::unordered_map& agentPointers); /** * @brief Retrieves a cached name for an agent pointer. diff --git a/src/Hooking/Hooks.cpp b/src/Hooking/Hooks.cpp index bd3f9bb..2b18ab1 100644 --- a/src/Hooking/Hooks.cpp +++ b/src/Hooking/Hooks.cpp @@ -60,7 +60,7 @@ namespace kx { // NOW we're on the game thread with valid TLS context! // We can safely use C++ objects outside of the __try block. if (pContextCollection && kx::SafeAccess::IsMemorySafe(pContextCollection)) { - std::vector agentPointers; + std::unordered_map agentPointers; agentPointers.reserve(512); // Reserve space for typical agent count kx::ReClass::ContextCollection ctxCollection(pContextCollection); @@ -70,9 +70,8 @@ namespace kx { if (charContext.data()) { kx::SafeAccess::CharacterList charList(charContext); for (const auto& character : charList) { - auto agent = character.GetAgent(); - if (agent.data()) { - agentPointers.push_back(agent.data()); + if (character.data()) { + agentPointers.emplace((void*)character.data(), 0); } } } @@ -82,10 +81,8 @@ namespace kx { if (gadgetContext.data()) { kx::SafeAccess::GadgetList gadgetList(gadgetContext); for (const auto& gadget : gadgetList) { - // *** THIS IS THE FIX *** - auto agent = gadget.GetAgKeyFramed(); // Corrected from GetAgKeyframed - if (agent.data()) { - agentPointers.push_back(agent.data()); + if (gadget.data()) { + agentPointers.emplace((void*)gadget.data(), 1); } } } diff --git a/src/Rendering/Core/ESPStageRenderer.cpp b/src/Rendering/Core/ESPStageRenderer.cpp index bd2521c..656fc81 100644 --- a/src/Rendering/Core/ESPStageRenderer.cpp +++ b/src/Rendering/Core/ESPStageRenderer.cpp @@ -81,22 +81,20 @@ void ESPStageRenderer::RenderEntityComponents(ImDrawList* drawList, const Entity } // Render player name for natural identification (players only) - if (context.entityType == ESPEntityType::Player && context.renderPlayerName) { - // For hostile players with an empty name, display their profession - std::string displayName = context.playerName; - if (displayName.empty() && context.attitude == Game::Attitude::Hostile) { - if (context.player) { - const char* prof = ESPFormatting::GetProfessionName(context.player->profession); - if (prof) { - displayName = prof; - } + // For hostile players with an empty name, display their profession + std::string displayName = context.playerName; + if (displayName.empty() && context.attitude == Game::Attitude::Hostile) { + if (context.player) { + const char* prof = ESPFormatting::GetProfessionName(context.player->profession); + if (prof) { + displayName = prof; } } + } - if (!displayName.empty()) { - // Use entity color directly (already attitude-based from ESPContextFactory) - ESPTextRenderer::RenderPlayerName(drawList, screenPos, displayName, fadedEntityColor, finalFontSize); - } + if (!displayName.empty()) { + // Use entity color directly (already attitude-based from ESPContextFactory) + ESPTextRenderer::RenderPlayerName(drawList, screenPos, displayName, fadedEntityColor, finalFontSize); } // Render details text (for all entities when enabled) diff --git a/src/Rendering/Extractors/EntityExtractor.cpp b/src/Rendering/Extractors/EntityExtractor.cpp index 0898509..c227928 100644 --- a/src/Rendering/Extractors/EntityExtractor.cpp +++ b/src/Rendering/Extractors/EntityExtractor.cpp @@ -90,7 +90,7 @@ namespace kx { outNpc.address = inCharacter.data(); // Extract name using cached name resolution (thread-safe) // Names are resolved on the game thread and cached for render thread access - outNpc.name = NameResolver::GetCachedName(agent.data()); + outNpc.name = NameResolver::GetCachedName(const_cast(inCharacter.data())); // --- Health --- ReClass::ChCliHealth health = inCharacter.GetHealth(); @@ -132,7 +132,7 @@ namespace kx { outGadget.address = inGadget.data(); // Extract name using cached name resolution (thread-safe) // Names are resolved on the game thread and cached for render thread access - outGadget.name = NameResolver::GetCachedName(agKeyFramed.data()); + outGadget.name = NameResolver::GetCachedName(const_cast(inGadget.data())); outGadget.type = inGadget.GetGadgetType(); outGadget.isGatherable = inGadget.IsGatherable(); diff --git a/src/Rendering/Renderers/ESPContextFactory.cpp b/src/Rendering/Renderers/ESPContextFactory.cpp index b582f43..853ea56 100644 --- a/src/Rendering/Renderers/ESPContextFactory.cpp +++ b/src/Rendering/Renderers/ESPContextFactory.cpp @@ -83,7 +83,6 @@ EntityRenderContext ESPContextFactory::CreateContextForNpc(const RenderableNpc* break; } - static const std::string emptyPlayerName = ""; return EntityRenderContext{ npc->position, npc->visualDistance, @@ -96,12 +95,12 @@ EntityRenderContext ESPContextFactory::CreateContextForNpc(const RenderableNpc* settings.npcESP.renderDot, settings.npcESP.renderDetails, settings.npcESP.renderHealthBar, - false, + true, ESPEntityType::NPC, npc->attitude, screenWidth, screenHeight, - emptyPlayerName, + npc->name, nullptr }; } @@ -111,7 +110,6 @@ EntityRenderContext ESPContextFactory::CreateContextForGadget(const RenderableGa const std::vector& details, float screenWidth, float screenHeight) { - static const std::string emptyPlayerName = ""; return EntityRenderContext{ gadget->position, gadget->visualDistance, @@ -123,13 +121,13 @@ EntityRenderContext ESPContextFactory::CreateContextForGadget(const RenderableGa settings.objectESP.renderDistance, settings.objectESP.renderDot, settings.objectESP.renderDetails, - false, + true, false, ESPEntityType::Gadget, Game::Attitude::Neutral, screenWidth, screenHeight, - emptyPlayerName, + gadget->name, nullptr }; }