feat: completely rewrite unlocker logic
Build / build (push) Failing after 2m33s

This commit is contained in:
2026-03-21 23:31:59 -03:00
parent 90a12905e4
commit 698ad23963
3 changed files with 520 additions and 359 deletions
+22 -359
View File
@@ -1,30 +1,22 @@
#include "proxy.h"
#include "spoofing.h"
#include <nerutils/log.h>
#include <windows.h>
#include <wininet.h>
#include <fstream>
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <format>
#include <mutex>
#include <ctime>
#include <regex>
#include <unordered_set>
#include <simdjson.h>
//#include <fstream>
//#include <iostream>
//#include <string>
//#include <string_view>
//#include <vector>
//#include <format>
//#include <mutex>
//#include <ctime>
//#include <regex>
//#include <unordered_set>
//#include <simdjson.h>
std::string getExeDir()
{
char buffer[MAX_PATH];
GetModuleFileNameA(NULL, buffer, MAX_PATH);
std::string path(buffer);
size_t pos = path.find_last_of("\\/");
if (pos != std::string::npos) return path.substr(0, pos + 1);
return "";
}
bool setProxy(bool enable, const std::string& proxyAddr)
{
@@ -62,8 +54,8 @@ bool setProxy(bool enable, const std::string& proxyAddr)
return true;
}
Proxy* g_proxy = nullptr;
bool running = true;
Proxy* g_Proxy = nullptr;
void cleanup()
{
@@ -73,10 +65,10 @@ void cleanup()
if (cleaned) return;
cleaned = true;
if (g_proxy)
if (g_Proxy)
{
Log::info("Shutting down proxy");
g_proxy->Shutdown();
g_Proxy->Shutdown();
}
Log::info("Restoring system proxy settings");
@@ -91,178 +83,11 @@ BOOL WINAPI consoleHandler(DWORD dwType)
running = false;
cleanup();
exit(0);
return TRUE;
//return TRUE;
}
return FALSE;
}
std::mutex g_dataMutex;
std::vector<std::string> g_allObjectIds;
std::vector<std::string> g_allCharacterIds;
std::vector<std::string> g_stackableItems;
std::vector<std::string> g_uniqueItems;
std::vector<std::string> g_perks;
void loadDictDump(const std::string& path, std::vector<std::string>& outStackable, std::vector<std::string>& outUnique)
{
simdjson::dom::parser parser;
simdjson::dom::element doc;
auto error = parser.load(path).get(doc);
if (error)
{
Log::warning("Failed to open or parse dict dump {}: {}", path, simdjson::error_message(error));
return;
}
simdjson::dom::array itemsArr;
if (doc.at_key("Items").get(itemsArr) == simdjson::SUCCESS)
{
for (auto item : itemsArr)
{
std::string_view id;
if (item.get(id) == simdjson::SUCCESS) outStackable.push_back(std::string(id));
}
}
simdjson::dom::array powersArr;
if (doc.at_key("Powers").get(powersArr) == simdjson::SUCCESS)
{
for (auto item : powersArr)
{
std::string_view id;
if (item.get(id) == simdjson::SUCCESS) outUnique.push_back(std::string(id));
}
}
}
void loadArrayDump(const std::string& path, std::vector<std::string>& outList)
{
simdjson::dom::parser parser;
simdjson::dom::element doc;
auto error = parser.load(path).get(doc);
if (error)
{
Log::warning("Failed to open or parse array dump {}: {}", path, simdjson::error_message(error));
return;
}
simdjson::dom::array arr;
if (doc.get(arr) == simdjson::SUCCESS)
{
for (auto item : arr)
{
std::string_view id;
if (item.get(id) == simdjson::SUCCESS) outList.push_back(std::string(id));
}
}
}
void loadAllCustomItems()
{
std::lock_guard<std::mutex> lock(g_dataMutex);
g_stackableItems.clear();
g_uniqueItems.clear();
g_perks.clear();
loadDictDump(getExeDir() + "addons.json", g_stackableItems, g_stackableItems);
loadDictDump(getExeDir() + "items.json", g_stackableItems, g_uniqueItems);
loadArrayDump(getExeDir() + "offerings.json", g_stackableItems);
loadArrayDump(getExeDir() + "perks.json", g_perks);
Log::info("Loaded {} stackable, {} unique (powers), and {} perk items from dumps", g_stackableItems.size(),
g_uniqueItems.size(), g_perks.size());
}
void parseCatalog(const std::string& data)
{
try
{
simdjson::ondemand::parser parser;
simdjson::padded_string json(data);
auto doc = parser.iterate(json);
std::vector<std::string> newObjectIds;
std::vector<std::string> newCharacterIds;
auto items_data = doc["data"];
for (auto item : items_data["character"]["items"])
{
std::string_view id;
if (item.get_string().get(id) == simdjson::SUCCESS)
{
newCharacterIds.push_back(std::string(id));
newObjectIds.push_back(std::string(id));
}
}
for (auto item : items_data["shrine"]["items"])
{
std::string_view id;
if (item.get_string().get(id) == simdjson::SUCCESS)
{
newObjectIds.push_back(std::string(id));
}
}
for (auto item : items_data["item"]["items"])
{
std::string_view id;
if (item.get_string().get(id) == simdjson::SUCCESS)
{
if (id.find("cell") == std::string_view::npos && id.find("Pack") == std::string_view::npos)
{
newObjectIds.push_back(std::string(id));
}
}
}
for (auto item : items_data["outfit"]["items"])
{
std::string_view id;
if (item.get_string().get(id) == simdjson::SUCCESS)
{
newObjectIds.push_back(std::string(id));
}
}
std::lock_guard<std::mutex> lock(g_dataMutex);
g_allObjectIds = std::move(newObjectIds);
g_allCharacterIds = std::move(newCharacterIds);
Log::info("Catalog parsed: {} items, {} characters", g_allObjectIds.size(), g_allCharacterIds.size());
}
catch (const std::exception& e)
{
Log::error("Failed to parse catalog: {}", e.what());
}
}
void updateCatalog(const std::string& data)
{
std::string path = getExeDir() + "catalog_dump.json";
std::ofstream file(path);
file << data;
file.close();
Log::info("Raw catalog saved to {}", path);
parseCatalog(data);
}
void loadCatalogOnStartup()
{
std::string path = getExeDir() + "catalog_dump.json";
std::ifstream file(path);
if (file.is_open())
{
Log::info("Found catalog_dump.json - re-parsing raw dump with current filters...");
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
parseCatalog(content);
}
else
Log::warning("No catalog_dump.json found. Start the game and visit the store once to initialise the unlocker.");
}
int main()
{
Log::createConsole();
@@ -271,15 +96,12 @@ int main()
Log::info("Init");
loadCatalogOnStartup();
loadAllCustomItems();
/*
proxy setup
*/
Log::info("Starting proxy");
g_proxy = new Proxy();
if (!g_proxy->Init())
g_Proxy = new Proxy();
if (!g_Proxy->Init())
{
Log::error("Proxy failed to start");
return 1;
@@ -287,171 +109,12 @@ int main()
setProxy(true, std::format("127.0.0.1:{}", PROXY_PORT));
/*
listeners
Spoofer setup
*/
g_proxy->OnServerResponse.addListener([](const std::string& url, std::string& data) {
#ifdef _DEBUG
if (url.find("bhvrdbd.com") != std::string::npos) Log::verbose("BHVR api res: {}", url);
#endif
Log::info("Spoofer init");
Spoofer* spoofer = new Spoofer();
if (url.find("api/v1/extensions/store/getCatalogItems") != std::string::npos)
updateCatalog(data);
else if (url.find("api/v1/dbd-inventories/all") != std::string::npos)
{
std::lock_guard<std::mutex> lock(g_dataMutex);
if (!g_allObjectIds.empty())
{
Log::info("Merging catalog and dumped items into real inventory response");
size_t closePos = data.rfind("]}");
if (closePos != std::string::npos)
{
uint64_t now = time(nullptr);
std::string injected;
injected.reserve((g_allObjectIds.size() + g_stackableItems.size()) * 80);
std::unordered_set<std::string> seenIds;
size_t pos = 0;
while ((pos = data.find("\"objectId\":\"", pos)) != std::string::npos)
{
pos += 12;
size_t end = data.find("\"", pos);
if (end != std::string::npos)
{
seenIds.insert(data.substr(pos, end - pos));
pos = end;
}
}
auto injectItem = [&](const std::string& id, int qty) {
if (id.empty() || seenIds.count(id)) return;
injected += std::format(",{{\"lastUpdateAt\":{},\"quantity\":{},\"objectId\":\"{}\"}}", now, qty, id);
seenIds.insert(id);
};
for (const auto& id : g_allObjectIds) injectItem(id, 1);
for (const auto& id : g_stackableItems) injectItem(id, 100);
for (const auto& id : g_uniqueItems) injectItem(id, 1);
for (const auto& id : g_perks) injectItem(id, 3);
if (!injected.empty()) data.insert(closePos, injected);
Log::info("Injected {} items into global inventory",
std::count(injected.begin(), injected.end(), '{'));
}
}
else
Log::warning("No catalog data available to inject into global inventory yet!");
}
else if (url.find("api/v1/dbd-character-data/") != std::string::npos)
{
std::vector<std::string> localStackable;
std::vector<std::string> localUnique;
std::vector<std::string> localPerks;
{
std::lock_guard<std::mutex> lock(g_dataMutex);
localStackable = g_stackableItems;
localUnique = g_uniqueItems;
localPerks = g_perks;
}
if (!localStackable.empty() || !localUnique.empty() || !localPerks.empty())
{
std::unordered_set<std::string> perkSet(localPerks.begin(), localPerks.end());
std::unordered_set<std::string> uniqueSet(localUnique.begin(), localUnique.end());
std::regex qtyRegex(R"("quantity"\s*:\s*\d+)");
const char* targetArrays[] = {"\"characterItems\":[", "\"inventory\":[", "\"bloodwebRewards\":[",
"\"prestigeRewards\":["};
bool isBloodweb = url.find("/bloodweb") != std::string::npos;
size_t arraysModified = 0;
for (const char* arrayKey : targetArrays)
{
size_t pos = 0;
while ((pos = data.find(arrayKey, pos)) != std::string::npos)
{
pos += strlen(arrayKey);
size_t endPos = data.find("]", pos);
if (endPos == std::string::npos) break;
std::string currentItems = data.substr(pos, endPos - pos);
std::unordered_set<std::string> seenIds;
std::unordered_set<std::string> stackableSet(localStackable.begin(), localStackable.end());
size_t itemPos = 0;
while ((itemPos = currentItems.find("\"itemId\":\"", itemPos)) != std::string::npos)
{
size_t idStart = itemPos + 10;
size_t idEnd = currentItems.find("\"", idStart);
if (idEnd == std::string::npos) break;
std::string id = currentItems.substr(idStart, idEnd - idStart);
seenIds.insert(id);
int qty = -1;
if (perkSet.count(id))
qty = 3;
else if (uniqueSet.count(id))
qty = 1;
else if (stackableSet.count(id))
qty = 100;
if (qty != -1)
{
size_t objStart = currentItems.rfind("{", itemPos);
size_t objEnd = currentItems.find("}", itemPos);
if (objStart != std::string::npos && objEnd != std::string::npos && objStart < objEnd)
{
std::string objStr = currentItems.substr(objStart, objEnd - objStart + 1);
if (std::regex_search(objStr, qtyRegex))
objStr =
std::regex_replace(objStr, qtyRegex, "\"quantity\":" + std::to_string(qty));
else
objStr.insert(objStr.length() - 1, ",\"quantity\":" + std::to_string(qty));
currentItems.replace(objStart, objEnd - objStart + 1, objStr);
itemPos = objStart + objStr.length();
}
else
itemPos = idEnd;
}
else
itemPos = idEnd;
}
auto injectIfMissing = [&](const std::vector<std::string>& list, int qty) {
for (const auto& id : list)
{
if (seenIds.find(id) == seenIds.end())
{
if (!currentItems.empty() && currentItems.back() != ',') currentItems += ",";
currentItems +=
"{\"itemId\":\"" + id + "\",\"quantity\":" + std::to_string(qty) + "}";
seenIds.insert(id);
}
}
};
if (!isBloodweb && (std::string_view(arrayKey) == "\"characterItems\":[" ||
std::string_view(arrayKey) == "\"inventory\":["))
{
injectIfMissing(localStackable, 100);
injectIfMissing(localUnique, 1);
injectIfMissing(localPerks, 3);
}
data.replace(pos, endPos - pos, currentItems);
pos += currentItems.length();
arraysModified++;
}
}
Log::info("Spoofed items in {} character data arrays across response", arraysModified);
}
else
Log::warning("No custom dumped items available to inject into character data!");
}
});
spoofer->init(g_Proxy);
/*
pause