Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions KX-Vision.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
<ClCompile Include="src\Game\AddressManager.cpp" />
<ClCompile Include="src\Game\Camera.cpp" />
<ClCompile Include="src\Game\MumbleLinkManager.cpp" />
<ClCompile Include="src\Game\NameResolver.cpp" />
<ClCompile Include="src\Rendering\Core\ESPDataExtractor.cpp" />
<ClCompile Include="src\Rendering\Core\ESPFilter.cpp" />
<ClCompile Include="src\Rendering\Core\ESPRenderer.cpp" />
Expand Down Expand Up @@ -162,6 +163,7 @@
<ClInclude Include="src\Game\Generated\StatData.h" />
<ClInclude Include="src\Game\MumbleLinkManager.h" />
<ClInclude Include="src\Core\Config.h" />
<ClInclude Include="src\Game\NameResolver.h" />
<ClInclude Include="src\Game\ReClassStructs.h" />
<ClInclude Include="src\Game\ReClass\AgentStructs.h" />
<ClInclude Include="src\Game\ReClass\CharacterStructs.h" />
Expand Down
4 changes: 4 additions & 0 deletions src/Core/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 24 additions & 0 deletions src/Game/AddressManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,37 @@ void AddressManager::Scan() {
ScanModuleInformation();
ScanContextCollectionFunc();
ScanGameThreadUpdateFunc();
ScanDecodeTextFunc();

// currently unused
//ScanAgentArray();
//ScanWorldViewContextPtr();
//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<uintptr_t> 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();
}
Expand Down
3 changes: 3 additions & 0 deletions src/Game/AddressManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
Expand Down
223 changes: 223 additions & 0 deletions src/Game/NameResolver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
#include "NameResolver.h"
#include "../Utils/MemorySafety.h"
#include "../Utils/StringHelpers.h"
#include "../Game/AddressManager.h"
#include <windows.h>
#include <unordered_map>
#include <mutex>
#include <atomic>

namespace kx {
namespace NameResolver {

// --- Asynchronous Request Management ---

// A unique ID for each name request
static std::atomic<uint64_t> 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<uint64_t, PendingRequest> s_pendingRequests;
static std::mutex s_requestsMutex;

// --- Caching ---
static std::unordered_map<void*, std::string> 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<std::mutex> lock(s_requestsMutex);
uint64_t requestId = reinterpret_cast<uint64_t>(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<uintptr_t**>(agent_ptr);
if (!SafeAccess::IsMemorySafe(vtable)) return nullptr;

GetCodedName_t pGetCodedName = reinterpret_cast<GetCodedName_t>(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<DecodeGameText_t>(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<std::mutex> lock(s_requestsMutex);
s_pendingRequests[requestId] = { agent_ptr, "" };
}

// Call the game function via our safe helper
bool success = CallDecodeTextSEH(
pDecodeGameText,
pCodedName,
reinterpret_cast<void*>(&DecodeNameCallback),
reinterpret_cast<void*>(requestId)
);

// If the call failed, remove the pending request to prevent it from sitting there forever
if (!success) {
std::lock_guard<std::mutex> lock(s_requestsMutex);
s_pendingRequests.erase(requestId);
}
}

// This function processes completed requests and moves them to the main cache.
void ProcessCompletedNameRequests() {
std::vector<std::pair<void*, std::string>> completed;

// Safely find and remove completed requests
{
std::lock_guard<std::mutex> 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<std::mutex> lock(s_nameCacheMutex);
for (const auto& pair : completed) {
s_nameCache[pair.first] = std::move(pair.second);
}
}
}

void CacheNamesForAgents(const std::unordered_map<void*, uint8_t>& 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(s_nameCacheMutex);
auto it = s_nameCache.find(agent_ptr);
if (it != s_nameCache.end()) {
return it->second;
}
return "";
}

void ClearNameCache() {
std::lock_guard<std::mutex> lock(s_nameCacheMutex);
s_nameCache.clear();

// Also clear any pending requests that might now be stale
std::lock_guard<std::mutex> 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
52 changes: 52 additions & 0 deletions src/Game/NameResolver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once

#include <string>
#include <vector>
#include <unordered_map>

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<void*, uint8_t>& 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
Loading