diff --git a/CMakeLists.txt b/CMakeLists.txt index 1054f54..75975d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,15 @@ set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT d group_files("${UNLOCKER_SOURCES}") +# copy resources +file(GLOB JSON_RES_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/res/*.json") +add_custom_command(TARGET dbd-unlocker POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${JSON_RES_FILES} + "$/" + COMMENT "Copying JSON resources to executable directory" +) + # ------------------------------ # dumper # ------------------------------ diff --git a/src/unlocker/main.cpp b/src/unlocker/main.cpp index 60e5a24..8e9ed19 100644 --- a/src/unlocker/main.cpp +++ b/src/unlocker/main.cpp @@ -4,6 +4,26 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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) { @@ -52,6 +72,144 @@ BOOL WINAPI consoleHandler(DWORD dwType) return FALSE; } +std::mutex g_dataMutex; +std::vector g_allObjectIds; +std::vector g_allCharacterIds; +std::vector g_dumpedItems; + +void loadJsonDump(const std::string& path) +{ + std::ifstream file(path); + if (!file.is_open()) + { + Log::warning("Failed to open dump file: {}", path); + return; + } + std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + try + { + simdjson::ondemand::parser parser; + simdjson::padded_string json(content); + auto doc = parser.iterate(json); + + for (auto field : doc.get_object()) + { + for (auto item : field.value().get_array()) + { + std::string_view id; + if (item.get_string().get(id) == simdjson::SUCCESS) + { + g_dumpedItems.push_back(std::string(id)); + } + } + } + } + catch (const std::exception& e) + { + Log::warning("Failed to parse dump file {}: {}", path, e.what()); + } +} + +void loadAllCustomItems() +{ + std::lock_guard lock(g_dataMutex); + g_dumpedItems.clear(); + loadJsonDump(getExeDir() + "itemdb.json"); + loadJsonDump(getExeDir() + "itemaddonsdb.json"); + loadJsonDump(getExeDir() + "offeringdb.json"); + Log::info("Loaded {} custom items from dumps", g_dumpedItems.size()); +} + +void parseCatalog(const std::string& data) +{ + try + { + simdjson::ondemand::parser parser; + simdjson::padded_string json(data); + auto doc = parser.iterate(json); + + std::vector newObjectIds; + std::vector 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 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(file)), std::istreambuf_iterator()); + 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(); @@ -59,7 +217,12 @@ int main() Log::info("Unlocker init"); + loadCatalogOnStartup(); + loadAllCustomItems(); + /* + proxy setup + */ Log::verbose("Starting proxy"); Proxy* proxy = new Proxy(); if (!proxy->Init()) @@ -67,13 +230,129 @@ int main() Log::error("Proxy failed to start"); return 1; } - setProxy(true, std::format("127.0.0.1:{}", PROXY_PORT)); + /* + listeners + */ + + + proxy->OnClientRequest.addListener([](const std::string& url, std::string& data) { + if (url.find("/v1/party") != std::string::npos) + { + data; + Log::verbose("party req"); + } + }); + + proxy->OnServerResponse.addListener([](const std::string& url, std::string& data) { + bool dumpOnly = true; + + if (url.find("/v1/party") != std::string::npos) + { + data; + Log::verbose("party res"); + } + + Log::verbose("res: {}", url); + if (url.find("api/v1/extensions/store/getCatalogItems") != std::string::npos) + updateCatalog(data); + //else if (dumpOnly == true) + // return; + else if (url.find("api/v1/dbd-inventories/all") != std::string::npos) + { + std::vector localObjectIds; + { + std::lock_guard lock(g_dataMutex); + localObjectIds = g_allObjectIds; + } + + if (!localObjectIds.empty()) + { + Log::info("Merging {} catalog items into real inventory response", localObjectIds.size()); + + size_t closePos = data.rfind("]}"); + if (closePos != std::string::npos) + { + uint64_t now = time(nullptr); + std::string injected; + injected.reserve(localObjectIds.size() * 60); + + for (const auto& id : localObjectIds) + { + if (data.find("\"objectId\":\"" + id + "\"") != std::string::npos) continue; + + injected += ",{\"lastUpdateAt\":" + std::to_string(now) + ",\"quantity\":1,\"objectId\":\"" + + id + "\"}"; + } + + 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 data available to inject into inventory yet!"); + } + else if (url.find("api/v1/dbd-character-data/get-all") != std::string::npos) + { + std::vector localItems; + { + std::lock_guard lock(g_dataMutex); + localItems = g_dumpedItems; + } + + if (!localItems.empty()) + { + size_t pos = 0; + size_t count = 0; + while ((pos = data.find("\"characterItems\":[", pos)) != std::string::npos) + { + pos += 18; + + size_t endPos = data.find("]", pos); + std::string currentItems; + if (endPos != std::string::npos) currentItems = data.substr(pos, endPos - pos); + + std::string injected; + for (const auto& item : localItems) + { + if (endPos == std::string::npos || + currentItems.find("\"itemId\":\"" + item + "\"") == std::string::npos) + { + injected += "{\"itemId\":\"" + item + "\",\"quantity\":100},"; + } + } + + if (!injected.empty()) + { + if (data[pos] == ']') injected.pop_back(); + + data.insert(pos, injected); + pos += injected.length(); + count++; + } + } + Log::info("Injected missing items into {} character inventories", count); + } + else + { + Log::warning("No custom dumped items available to inject into character inventory!"); + } + } + }); + + /* + pause + */ Log::verbose("Proxy running (CTRL+C to stop)"); while (running) Sleep(100); + /* + cleanup + */ Log::verbose("Shutting down proxy"); proxy->Shutdown(); delete proxy;