From d4650aac84ac87b440bcb76fa03d1170546fb331 Mon Sep 17 00:00:00 2001 From: neru Date: Thu, 9 Apr 2026 06:38:08 -0300 Subject: [PATCH] feat: completely refactor dumper again --- src/unlocker/spoofing.cpp | 895 +++++++++++++++++--------------------- src/unlocker/spoofing.h | 22 +- 2 files changed, 412 insertions(+), 505 deletions(-) diff --git a/src/unlocker/spoofing.cpp b/src/unlocker/spoofing.cpp index 73077f2..7eee707 100644 --- a/src/unlocker/spoofing.cpp +++ b/src/unlocker/spoofing.cpp @@ -1,12 +1,13 @@ #include "spoofing.h" +#include "utils.h" +#include #include -#include -#include #include -#include +#include +#include -#include +#include #include @@ -14,24 +15,34 @@ using json = nlohmann::json; -std::string getExeDir() +static std::random_device rd; +static std::mt19937 gen(rd()); + +/* + utils +*/ +std::unordered_set slasherNames = { + "Chuckles", "Bob", "HillBilly", "Nurse", "Shape", "Witch", "Killer07", "Cannibal", "Bear", + "Nightmare", "Pig", "Clown", "Spirit", "Plague", "Ghostface", "Demogorgon", "Oni", "Gunslinger"}; + +bool isSlasher(std::string name) { - 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 ""; + if (slasherNames.contains(name) || (name.length() == 3 && name[0] == 'K')) return true; + return false; } +/* + setup / init +*/ void Spoofer::init(Proxy* proxy) { - loadData(); registerListeners(proxy); + loadData(); } void Spoofer::registerListeners(Proxy* proxy) { + Log::info("Registering Proxy event listeners for spoofer"); proxy->OnServerResponse.addListener([this](const std::string& url, std::string& body, std::string& respHeaders) { this->serverResponseHandler(url, body, respHeaders); }); @@ -41,242 +52,257 @@ void Spoofer::registerListeners(Proxy* proxy) }); } +#define LOADDATA(fileName, type, camperSet, slasherSet, errorMsg) \ + std::string path##type = utils::getExePath() + fileName; \ + std::ifstream file##type(path##type); \ + if (file##type.is_open()) \ + { \ + std::stringstream buff; \ + buff << file##type.rdbuf(); \ + if (!parseStackable(buff.str(), camperSet, slasherSet)) \ + Log::error("Failed to parse {} - {}", fileName, errorMsg); \ + else \ + Log::verbose("Loaded {} camper ids and {} slasher ids from {}", camperSet.size(), slasherSet.size(), \ + fileName); \ + } \ + else \ + Log::warning("Missing {} - {}", fileName, errorMsg); + void Spoofer::loadData() { - Log::info("Loading dump data"); + Log::info("Loading data"); - std::string catalogDumpPath = getExeDir() + "catalog.json"; - std::ifstream catalogFile(catalogDumpPath); + std::string catalogPath = utils::getExePath() + "catalog.json"; + std::ifstream catalogFile(catalogPath); if (catalogFile.is_open()) { - std::stringstream buffer; - buffer << catalogFile.rdbuf(); - parseCatalog(buffer.str()); + std::stringstream buff; + buff << catalogFile.rdbuf(); + if (!parseCatalog(buff.str())) Log::warning("Failed to parse catalog.json - Customizations won't be unlocked"); } else - Log::warning("Missing catalog.json"); + Log::warning("Missing catalog.json - Customizations won't be unlocked"); - std::string itemDumpPath = getExeDir() + "items.json"; - std::ifstream itemFile(itemDumpPath); - if (itemFile.is_open()) + LOADDATA("items.json", Items, _camperItemIds, _slasherPowerIds, "Items won't be added"); + LOADDATA("offerings.json", Offerings, _camperOfferingIds, _slasherOfferingIds, "Offerings won't be added"); + LOADDATA("addons.json", Addons, _camperAddonIds, _slasherAddonIds, "Addons won't be added"); + LOADDATA("perks.json", Perks, _camperPerkIds, _slasherPerkIds, "Perks won't be added"); +} + +/* + data parsing +*/ +bool Spoofer::parseCatalog(std::string data) +{ + json doc = json::parse(data, nullptr, false); + + if (doc.is_discarded()) { - try - { - std::string content((std::istreambuf_iterator(itemFile)), std::istreambuf_iterator()); - json doc = json::parse(content); - - if (doc.contains("Camper") && doc["Camper"].is_object()) - if (doc["Camper"].contains("Items") && doc["Camper"]["Items"].is_array()) - for (const auto& item : doc["Camper"]["Items"]) - if (item.is_string()) _camperItemIds.insert(item.get()); - - if (doc.contains("Slasher") && doc["Slasher"].is_object()) - if (doc["Slasher"].contains("Powers") && doc["Slasher"]["Powers"].is_array()) - for (const auto& item : doc["Slasher"]["Powers"]) - if (item.is_string()) _slasherPowerIds.insert(item.get()); - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", "items.json", e.what()); - } + Log::error("Failed to parse catalog"); + return false; } - else - Log::warning("Missing items.json"); - - std::string offeringDumpPath = getExeDir() + "offerings.json"; - std::ifstream offeringsFile(offeringDumpPath); - if (offeringsFile.is_open()) - { - try - { - std::string content((std::istreambuf_iterator(offeringsFile)), std::istreambuf_iterator()); - json doc = json::parse(content); - - if (doc.contains("Slashers") && doc["Slashers"].is_array()) - for (const auto& offering : doc["Slashers"]) - if (offering.is_string()) _slasherOfferingIds.insert(offering.get()); - - if (doc.contains("Campers") && doc["Campers"].is_array()) - for (const auto& offering : doc["Campers"]) - if (offering.is_string()) _camperOfferingIds.insert(offering.get()); - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", "offerings.json", e.what()); - } - } - else - Log::warning("Missing offerings.json"); - - std::string addonDumpPath = getExeDir() + "addons.json"; - std::ifstream addonFile(addonDumpPath); - if (addonFile.is_open()) - { - try - { - std::string content((std::istreambuf_iterator(addonFile)), std::istreambuf_iterator()); - json doc = json::parse(content); - - if (doc.contains("Slashers") && doc["Slashers"].is_array()) - for (const auto& item : doc["Slashers"]) - if (item.is_string()) _slasherAddonIds.insert(item.get()); - - if (doc.contains("Campers") && doc["Campers"].is_array()) - for (const auto& power : doc["Campers"]) - if (power.is_string()) _camperAddonIds.insert(power.get()); - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", "addons.json", e.what()); - } - } - else - Log::warning("Missing addons.json"); try { - std::string perkDumpPaths = getExeDir() + "perks.json"; - std::ifstream perkFile(perkDumpPaths); - if (perkFile.is_open()) - { - std::string content((std::istreambuf_iterator(perkFile)), std::istreambuf_iterator()); - json doc = json::parse(content); + const auto& catalogData = doc.at("data"); - if (doc.contains("Slashers") && doc["Slashers"].is_array()) - for (const auto& item : doc["Slashers"]) - if (item.is_string()) _slasherPerkIds.insert(item.get()); + auto extractIds = [&](const std::string& key, std::unordered_set& targetSet) { + if (catalogData.contains(key) && catalogData[key].contains("items")) + for (const auto& id : catalogData[key]["items"]) + if (id.is_string()) + targetSet.insert(id.get()); + else + Log::warning("Catalog missing or invalid category: {}", key); + }; - if (doc.contains("Campers") && doc["Campers"].is_array()) - for (const auto& power : doc["Campers"]) - if (power.is_string()) _camperPerkIds.insert(power.get()); - } - else - Log::warning("Missing perks.json"); + extractIds("item", _catalogItemIds); + extractIds("outfit", _catalogOutfitIds); } - catch (const json::parse_error& e) + catch (const json::exception& e) { - Log::error("JSON parse error in {}: {}", "perks.json", e.what()); + Log::error("Invalid catalog format: {}", e.what()); + return false; + } + + Log::info("Parsed {} items and {} outfits from catalog", _catalogItemIds.size(), _catalogOutfitIds.size()); + + return true; +} + +bool Spoofer::parseStackable(std::string data, std::unordered_set& camperSet, + std::unordered_set& slasherSet) +{ + json doc = json::parse(data, nullptr, false); + if (doc.is_discarded()) + { + Log::error("JSON parse error @ parseStackable"); + return false; + } + + auto populate = [&](const std::string& key, std::unordered_set& targetSet) { + if (doc.contains(key) && doc[key].is_array()) + for (const auto& item : doc[key]) + if (item.is_string()) + targetSet.insert(item.get()); + else + Log::warning("Missing stackables array ({})", key); + }; + + populate("Slashers", slasherSet); + populate("Campers", camperSet); + + return true; +} + +/* + misc functions +*/ +std::string Spoofer::getRandomItem() +{ + std::vector*> allSets = { + &_camperItemIds, &_slasherPowerIds, &_camperOfferingIds, &_slasherOfferingIds, + &_camperAddonIds, &_slasherAddonIds, &_camperPerkIds, &_slasherPerkIds}; + + std::vector*> validSets; + for (auto* s : allSets) + if (!s->empty()) validSets.push_back(s); + + if (validSets.empty()) return "Spring2024Offering"; + + std::uniform_int_distribution<> setDist(0, static_cast(validSets.size()) - 1); + const auto& selectedSet = *validSets[setDist(gen)]; + + std::uniform_int_distribution<> itemDist(0, static_cast(selectedSet.size()) - 1); + auto it = selectedSet.begin(); + std::advance(it, itemDist(gen)); + + return *it; +} + +int Spoofer::getRandomQuantity() +{ + std::uniform_int_distribution<> dist(64, 97); + return dist(gen); +} + +void Spoofer::generateBloodweb(nlohmann::json& js) +{ + if (!js.is_object()) js = json::object(); + + std::vector paths; + json ringDataArray = json::array(); + + ringDataArray.push_back({{"nodeData", {{{"nodeId", "0"}, {"state", "Collected"}}}}}); + + int nodesPerRing[] = {6, 12, 12}; + std::vector prevRingNodes = {"0"}; + + for (int ring = 1; ring <= 3; ++ring) + { + json nodeDataArray = json::array(); + std::vector currentRingNodes; + int numNodes = nodesPerRing[ring - 1]; + + for (int i = 1; i <= numNodes; ++i) + { + std::string childId = std::to_string((ring * 100) + i); + currentRingNodes.push_back(childId); + + int parentIndex = (i - 1) / (numNodes / static_cast(prevRingNodes.size())); + std::string parentId = prevRingNodes[(std::min)(parentIndex, (int)prevRingNodes.size() - 1)]; + + paths.push_back(parentId + "_" + childId); + + std::string item = getRandomItem(); + if (item.empty()) item = "Spring2024Offering"; + + nodeDataArray.push_back({{"nodeId", childId}, {"state", "Collected"}, {"contentId", item}}); + } + ringDataArray.push_back({{"nodeData", nodeDataArray}}); + prevRingNodes = std::move(currentRingNodes); + } + + js["paths"] = paths; + js["ringData"] = ringDataArray; +} + +/* + data modification +*/ +void Spoofer::modifyCharacterData(json& js) +{ + if (!js.contains("characterName") || !js["characterName"].is_string()) + { + Log::verbose("attempted to modify invalid char"); return; } - Log::info("Loaded: Camper items={}, Slasher powers={}, " - "Slasher addons={}, Camper addons={}, Camper perks={}, Slasher perks={}" - "Slasher offerings={}, Camper offerings={}", - _camperItemIds.size(), _slasherPowerIds.size(), _slasherAddonIds.size(), _camperAddonIds.size(), - _camperPerkIds.size(), _slasherPerkIds.size(), _slasherOfferingIds.size(), _camperOfferingIds.size()); -} + std::string name = js["characterName"]; + bool slasher = isSlasher(js["characterName"]); -void Spoofer::parseCatalog(std::string data) -{ - try + bool needsSpoofing = false; + if (js.value("isEntitled", true) == false) { - json doc = json::parse(data); - - if (doc.contains("data")) doc = doc["data"]; - - if (doc.contains("item") && doc["item"].contains("items") && doc["item"]["items"].is_array()) - for (const auto& item : doc["item"]["items"]) - if (item.is_string()) _catalogItemIds.insert(item.get()); - - if (doc.contains("outfit") && doc["outfit"].contains("items") && doc["outfit"]["items"].is_array()) - for (const auto& item : doc["outfit"]["items"]) - if (item.is_string()) _catalogOutfitIds.insert(item.get()); - - Log::info("Parsed {} items and {} outfits from catalog", _catalogItemIds.size(), _catalogOutfitIds.size()); + _unownedCharacters.insert(name); + js["isEntitled"] = true; + js["purchaseInfo"] = {{"quantity", 1}, + {"origin", "PlayerInventory"}, + {"reason", "Item(s) added via Purchase"}, + {"lastUpdateAt", std::time(nullptr)}, + {"objectId", name}}; + needsSpoofing = true; } - catch (const json::parse_error& e) + else if (_unownedCharacters.contains(name)) + needsSpoofing = true; + + /* + modifications for unowned characters (spoof level and fake bloodweb) + */ + if (needsSpoofing) { - Log::error("JSON parse error in {}: {}", "parseCatalog", e.what()); + if (js.contains("bloodWebLevel") && js["bloodWebLevel"].is_number() && js["bloodWebLevel"] <= 15) + if (!js.contains("prestigeLevel") || (js["prestigeLevel"].is_number() && js["prestigeLevel"] <= 0)) + js["bloodWebLevel"] = 16; + + if (js.contains("bloodWebData")) generateBloodweb(js["bloodWebData"]); } -} - -void Spoofer::parseAndDumpCatalog(std::string& data) -{ - std::string path = getExeDir() + "catalog.json"; - std::ofstream file(path); - file << data; - file.close(); - Log::info("Raw catalog saved to {}", path); - - parseCatalog(data); -} - -std::unordered_set killerNames = { - "Chuckles", "Bob", "HillBilly", "Nurse", "Shape", "Witch", "Killer07", "Cannibal", "Bear", - "Nightmare", "Pig", "Clown", "Spirit", "Plague", "Ghostface", "Demogorgon", "Oni", "Gunslinger"}; - -void Spoofer::modifyCharacterData(json& js) -{ - std::unordered_set existingItemIds; - bool isSlasher = false; - - if (js.contains("characterName") && js["characterName"].is_string()) + else { - std::string name = js["characterName"]; - if (killerNames.contains(name) || (name.length() == 3 && name[0] == 'K')) isSlasher = true; - } - - if (js.contains("prestigeLevel") && js["prestigeLevel"].get() <= 1) - if (js.contains("bloodWebLevel") && js["bloodWebLevel"].get() <= 15) js["bloodWebLevel"] = 16; - - if (js.contains("bloodWebData")) - { - static std::random_device rd; - static std::mt19937 rng(rd()); - - std::unordered_set stackableIds; - stackableIds.insert(_camperItemIds.begin(), _camperItemIds.end()); - stackableIds.insert(_camperOfferingIds.begin(), _camperOfferingIds.end()); - stackableIds.insert(_camperAddonIds.begin(), _camperAddonIds.end()); - stackableIds.insert(_slasherAddonIds.begin(), _slasherAddonIds.end()); - stackableIds.insert(_slasherOfferingIds.begin(), _slasherOfferingIds.end()); - - std::string selectedContentId = "Spring2024Offering"; - auto it = stackableIds.begin(); - std::advance(it, std::uniform_int_distribution<>(0, static_cast(_camperOfferingIds.size()) - 1)(rng)); - selectedContentId = *it; - - std::vector paths; - json ringDataArray = json::array(); - - ringDataArray.push_back({{"nodeData", json::array({{{"nodeId", "0"}, {"state", "Collected"}}})}}); - - int nodesPerRing[] = {6, 12, 12}; - std::vector prevRingNodes = {"0"}; - - for (int ring = 1; ring <= 3; ++ring) + /* + ghost node hotfix (untested) + */ + /* if (js.contains("bloodWebData") && js["bloodWebData"].contains("ringData")) { - json nodeDataArray = json::array(); - std::vector currentRingNodes; - int numNodes = nodesPerRing[ring - 1]; + auto& ringData = js["bloodWebData"]["ringData"]; - for (int i = 1; i <= numNodes; ++i) + for (auto& ring : ringData) { - std::string childId = std::to_string((ring * 100) + i); - currentRingNodes.push_back(childId); - - int parentIndex = (i - 1) / (numNodes / static_cast(prevRingNodes.size())); - - if (parentIndex >= static_cast(prevRingNodes.size())) - parentIndex = static_cast(prevRingNodes.size()) - 1; - - std::string parentId = prevRingNodes[parentIndex]; - paths.push_back(parentId + "_" + childId); - - nodeDataArray.push_back( - {{"nodeId", childId}, {"state", "Collected"}, {"contentId", selectedContentId}}); + if (ring.contains("nodeData") && ring["nodeData"].is_array()) + { + for (auto& node : ring["nodeData"]) + { + if (node.contains("nodeId") && node["nodeId"] != "0") + { + if (!node.contains("contentId") || node["contentId"].get().empty()) + { + node["contentId"] = "Spring2024Offering"; + if (!node.contains("state")) + node["state"] = "Available"; + } + } + } + } } - - ringDataArray.push_back({{"nodeData", nodeDataArray}}); - prevRingNodes = std::move(currentRingNodes); - } - - js["bloodWebData"]["paths"] = paths; - js["bloodWebData"]["ringData"] = ringDataArray; + }*/ } + /* + item spoofing + */ if (js.contains("characterItems") && js["characterItems"].is_array()) { + std::unordered_set existingItemIds; + std::unordered_set stackableIds; stackableIds.insert(_camperItemIds.begin(), _camperItemIds.end()); stackableIds.insert(_camperOfferingIds.begin(), _camperOfferingIds.end()); @@ -286,320 +312,187 @@ void Spoofer::modifyCharacterData(json& js) for (auto& item : js["characterItems"]) { + /* + set existing items to rnd number + */ if (item.contains("itemId") && item["itemId"].is_string()) { std::string itemId = item["itemId"]; existingItemIds.insert(itemId); - - if (_slasherPowerIds.find(itemId) != _slasherPowerIds.end()) - isSlasher = true; - else if (stackableIds.contains(itemId)) - item["quantity"] = 100; + if (stackableIds.contains(itemId)) item["quantity"] = getRandomQuantity(); } } - if (!isSlasher) + auto appendItems = [&](const std::unordered_set& idList, bool isPerk) { + for (const auto& itemId : idList) + if (existingItemIds.find(itemId) == existingItemIds.end()) + js["characterItems"].push_back( + {{"itemId", itemId}, {"quantity", isPerk ? 3 : getRandomQuantity()}}); + }; + + if (!slasher) { - for (const std::string& itemId : _camperItemIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 100}}); - for (const std::string& itemId : _camperAddonIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 100}}); - for (const std::string& itemId : _camperOfferingIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 100}}); - for (const std::string& itemId : _camperPerkIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 3}}); + appendItems(_camperItemIds, false); + appendItems(_camperAddonIds, false); + appendItems(_camperOfferingIds, false); + appendItems(_camperPerkIds, true); } else { - for (const std::string& itemId : _slasherAddonIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 100}}); - for (const std::string& itemId : _slasherOfferingIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 100}}); - for (const std::string& itemId : _slasherPerkIds) - if (existingItemIds.find(itemId) == existingItemIds.end()) - js["characterItems"].push_back({{"itemId", itemId}, {"quantity", 3}}); + appendItems(_slasherAddonIds, false); + appendItems(_slasherOfferingIds, false); + appendItems(_slasherPerkIds, true); } } + + Log::verbose("Spoofed data for character {}", name); } +/* + endpoint handlers +*/ +void Spoofer::onGetAll(std::string& body) +{ + json doc = json::parse(body, nullptr, false); + if (doc.is_discarded()) return Log::error("JSON parse error for dbd-character-data/get-all"); + if (!doc.contains("list") || !doc["list"].is_array()) + return Log::error("Invalid json for dbd-character-data/get-all"); + + for (auto& charData : doc["list"]) + modifyCharacterData(charData); + + body = doc.dump(); +} + +void Spoofer::onInventoryAll(std::string& body) +{ + json doc = json::parse(body, nullptr, false); + if (doc.is_discarded()) return Log::error("JSON parse error for dbd-inventories/all"); + if (!doc.contains("inventoryItems") || !doc["inventoryItems"].is_array()) + return Log::error("Invalid json for JSON parse error for dbd-inventories/all"); + + auto& itemsArr = doc["inventoryItems"]; + std::unordered_set existingIds; + int64_t now = std::time(nullptr); + + /* + item updates + */ + struct Category + { + const std::unordered_set& source; + int quantity; + }; + + std::vector categories = {{_camperPerkIds, 3}, {_slasherPerkIds, 3}, {_camperOfferingIds, -1}, + {_slasherOfferingIds, -1}, {_catalogOutfitIds, 1}, {_catalogItemIds, 1}}; + + for (auto& item : itemsArr) + { + std::string id = item.value("objectId", ""); + if (id.empty()) continue; + + existingIds.insert(id); + + for (const auto& cat : categories) + { + if (cat.source.contains(id)) + { + item["quantity"] = (cat.quantity == -1) ? getRandomQuantity() : cat.quantity; + break; + } + } + } + + /* + item inserts + */ + for (const auto& cat : categories) + { + for (const std::string& id : cat.source) + { + if (!existingIds.contains(id)) + { + int qty = (cat.quantity == -1) ? getRandomQuantity() : cat.quantity; + itemsArr.push_back({{"objectId", id}, {"quantity", qty}, {"lastUpdateAt", now}}); + existingIds.insert(id); + } + } + } + + body = doc.dump(); + Log::verbose("Inventory spoofed"); +} + +void Spoofer::onBloodweb(std::string& body, std::string& respHeaders) +{ + json doc = json::parse(body, nullptr, false); + if (doc.is_discarded()) return Log::error("JSON parse error for bloodweb response"); + + if (body.find("NotAllowedException") != std::string::npos && body.find("not owned") != std::string::npos) + { + Log::info("Spoofing bloodweb error for unowned character"); + json mock; + mock["bloodWebLevelChanged"] = false; + mock["updatedWallets"] = json::array(); + mock["bloodWebLevel"] = 16; + mock["prestigeLevel"] = 0; + mock["bloodWebData"] = json::object(); + mock["characterItems"] = json::array(); + mock["characterName"] = this->_lastBloodWebChar; + mock["isEntitled"] = true; + mock["purchaseInfo"] = {{"quantity", 1}, + {"origin", "PlayerInventory"}, + {"reason", "Item(s) added via Purchase"}, + {"lastUpdateAt", std::time(nullptr)}, + {"objectId", this->_lastBloodWebChar}}; + + _unownedCharacters.insert(this->_lastBloodWebChar); // probably not needed but just in case + + modifyCharacterData(mock); + + std::regex statusRegex(R"(HTTP\/\d\.\d\s+403)"); + respHeaders = std::regex_replace(respHeaders, statusRegex, "HTTP/1.1 200"); + + body = mock.dump(); + Log::verbose("Spoofed bloodweb request for unowned character."); + return; + } + + modifyCharacterData(doc); + body = doc.dump(); + Log::verbose("Spoofed bloodweb items for owned character"); +} + +/* + event handlers +*/ void Spoofer::serverResponseHandler(const std::string& url, std::string& body, std::string& respHeaders) { if (url.find("bhvrdbd.com") == std::string::npos) return; + std::lock_guard lock(_mtx); + #ifdef _DEBUG - else - Log::verbose("BHVR api res @ {}", url); + Log::verbose("BHVR api res @ {}", url); #endif - if (url.find("api/v1/extensions/store/getCatalogItems") != std::string::npos) return parseAndDumpCatalog(body); - - if (url.find("api/v1/messages/listV2") != std::string::npos) - { - try - { - auto now = std::chrono::system_clock::now(); - auto now_seconds = std::chrono::duration_cast(now.time_since_epoch()).count(); - auto now_milliseconds = - std::chrono::duration_cast(now.time_since_epoch()).count(); - - json doc = json::parse(body); - if (!doc.contains("messages")) return; - - json msg; - /* - msg base - */ - msg["allowedPlatforms"] = json::array({"egs", "grdk", "ps4", "ps5", "steam", "xbox", "xsx"}); - msg["flag"] = "READ"; - msg["gameSpecificData"] = {}; - msg["read"] = false; - msg["tag"] = json::array({"inbox"}); - msg["expireAt"] = now_seconds + (1337 * 24 * 60 * 60); - msg["received"] = now_milliseconds; - msg["recipientId"] = "system"; - - /* - msg content - */ - json bodyContent; - bodyContent["sections"] = json::array(); - bodyContent["sections"].push_back( - {{"type", "text"}, - {"text", "Japan is turning footsteps into electricity!

