#include "spoofing.h" #include "utils.h" #include #include #include #include #include #include #include #include using json = nlohmann::json; 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) { if (slasherNames.contains(name) || (name.length() == 3 && name[0] == 'K')) return true; return false; } /* setup / init */ void Spoofer::init(Proxy* proxy) { 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); }); proxy->OnClientRequest.addListener([this](std::string& url, const std::string& body, std::string& reqHeaders) { this->clientRequestHandler(url, body, reqHeaders); }); } #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 data"); std::string catalogPath = utils::getExePath() + "catalog.json"; std::ifstream catalogFile(catalogPath); if (catalogFile.is_open()) { 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 - Customizations won't be unlocked"); 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()) { Log::error("Failed to parse catalog"); return false; } std::string outPath = utils::getExePath() + "catalog.json"; std::ofstream file(outPath); if (file.is_open()) { file << data; file.close(); Log::verbose("Raw catalog saved to {}", outPath); } else Log::error("Unable to write to catalog.json"); try { const auto& catalogData = doc.at("data"); 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); }; extractIds("item", _catalogItemIds); extractIds("outfit", _catalogOutfitIds); } catch (const json::exception& e) { 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; } std::string name = js["characterName"]; bool slasher = isSlasher(js["characterName"]); bool needsSpoofing = false; if (js.value("isEntitled", true) == false) { _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; } else if (_unownedCharacters.contains(name)) needsSpoofing = true; /* modifications for unowned characters (spoof level and fake bloodweb) */ if (needsSpoofing) { 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"]); } else { /* ghost node hotfix (untested) */ /* if (js.contains("bloodWebData") && js["bloodWebData"].contains("ringData")) { auto& ringData = js["bloodWebData"]["ringData"]; for (auto& ring : ringData) { 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"; } } } } } }*/ } /* 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()); stackableIds.insert(_camperAddonIds.begin(), _camperAddonIds.end()); stackableIds.insert(_slasherAddonIds.begin(), _slasherAddonIds.end()); stackableIds.insert(_slasherOfferingIds.begin(), _slasherOfferingIds.end()); 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 (stackableIds.contains(itemId)) item["quantity"] = getRandomQuantity(); } } 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) { appendItems(_camperItemIds, false); appendItems(_camperAddonIds, false); appendItems(_camperOfferingIds, false); appendItems(_camperPerkIds, true); } else { 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::onMessageList(std::string& body) { json doc = json::parse(body, nullptr, false); if (doc.is_discarded()) return Log::error("JSON parse error for dbd-messages/listV2"); 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 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(); Log::verbose("Spoofed message list"); } 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(); #ifdef _DEBUG Log::verbose("Spoofed bloodweb request for unowned character."); #endif return; } modifyCharacterData(doc); body = doc.dump(); #ifdef _DEBUG Log::verbose("Spoofed bloodweb items for owned character"); #endif } /* 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 Log::verbose("BHVR api res @ {}", url); #endif 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/messages/listV2") != std::string::npos) return onMessageList(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) { 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"]; } }