472 lines
16 KiB
C++
472 lines
16 KiB
C++
#include "proxy.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>
|
|
|
|
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)
|
|
{
|
|
INTERNET_PER_CONN_OPTION_LIST list;
|
|
INTERNET_PER_CONN_OPTION options[3];
|
|
unsigned long listSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
|
|
|
|
options[0].dwOption = INTERNET_PER_CONN_FLAGS;
|
|
if (enable)
|
|
options[0].Value.dwValue = PROXY_TYPE_PROXY | PROXY_TYPE_DIRECT;
|
|
else
|
|
options[0].Value.dwValue = PROXY_TYPE_DIRECT;
|
|
|
|
options[1].dwOption = INTERNET_PER_CONN_PROXY_SERVER;
|
|
options[1].Value.pszValue = const_cast<char*>(proxyAddr.c_str());
|
|
|
|
options[2].dwOption = INTERNET_PER_CONN_PROXY_BYPASS;
|
|
options[2].Value.pszValue = const_cast<char*>("<local>");
|
|
|
|
list.dwSize = sizeof(INTERNET_PER_CONN_OPTION_LIST);
|
|
list.pszConnection = NULL;
|
|
list.dwOptionCount = 3;
|
|
list.dwOptionError = 0;
|
|
list.pOptions = options;
|
|
|
|
if (!InternetSetOptionA(NULL, INTERNET_OPTION_PER_CONNECTION_OPTION, &list, listSize))
|
|
{
|
|
Log::error("Failed to set proxy options, Err: {}", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
InternetSetOption(NULL, INTERNET_OPTION_SETTINGS_CHANGED, NULL, 0);
|
|
InternetSetOption(NULL, INTERNET_OPTION_REFRESH, NULL, 0);
|
|
|
|
return true;
|
|
}
|
|
|
|
Proxy* g_proxy = nullptr;
|
|
bool running = true;
|
|
|
|
void cleanup()
|
|
{
|
|
static std::mutex cleanupMutex;
|
|
std::lock_guard<std::mutex> lock(cleanupMutex);
|
|
static bool cleaned = false;
|
|
if (cleaned) return;
|
|
cleaned = true;
|
|
|
|
if (g_proxy)
|
|
{
|
|
Log::info("Shutting down proxy");
|
|
g_proxy->Shutdown();
|
|
}
|
|
|
|
Log::info("Restoring system proxy settings");
|
|
setProxy(false, "");
|
|
}
|
|
|
|
BOOL WINAPI consoleHandler(DWORD dwType)
|
|
{
|
|
if (dwType == CTRL_C_EVENT || dwType == CTRL_CLOSE_EVENT || dwType == CTRL_LOGOFF_EVENT || dwType == CTRL_SHUTDOWN_EVENT)
|
|
{
|
|
running = false;
|
|
cleanup();
|
|
exit(0);
|
|
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();
|
|
SetConsoleCtrlHandler(consoleHandler, TRUE);
|
|
atexit(cleanup);
|
|
|
|
Log::info("Init");
|
|
|
|
loadCatalogOnStartup();
|
|
loadAllCustomItems();
|
|
|
|
/*
|
|
proxy setup
|
|
*/
|
|
Log::info("Starting proxy");
|
|
g_proxy = new Proxy();
|
|
if (!g_proxy->Init())
|
|
{
|
|
Log::error("Proxy failed to start");
|
|
return 1;
|
|
}
|
|
setProxy(true, std::format("127.0.0.1:{}", PROXY_PORT));
|
|
|
|
/*
|
|
listeners
|
|
*/
|
|
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
|
|
|
|
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() || !g_stackableItems.empty() || !g_uniqueItems.empty())
|
|
{
|
|
Log::info("Merging catalog and custom items into real inventory response");
|
|
|
|
std::regex qtyRegex(R"("quantity"\s*:\s*\d+)");
|
|
for (const auto& id : g_stackableItems)
|
|
{
|
|
size_t pos = data.find("\"objectId\":\"" + id + "\"");
|
|
if (pos != std::string::npos)
|
|
{
|
|
size_t start = data.rfind("{", pos);
|
|
size_t end = data.find("}", pos);
|
|
if (start != std::string::npos && end != std::string::npos && start < end)
|
|
{
|
|
std::string objStr = data.substr(start, end - start + 1);
|
|
objStr = std::regex_replace(objStr, qtyRegex, "\"quantity\":100");
|
|
data.replace(start, end - start + 1, objStr);
|
|
}
|
|
}
|
|
}
|
|
|
|
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() + g_uniqueItems.size()) * 60);
|
|
|
|
std::unordered_set<std::string> dbIds;
|
|
for (auto& id : g_stackableItems)
|
|
dbIds.insert(id);
|
|
for (auto& id : g_uniqueItems)
|
|
dbIds.insert(id);
|
|
for (auto& id : g_perks)
|
|
dbIds.insert(id);
|
|
|
|
std::unordered_set<std::string> handledIds;
|
|
auto injectItem = [&](const std::string& id, int qty) {
|
|
if (id.empty()) return;
|
|
if (handledIds.count(id)) return;
|
|
|
|
std::string searchPat = "\"objectId\":\"" + id + "\"";
|
|
if (data.find(searchPat) != std::string::npos)
|
|
{
|
|
handledIds.insert(id);
|
|
return;
|
|
}
|
|
|
|
injected += ",{\"lastUpdateAt\":" + std::to_string(now) +
|
|
",\"quantity\":" + std::to_string(qty) + ",\"objectId\":\"" + id + "\"}";
|
|
handledIds.insert(id);
|
|
};
|
|
|
|
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);
|
|
|
|
for (const auto& id : g_allObjectIds)
|
|
{
|
|
if (dbIds.find(id) == dbIds.end()) injectItem(id, 1);
|
|
}
|
|
|
|
if (!injected.empty()) data.insert(closePos, injected);
|
|
|
|
Log::info("Injected {} new items into inventory",
|
|
injected.empty() ? 0 : std::count(injected.begin(), injected.end(), '{'));
|
|
}
|
|
}
|
|
else
|
|
Log::warning("No catalog or custom data available to inject into inventory yet!");
|
|
}
|
|
else if (url.find("api/v1/dbd-character-data/get-all") != 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())
|
|
{
|
|
size_t pos = 0;
|
|
size_t count = 0;
|
|
std::unordered_set<std::string> stackableSet(localStackable.begin(), localStackable.end());
|
|
std::unordered_set<std::string> uniqueSet(localUnique.begin(), localUnique.end());
|
|
std::unordered_set<std::string> perkSet(localPerks.begin(), localPerks.end());
|
|
|
|
while ((pos = data.find("\"characterItems\":[", pos)) != std::string::npos)
|
|
{
|
|
pos += 18;
|
|
|
|
size_t endPos = data.find("]", pos);
|
|
if (endPos != std::string::npos)
|
|
{
|
|
std::string currentItems = data.substr(pos, endPos - pos);
|
|
std::string newItems;
|
|
newItems.reserve(currentItems.size() * 2);
|
|
|
|
std::unordered_set<std::string> seenIds;
|
|
|
|
size_t itemPos = 0;
|
|
while ((itemPos = currentItems.find("\"itemId\":\"", itemPos)) != std::string::npos)
|
|
{
|
|
itemPos += 10;
|
|
size_t quotePos = currentItems.find("\"", itemPos);
|
|
if (quotePos == std::string::npos) break;
|
|
|
|
std::string id = currentItems.substr(itemPos, quotePos - itemPos);
|
|
seenIds.insert(id);
|
|
|
|
int qty = 100;
|
|
if (perkSet.count(id))
|
|
qty = 3;
|
|
else if (uniqueSet.count(id))
|
|
qty = 1;
|
|
|
|
newItems += "{\"itemId\":\"" + id + "\",\"quantity\":" + std::to_string(qty) + "},";
|
|
}
|
|
|
|
auto injectIfMissing = [&](const std::vector<std::string>& list, int qty) {
|
|
for (const auto& id : list)
|
|
{
|
|
if (seenIds.find(id) == seenIds.end())
|
|
{
|
|
newItems += "{\"itemId\":\"" + id + "\",\"quantity\":" + std::to_string(qty) + "},";
|
|
seenIds.insert(id);
|
|
}
|
|
}
|
|
};
|
|
|
|
injectIfMissing(localStackable, 100);
|
|
injectIfMissing(localUnique, 1);
|
|
injectIfMissing(localPerks, 3);
|
|
|
|
if (!newItems.empty()) newItems.pop_back(); // trailing comma
|
|
|
|
data.replace(pos, endPos - pos, newItems);
|
|
|
|
endPos = pos + newItems.length();
|
|
pos = endPos;
|
|
count++;
|
|
}
|
|
}
|
|
Log::info("Added missing items and targeted perk tiers in {} character inventories", count);
|
|
}
|
|
else
|
|
Log::warning("No custom dumped items available to inject into character inventory!");
|
|
}
|
|
});
|
|
|
|
/*
|
|
pause
|
|
*/
|
|
Log::info("Proxy running (CTRL+C to stop)");
|
|
while (running)
|
|
Sleep(100);
|
|
|
|
/*
|
|
cleanup
|
|
*/
|
|
cleanup();
|
|
|
|
return 0;
|
|
} |