diff --git a/KX-Vision.vcxproj b/KX-Vision.vcxproj
index 7f4190b0..50d755be 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 e3f24ffa..2625b323 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 e1808ca2..e51b1e67 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 c09161b7..4f62aeb0 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 00000000..8eb97268
--- /dev/null
+++ b/src/Game/NameResolver.cpp
@@ -0,0 +1,223 @@
+#include "NameResolver.h"
+#include "../Utils/MemorySafety.h"
+#include "../Utils/StringHelpers.h"
+#include "../Game/AddressManager.h"
+#include
+#include
+#include
+#include
+
+namespace kx {
+ namespace NameResolver {
+
+ // --- Asynchronous Request Management ---
+
+ // A unique ID for each name request
+ static std::atomic s_nextRequestId = 1;
+
+ // Stores the agent pointer and the resulting name for a pending request
+ struct PendingRequest {
+ void* agentPtr;
+ std::string result;
+ };
+
+ // Thread-safe map of pending requests
+ static std::unordered_map s_pendingRequests;
+ static std::mutex s_requestsMutex;
+
+ // --- 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);
+
+ // 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;
+ }
+
+ // --- 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;
+ }
+
+ // 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 to get the coded name pointer
+ 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(type == 0 ? vtable[57] : vtable[8]);
+ if (!SafeAccess::IsMemorySafe((void*)pGetCodedName)) return nullptr;
+
+ return pGetCodedName(agent_ptr);
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER) {
+ return nullptr;
+ }
+ }
+
+ // Helper function to isolate the __try block
+ static bool CallDecodeTextSEH(DecodeGameText_t pDecodeGameText, void* pCodedName, void* callback, void* ctx) {
+ __try {
+ pDecodeGameText(pCodedName, callback, ctx);
+ return true; // Indicate success
+ }
+ __except (EXCEPTION_EXECUTE_HANDLER) {
+ return false; // Indicate failure
+ }
+ }
+
+ // This function NO LONGER returns a name. It just starts the decoding process.
+ void RequestNameForAgent(void* agent_ptr, uint8_t type) {
+ if (!SafeAccess::IsVTablePointerValid(agent_ptr) || !AddressManager::GetContextCollectionPtr()) {
+ return;
+ }
+
+ auto pDecodeGameText = reinterpret_cast(AddressManager::GetDecodeTextFunc());
+ if (!pDecodeGameText) {
+ return;
+ }
+
+ void* pCodedName = GetCodedNamePointerSEH(agent_ptr, type);
+ if (!pCodedName) {
+ 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, "" };
+ }
+
+ // 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);
+ }
+ }
+
+ // 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;
+ }
+ }
+ }
+
+ // 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::unordered_map& agentPointers) {
+ // 1. Process any requests that were completed since the last frame
+ ProcessCompletedNameRequests();
+
+ // 2. Request names for any new agents
+ for (auto [agentPtr, type] : agentPointers) {
+ if (!agentPtr) continue;
+
+ // --- 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 (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, type); // Only request if not cached and not pending
+ }
+ }
+ }
+
+ 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();
+
+ // 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
+} // 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 00000000..3ea9ec43
--- /dev/null
+++ b/src/Game/NameResolver.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include
+#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::unordered_map& 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 6a261cc5..2b18ab17 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,74 @@ 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::unordered_map 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) {
+ if (character.data()) {
+ agentPointers.emplace((void*)character.data(), 0);
+ }
+ }
+ }
+
+ // Collect gadget agents
+ kx::ReClass::GdCliContext gadgetContext = ctxCollection.GetGdCliContext();
+ if (gadgetContext.data()) {
+ kx::SafeAccess::GadgetList gadgetList(gadgetContext);
+ for (const auto& gadget : gadgetList) {
+ if (gadget.data()) {
+ agentPointers.emplace((void*)gadget.data(), 1);
+ }
+ }
}
- __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 +168,4 @@ namespace kx {
LOG_INFO("[Hooks] Cleanup finished.");
}
-} // namespace kx
+} // namespace kx
\ No newline at end of file
diff --git a/src/Rendering/Core/ESPStageRenderer.cpp b/src/Rendering/Core/ESPStageRenderer.cpp
index bd2521c5..656fc81e 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 338347a5..c2279287 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(const_cast(inCharacter.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(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 b582f430..853ea56f 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
};
}
diff --git a/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp b/src/Rendering/Utils/ESPEntityDetailsBuilder.cpp
index f03952d0..831a67b4 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 });