#include "spoofing.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #define PLACEHOLDER_ITEMID "Anniversary2025Offering" 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(); loadConfig(); } 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](const std::string& url, std::string& body, std::string& reqHeaders) { this->clientRequestHandler(url, body, reqHeaders); }); } void Spoofer::loadData() { Log::info("Loading data"); /* stackables */ static auto loadData = [this](const char* fileName, std::unordered_set* camperSet, std::unordered_set* slasherSet, std::unordered_set* globalSet, const char* errorMsg) { std::ifstream file(utils::getExePath() + fileName); if (file.is_open()) { std::stringstream buff; buff << file.rdbuf(); if (!parseStackable(buff.str(), camperSet, slasherSet, globalSet)) return Log::error("Failed to parse {} - {}", fileName, errorMsg); else Log::verbose("Loaded {}", fileName); } else Log::warning("Missing file: {} - {}", fileName, errorMsg); }; loadData("items.json", &_camperItemIds, 0, 0, "Items wont be added"); loadData("offerings.json", &_camperOfferingIds, &_slasherOfferingIds, &_globalOfferingIds, "Offerings won't be added"); loadData("addons.json", &_camperAddonIds, &_slasherAddonIds, 0, "Offerings won't be added"); loadData("perks.json", &_camperPerkIds, &_slasherPerkIds, 0, "Offerings won't be added"); /* catalog items */ std::ifstream customizationsFile(utils::getExePath() + "customizations.json"); if (customizationsFile.is_open()) { std::stringstream buff; buff << customizationsFile.rdbuf(); if (!parseCustomizations(buff.str())) Log::warning("Failed to parse customizations.json - Outfits and character items won't be added"); else Log::verbose("Loaded customizations.json"); } /* characters */ std::ifstream characterFile(utils::getExePath() + "characters.json"); if (characterFile.is_open()) { std::stringstream buff; buff << characterFile.rdbuf(); json doc = json::parse(buff.str(), nullptr, false); if (doc.is_discarded()) Log::error("Failed to parse characters.json"); else { for (const auto& character : doc) { if (character.is_string()) _characterList.insert(character.get()); } } } /* dlcs */ std::ifstream dlcFile(utils::getExePath() + "dlcs.json"); if (dlcFile.is_open()) { std::stringstream buff; buff << dlcFile.rdbuf(); json doc = json::parse(buff.str(), nullptr, false); if (doc.is_discarded()) Log::error("Failed to parse dlcs.json"); else { for (const auto& dlc : doc) { if (dlc.contains("grdk")) _dlcListGRDK.insert(dlc["grdk"].get()); if (dlc.contains("egs")) _dlcListEGS.insert(dlc["egs"].get()); } } } Log::verbose("Finished loading data"); Log::verbose("Items - {} camper", _camperItemIds.size()); Log::verbose("Offerings - {} camper | {} slasher | {} global", _camperOfferingIds.size(), _slasherOfferingIds.size(), _globalOfferingIds.size()); Log::verbose("Addons - {} camper | {} slasher ", _camperAddonIds.size(), _slasherAddonIds.size()); Log::verbose("Perks - {} camper | {} slasher ", _camperPerkIds.size(), _slasherPerkIds.size()); Log::verbose("Catalog - {} outfits | {} items", _catalogOutfitIds.size(), _catalogItemIds.size()); Log::verbose("Characters - {}", _characterList.size()); Log::verbose("DLCs - GRDK {} - EGS: {}", _dlcListGRDK.size(), _dlcListEGS.size()); } void Spoofer::loadConfig() { std::string configPath = utils::getExePath() + "config.json"; std::ifstream configFile(configPath); if (configFile.is_open()) { try { json configJson = json::parse(configFile); _config.spoofCharacterOwnership = configJson.value("spoofCharacterOwnership", _config.spoofCharacterOwnership); _config.spoofInventory = configJson.value("spoofInventory", _config.spoofInventory); _config.spoofCustomization = configJson.value("spoofCustomization", _config.spoofCustomization); _config.accessKey = configJson.value("accessKey", _config.accessKey); _config.keyId = configJson.value("keyId", _config.keyId); } catch (...) { Log::error("Failed to parse config.json, using defaults"); } } else { Log::info("config.json not found, using default settings"); json defaultConfig = {{"spoofCharacterOwnership", _config.spoofCharacterOwnership}, {"spoofInventory", _config.spoofInventory}, {"spoofCustomization", _config.spoofCustomization}, {"accessKey", _config.accessKey}, {"keyId", _config.keyId}}; std::ofstream out(configPath); out << defaultConfig.dump(4); } Log::info("Loaded config: Ownership={}, Inventory={}, Customization={}, AccessKey={}, keyId={}", _config.spoofCharacterOwnership, _config.spoofInventory, _config.spoofCustomization, _config.accessKey, _config.keyId); } /* data parsing */ bool Spoofer::parseStackable(std::string data, std::unordered_set* camperSet, std::unordered_set* slasherSet, std::unordered_set* globalSet) { 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); }; if (camperSet) populate("Campers", camperSet); if (slasherSet) populate("Slashers", slasherSet); if (globalSet) populate("All", globalSet); return true; } bool Spoofer::parseCustomizations(std::string data) { json doc = json::parse(data, nullptr, false); if (doc.is_discarded()) { Log::error("JSON parse error @ parseCustomizations"); return false; } if (doc.contains("Items")) { for (const auto& item : doc["Items"]) { if (item.is_string()) _catalogItemIds.insert(item.get()); } } if (doc.contains("Outfits")) { for (const auto& outfit : doc["Outfits"]) { if (outfit.is_string()) _catalogOutfitIds.insert(outfit.get()); } } return true; } /* misc functions */ std::string Spoofer::getRandomItem() { std::vector*> allSets = { &_camperItemIds, &_camperOfferingIds, &_slasherOfferingIds, &_camperAddonIds, &_slasherAddonIds, &_camperPerkIds, &_slasherPerkIds}; std::vector*> validSets; for (auto* s : allSets) if (!s->empty()) validSets.push_back(s); if (validSets.empty()) return PLACEHOLDER_ITEMID; 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"]; if (_config.spoofCharacterOwnership) { bool needsSpoofing = false; if (js.contains("isEntitled") && js["isEntitled"] == 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"]); } } /* item spoofing */ if (_config.spoofInventory) { bool slasher = isSlasher(js["characterName"]); 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(_globalOfferingIds.begin(), _globalOfferingIds.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); } appendItems(_globalOfferingIds, false); } } #ifdef _DEBUG Log::verbose("Spoofed data for character {}", name); #endif } void Spoofer::onGetAllClient(std::string& body) { if (!_config.spoofCharacterOwnership) return; json doc = json::parse(body, nullptr, false); if (doc.is_discarded()) return Log::error("JSON parse error for dbd-inventories/all (client)"); if (doc.contains("ownedCharacters") && doc["ownedCharacters"].is_array()) { auto& jsonList = doc["ownedCharacters"]; for (const std::string& charName : _characterList) { if (charName == "K25") continue; if (std::find(jsonList.begin(), jsonList.end(), charName) == jsonList.end()) jsonList.push_back(charName); } body = doc.dump(); } } /* 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; if (_config.spoofInventory) { categories.push_back({_camperPerkIds, 3}); categories.push_back({_slasherPerkIds, 3}); categories.push_back({_camperOfferingIds, -1}); categories.push_back({_slasherOfferingIds, -1}); categories.push_back({_camperItemIds, -1}); categories.push_back({_camperAddonIds, -1}); categories.push_back({_slasherAddonIds, -1}); } if (_config.spoofCustomization) { categories.push_back({_catalogOutfitIds, 1}); categories.push_back({_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(); #ifdef _DEBUG Log::verbose("Spoofed message list"); #endif } void Spoofer::onBloodwebClient(std::string& body) { 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"]; } 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"); /* return fake bloodweb data */ if (_config.spoofCharacterOwnership) { 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; } } /* bloodweb fixup for perks (if all perks are unlocked, the game will interpret bloodwebs with perks as invalid so perks will be replaced with PLACEHOLDER_ITEMID) */ if (_config.spoofInventory) { if (doc.contains("bloodWebData") && doc["bloodWebData"].contains("ringData")) { for (auto& ring : doc["bloodWebData"]["ringData"]) { if (!ring.contains("nodeData")) continue; for (auto& node : ring["nodeData"]) { if (!node.contains("contentId")) continue; std::string contentId = node["contentId"]; if (_camperPerkIds.contains(contentId) || _slasherPerkIds.contains(contentId)) node["contentId"] = PLACEHOLDER_ITEMID; } } } } /* prevent bloodweb reqs from overriding inventory values */ modifyCharacterData(doc); body = doc.dump(); #ifdef _DEBUG Log::verbose("Spoofed bloodweb items for owned character"); #endif } void Spoofer::onUpdateEntitlements(const std::string& url, std::string& body) { if (!_config.spoofCharacterOwnership) return; json js = json::parse(body, nullptr, false); if (js.is_discarded()) return Log::error("JSON parse error for get-update-entitlements"); if (js.contains("entitlements")) { auto& jsonList = js["entitlements"]; std::unordered_set* list = nullptr; if (url.starts_with("https://grdk.live.bhvrdbd.com/")) list = &_dlcListGRDK; else if (url.starts_with("https://egs.live.bhvrdbd.com/")) list = &_dlcListEGS; else return Log::error("Invalid url?"); if (list == nullptr) return; for (const std::string& dlcId : *list) { if (std::find(jsonList.begin(), jsonList.end(), dlcId) == jsonList.end()) jsonList.push_back(dlcId); } } body = js.dump(); } void Spoofer::onUpdateEntitlementsClient(const std::string& url, std::string& body) { if (!_config.spoofCharacterOwnership) return; json js = json::parse(body, nullptr, false); if (js.is_discarded()) return Log::error("JSON parse error for get-update-entitlements"); if (js.contains("clientEntitlementIds")) { auto& jsonList = js["clientEntitlementIds"]; std::unordered_set* list = nullptr; if (url.starts_with("https://grdk.live.bhvrdbd.com/")) list = &_dlcListGRDK; else if (url.starts_with("https://egs.live.bhvrdbd.com/")) list = &_dlcListEGS; else return Log::error("Invalid url?"); if (list == nullptr) return; for (const std::string& dlcId : *list) { if (std::find(jsonList.begin(), jsonList.end(), dlcId) == jsonList.end()) jsonList.push_back(dlcId); } } body = js.dump(); } /* 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.ends_with("api/v1/dbd-inventories/all")) return onInventoryAll(body); if (url.find("api/v1/messages/listV2") != std::string::npos) return onMessageList(body); if (url.ends_with("api/v1/dbd-character-data/get-all")) return onGetAll(body); if (url.ends_with("api/v1/dbd-character-data/bloodweb") || url.ends_with("api/v1/dbd-character-data/bulk-spending-bloodweb")) return onBloodweb(body, respHeaders); if (url.ends_with("api/v1/owned-products/get-update-entitlements")) return onUpdateEntitlements(url, body); } void Spoofer::clientRequestHandler(const std::string& url, std::string& body, std::string& /*reqHeaders*/) { if (url.find("bhvrdbd.com") == std::string::npos) return; std::lock_guard lock(_mtx); if (url.ends_with("api/v1/dbd-character-data/get-all")) return onGetAllClient(body); if (url.ends_with("api/v1/dbd-character-data/bloodweb") || url.ends_with("api/v1/dbd-character-data/bulk-spending-bloodweb")) return onBloodwebClient(body); if (url.ends_with("api/v1/owned-products/get-update-entitlements")) return onUpdateEntitlementsClient(url, body); }