Using piezoelectric tiles, every step " - "you take generates a small amount of energy. Millions of steps together can power LED " - "lights and displays in busy places like Shibuya Station.

A brilliant way to create a " - "sustainable and smart city — turning movement into clean, renewable energy."}}); - - bodyContent["image"] = { - {"packagedPath", "/Game/UI/UMGAssets/Icons/ItemAddons/iconAddon_powerBulb.iconAddon_powerBulb"}, - {"contentVersion", "ccc3f02b0a671fe19a0017d6a69293876a465fd9"}, - {"uri", ""}}; - - bodyContent["sections"].push_back( - {{"type", "itemshowcase"}, - {"rewards", - json::array({{{"type", "inventory"}, {"id", "ADDON_flashlight_oddbulb"}, {"amount", 1}}})}}); - - msg["message"] = {}; - msg["message"]["title"] = "Japan is turning footsteps into electricity"; - msg["message"]["body"] = bodyContent.dump(); - - doc["messages"].push_back(msg); - - body = doc.dump(); - - return; - } - catch (const json::parse_error& /*e*/) - { - } - } - - if (url.find("api/v1/dbd-inventories/all") != std::string::npos) - { - try - { - json doc = json::parse(body); - auto& itemsArr = doc["inventoryItems"]; - - std::unordered_set foundObjects; - std::unordered_set foundAddons; - std::unordered_set foundPerks; - std::unordered_set foundOfferings; - std::unordered_set foundCatalogItems; - - std::unordered_set offeringIds; - offeringIds.insert(_camperOfferingIds.begin(), _camperOfferingIds.end()); - offeringIds.insert(_slasherOfferingIds.begin(), _slasherOfferingIds.end()); - - std::unordered_set catalogIds; - catalogIds.insert(_catalogOutfitIds.begin(), _catalogOutfitIds.end()); - catalogIds.insert(_catalogItemIds.begin(), _catalogItemIds.end()); - - std::unordered_set perkIds; - perkIds.insert(_slasherPerkIds.begin(), _slasherPerkIds.end()); - perkIds.insert(_camperPerkIds.begin(), _camperPerkIds.end()); - - for (auto& item : itemsArr) - { - std::string objectId = item["objectId"]; - - if (perkIds.find(objectId) != perkIds.end()) - { - foundPerks.insert(objectId); - item["quantity"] = 3; - continue; - } - - if (offeringIds.find(objectId) != offeringIds.end()) - { - foundOfferings.insert(objectId); - item["quantity"] = 100; - continue; - } - - if (catalogIds.find(objectId) != catalogIds.end()) - { - foundCatalogItems.insert(objectId); - continue; - } - } - - for (const std::string& id : perkIds) - { - if (foundPerks.find(id) == foundPerks.end()) - { - itemsArr.push_back({ - {"objectId", id}, - {"quantity", 3}, - {"lastUpdateAt", std::time(nullptr)}, - }); - } - } - - for (const std::string& id : offeringIds) - { - if (foundOfferings.find(id) == foundOfferings.end()) - { - itemsArr.push_back({ - {"objectId", id}, - {"quantity", 100}, - {"lastUpdateAt", std::time(nullptr)}, - }); - } - } - - for (const std::string& id : catalogIds) - { - if (foundCatalogItems.find(id) == foundCatalogItems.end()) - { - itemsArr.push_back({ - {"objectId", id}, - {"quantity", 1}, - {"lastUpdateAt", std::time(nullptr)}, - }); - } - } - - std::string updatedJson = doc.dump(); - body = updatedJson; - - Log::verbose("Inventory updated: Items={}, Addons={}, Perks={}, Offerings={}", foundObjects.size(), - foundAddons.size(), foundPerks.size(), foundOfferings.size()); - - return; - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", url, e.what()); - return; - } - - return; - } - - if (url.find("api/v1/dbd-character-data/get-all") != std::string::npos) - { - try - { - json doc = json::parse(body); - auto& charList = doc["list"]; - - for (auto& charInfo : charList) - { - if (charInfo.contains("bloodWebLevel") && charInfo.at("bloodWebLevel").get() <= 15) - if (!charInfo.contains("prestigeLevel") || charInfo.at("prestigeLevel").get() <= 0) - charInfo["bloodWebLevel"] = 16; - - if (charInfo.contains("bloodWebData") && charInfo.at("bloodWebData").contains("level")) - charInfo["bloodWebData"]["level"] = 1; - - if (charInfo["isEntitled"] == false) - { - charInfo["isEntitled"] = true; - if (charInfo.contains("origin")) - { - charInfo["purchaseInfo"] = {{"quantity", 1}, - {"origin", "PlayerInventory"}, - {"reason", "Item(s) added via Purchase"}, - {"lastUpdateAt", 1770702482}, - {"objectId", charInfo["characterName"]}}; - } - } - - modifyCharacterData(charInfo); - } - body = doc.dump(); - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", url, e.what()); - } - return; - } - - if (url.find("api/v1/dbd-character-data/") != std::string::npos) - { - try - { - json doc = json::parse(body); - - if (body.find("NotAllowedException") != std::string::npos && body.find("not owned") != std::string::npos) - { - Log::info("Spoofing bloodweb error for unowned character"); - json mock; - mock["bloodWebLevelChanged"] = false; - mock["updatedWallets"] = json::array(); - mock["bloodWebLevel"] = 16; - mock["prestigeLevel"] = 0; - mock["bloodWebData"] = {{"ringData", json::array()}, {"paths", json::array()}}; - mock["characterItems"] = json::array(); - mock["characterName"] = this->_lastBloodWebChar; - - modifyCharacterData(mock); - - size_t firstSpace = respHeaders.find(' '); - if (firstSpace != std::string::npos) respHeaders.replace(firstSpace + 1, 3, "200"); - - body = mock.dump(); - return; - } - else - { - modifyCharacterData(doc); - body = doc.dump(); - } - return; - } - catch (const json::parse_error& e) - { - Log::error("JSON parse error in {}: {}", url, e.what()); - } - return; - } + if (url.find("api/v1/dbd-character-data/get-all") != std::string::npos) return onGetAll(body); + if (url.find("api/v1/dbd-inventories/all") != std::string::npos) return onInventoryAll(body); + if (url.find("api/v1/dbd-character-data/bloodweb") != std::string::npos || + url.find("api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos) + return onBloodweb(body, respHeaders); } void Spoofer::clientRequestHandler(std::string& url, const std::string& body, std::string& /*reqHeaders*/) { + if (url.find("bhvrdbd.com") == std::string::npos) return; + std::lock_guard lock(_mtx); + if (url.find("api/v1/dbd-character-data/bloodweb") != std::string::npos || url.find("api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos) { - try - { - json req = json::parse(body); - if (req.contains("characterName")) - { - this->_lastBloodWebChar = req["characterName"]; - Log::info("Detected bloodweb request for character: {}", this->_lastBloodWebChar); - } - } - catch (...) - { - } + json req = json::parse(body, nullptr, false); + if (req.is_discarded()) return Log::error("JSON parse error for bloodweb request handler"); + if (req.contains("characterName")) this->_lastBloodWebChar = req["characterName"]; } } diff --git a/src/unlocker/spoofing.h b/src/unlocker/spoofing.h index c28f001..621780f 100644 --- a/src/unlocker/spoofing.h +++ b/src/unlocker/spoofing.h @@ -5,6 +5,8 @@ #include #include +#include + #include class Spoofer @@ -16,11 +18,20 @@ class Spoofer void registerListeners(Proxy* proxy); void loadData(); - void parseCatalog(std::string data); + bool parseCatalog(std::string data); + bool parseStackable(std::string data, std::unordered_set& camperSet, + std::unordered_set& slasherSet); - void parseAndDumpCatalog(std::string& data); + std::string getRandomItem(); + int getRandomQuantity(); + + void generateBloodweb(nlohmann::json& data); void modifyCharacterData(nlohmann::json& js); + void onGetAll(std::string& body); + void onInventoryAll(std::string& body); + void onBloodweb(std::string& body, std::string& respHeaders); + void serverResponseHandler(const std::string& url, std::string& body, std::string& respHeaders); void clientRequestHandler(std::string& url, const std::string& body, std::string& reqHeaders); @@ -33,11 +44,14 @@ class Spoofer std::unordered_set _camperAddonIds; std::unordered_set _slasherAddonIds; - std::unordered_set _slasherPerkIds; std::unordered_set _camperPerkIds; + std::unordered_set _slasherPerkIds; std::unordered_set _catalogOutfitIds; std::unordered_set _catalogItemIds; - std::string _lastBloodWebChar = "Ace"; + std::unordered_set _unownedCharacters; + + std::string _lastBloodWebChar = ""; + std::mutex _mtx; };