Core bpay fixes
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
@@ -720,7 +721,7 @@ bool BattlePayMgr::AssembleCatalog(std::vector<uint8>& outBlob, std::unordered_m
|
||||
outRouting.clear();
|
||||
|
||||
if (_templateBlob.empty())
|
||||
return false;
|
||||
return BuildCatalogFromDB(outBlob, outRouting);
|
||||
|
||||
// The writer now decodes ALL FOUR arrays (94 products, 116 deliverables, 21 groups, 97 shop
|
||||
// entries) with a byte-exact round trip, instead of only the first 9 "simple shape" records.
|
||||
@@ -810,33 +811,46 @@ bool BattlePayMgr::AssembleCatalog(std::vector<uint8>& outBlob, std::unordered_m
|
||||
rec.DisplayInfo->Flags = displayInfoFlags;
|
||||
};
|
||||
|
||||
// Build a name->shop_product index for automatic matching of retail catalog products to our
|
||||
// gold-priced shop_product rows. This lets the template's display art (FileDataID, ModelSceneID,
|
||||
// DisplayCards) survive while we patch prices and deliverables.
|
||||
std::unordered_map<std::string, ShopProduct const*> nameIndex;
|
||||
for (auto const& [id, product] : _products)
|
||||
if (product.Enabled && !product.Name.empty())
|
||||
nameIndex[product.Name] = &product;
|
||||
|
||||
// Track which shop_products have been assigned so we can emit unpinned ones as extras.
|
||||
std::unordered_set<uint32> assignedProducts;
|
||||
|
||||
size_t candIdx = 0;
|
||||
for (size_t slot = 0; slot < catalog.Products.size(); ++slot)
|
||||
{
|
||||
// The id the client actually purchases by. The previous code read record+81, which is really
|
||||
// DisplayInfo.fileDataID - the card ARTWORK id - so the routing map was built from FileDataIDs
|
||||
// and never resolved. Purchases only worked because GetProductByAdvertisedId falls through to
|
||||
// GetProduct(id).
|
||||
uint32 const slotProductId = catalog.Products[slot].ProductID;
|
||||
ShopProduct const* assigned = nullptr;
|
||||
|
||||
// Priority 1: explicit slot override
|
||||
auto ovr = _slotOverrides.find(uint8(slot));
|
||||
if (ovr != _slotOverrides.end())
|
||||
{
|
||||
if (ovr->second != 0) // pinned; 0 = forced placeholder
|
||||
assigned = GetProduct(ovr->second);
|
||||
}
|
||||
// NOTE: no automatic slot filling any more. The catalog we ship already describes 94 real
|
||||
// products with our own gold prices patched in, and shop_product is keyed on those same
|
||||
// wire productIDs, so every card resolves on its own. Auto-assigning candidates to slots
|
||||
// here would stamp one product's name and price onto a different product's card - harmless
|
||||
// when only 9 rows existed, actively wrong now there are 66. Explicit shop_slot_override
|
||||
// pins still work above for anyone who wants to re-badge a specific slot.
|
||||
|
||||
// Priority 2: match by DisplayInfo name against shop_product names.
|
||||
// This picks up the remaining ~57 shop_products that have no slot override but whose
|
||||
// names appear in the retail catalog (e.g. "Anima Wyrmling", "Lil' Ursoc", etc.).
|
||||
if (!assigned && catalog.Products[slot].DisplayInfo)
|
||||
{
|
||||
auto it = nameIndex.find(catalog.Products[slot].DisplayInfo->Name1);
|
||||
if (it != nameIndex.end())
|
||||
assigned = it->second;
|
||||
}
|
||||
|
||||
if (assigned)
|
||||
{
|
||||
reskin(catalog.Products[slot], *assigned);
|
||||
outRouting[slotProductId] = assigned->ProductID;
|
||||
assignedProducts.insert(assigned->ProductID);
|
||||
if (report)
|
||||
report->append(Trinity::StringFormat(" slot {}: [{}] '{}' -> product {} (price {}, {}{})\n",
|
||||
slot, slotProductId, assigned->Name, assigned->ProductID, assigned->Price,
|
||||
@@ -844,22 +858,126 @@ bool BattlePayMgr::AssembleCatalog(std::vector<uint8>& outBlob, std::unordered_m
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not pinned. Leave the shipped record's price alone - the blob already describes a real
|
||||
// product with our gold price patched in, and shop_product is keyed on the same wire
|
||||
// productID. Only give it a DisplayInfo when it has none, so the confirmation dialog can
|
||||
// render a name instead of erroring on nil.
|
||||
if (!catalog.Products[slot].DisplayInfo)
|
||||
{
|
||||
if (ShopProduct const* known = GetProduct(slotProductId))
|
||||
catalog.Products[slot].DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(known->Name, known->Description);
|
||||
else
|
||||
// Not pinned and no name match. Hide this retail-only card by clearing its
|
||||
// DisplayInfo so the client treats it as an empty slot. This prevents the user
|
||||
// from clicking a $-priced product that cannot be delivered.
|
||||
catalog.Products[slot].DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(placeholderName, std::string());
|
||||
}
|
||||
catalog.Products[slot].Flags = 0; // clear buyableHere bits so Buy button is greyed out
|
||||
if (report)
|
||||
report->append(Trinity::StringFormat(" slot {}: [{}] <placeholder - not purchasable>\n", slot, slotProductId));
|
||||
report->append(Trinity::StringFormat(" slot {}: [{}] <hidden - not routable>\n", slot, slotProductId));
|
||||
}
|
||||
}
|
||||
|
||||
// Append shop_products that did NOT match any catalog slot as new entries at the end.
|
||||
// These are products in the DB that have no retail template counterpart.
|
||||
for (auto const& [id, product] : _products)
|
||||
{
|
||||
if (!product.Enabled || assignedProducts.count(product.ProductID))
|
||||
continue;
|
||||
|
||||
BattlePayCatalogProduct catalogProd;
|
||||
catalogProd.ProductID = product.ProductID;
|
||||
catalogProd.Flags = 10;
|
||||
catalogProd.Type = 0;
|
||||
catalogProd.Eligibility = 0;
|
||||
catalogProd.PmtProductID = product.ProductID;
|
||||
catalogProd.RequiredDeliverableID = 0;
|
||||
catalogProd.PadBits = 0;
|
||||
catalogProd.BundledProductIDs = {};
|
||||
|
||||
uint64 displayPrice;
|
||||
if (product.HasDisplayPrice)
|
||||
displayPrice = product.DisplayPrice;
|
||||
else if (product.Currency == 1)
|
||||
displayPrice = (product.Price / 10000) * 100000;
|
||||
else
|
||||
displayPrice = 0;
|
||||
|
||||
catalogProd.NormalPriceFixedPoint = displayPrice;
|
||||
catalogProd.CurrentPriceFixedPoint = displayPrice;
|
||||
catalogProd.DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(product.Name, product.Description);
|
||||
|
||||
// Build deliverables for this product
|
||||
std::vector<uint32> delivIds;
|
||||
for (size_t i = 0; i < product.Deliverables.size(); ++i)
|
||||
{
|
||||
uint32 delivId = uint32(catalog.Deliverables.size()) + 1;
|
||||
ShopDeliverable const& dv = product.Deliverables[i];
|
||||
BattlePayDeliverable catalogDv;
|
||||
catalogDv.DeliverableID = delivId;
|
||||
catalogDv.Name = product.Name;
|
||||
catalogDv.AlreadyOwns = 0;
|
||||
catalogDv.HasPetResult = 0;
|
||||
catalogDv.PetResult = 0;
|
||||
catalogDv.PadBits = 0;
|
||||
|
||||
switch (dv.Type)
|
||||
{
|
||||
case 1: catalogDv.Type = 14; catalogDv.ItemID = dv.Id; catalogDv.Quantity = dv.Count; break;
|
||||
case 2: catalogDv.Type = 3; catalogDv.MountSpellID = dv.Id; break;
|
||||
case 3: catalogDv.Type = 4; catalogDv.Quantity = dv.Count; break;
|
||||
case 5: catalogDv.Type = dv.Id;
|
||||
if (dv.Id == 1) catalogDv.BoostID = 11;
|
||||
break;
|
||||
default: catalogDv.Type = dv.Type; catalogDv.ItemID = dv.Id; catalogDv.Quantity = dv.Count; break;
|
||||
}
|
||||
catalog.Deliverables.push_back(catalogDv);
|
||||
delivIds.push_back(delivId);
|
||||
}
|
||||
if (delivIds.empty())
|
||||
{
|
||||
uint32 delivId = uint32(catalog.Deliverables.size()) + 1;
|
||||
BattlePayDeliverable catalogDv;
|
||||
catalogDv.DeliverableID = delivId;
|
||||
catalogDv.Type = 0;
|
||||
catalogDv.ItemID = 0;
|
||||
catalogDv.Quantity = 0;
|
||||
catalogDv.Name = product.Name;
|
||||
catalog.Deliverables.push_back(catalogDv);
|
||||
delivIds.push_back(delivId);
|
||||
}
|
||||
|
||||
catalogProd.DeliverableIDs = std::move(delivIds);
|
||||
|
||||
catalog.Products.push_back(catalogProd);
|
||||
|
||||
// Add a group if needed
|
||||
bool groupExists = false;
|
||||
for (auto const& g : catalog.Groups)
|
||||
if (g.GroupID == product.GroupId) { groupExists = true; break; }
|
||||
if (!groupExists)
|
||||
{
|
||||
BattlePayProductGroup group;
|
||||
group.GroupID = product.GroupId;
|
||||
group.Ordering = product.GroupId;
|
||||
group.Flags = 0;
|
||||
group.ParentGroupID = 0;
|
||||
group.DisplayType = 0;
|
||||
group.IconFileDataID = 0;
|
||||
group.Name = "Category " + std::to_string(product.GroupId);
|
||||
group.DisabledDescription = "";
|
||||
catalog.Groups.push_back(group);
|
||||
}
|
||||
|
||||
// Add a shop entry
|
||||
BattlePayShopEntry entry;
|
||||
entry.EntryID = uint32(catalog.Entries.size()) + 1;
|
||||
entry.GroupID = product.GroupId;
|
||||
entry.ProductID = product.ProductID;
|
||||
entry.Ordering = product.Ordering;
|
||||
entry.Flags = 0;
|
||||
entry.BannerType = 0;
|
||||
entry.DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(product.Name, product.Description);
|
||||
entry.PadBits = 0;
|
||||
catalog.Entries.push_back(entry);
|
||||
|
||||
outRouting[product.ProductID] = product.ProductID;
|
||||
|
||||
if (report)
|
||||
report->append(Trinity::StringFormat(" slot {}: [NEW] '{}' -> product {} (price {})\n",
|
||||
catalog.Products.size() - 1, product.Name, product.ProductID, displayPrice));
|
||||
}
|
||||
|
||||
if (report && candIdx < candidates.size())
|
||||
report->append(Trinity::StringFormat(" OVERFLOW: {} enabled product(s) could not be shown (only {} slots).\n",
|
||||
candidates.size() - candIdx, catalog.Products.size()));
|
||||
@@ -1014,3 +1132,225 @@ std::string BattlePayMgr::BuildStatusReport() const
|
||||
report.append(Trinity::StringFormat("Assembled blob: {} bytes, generation {}.", blob.size(), _catalogGeneration));
|
||||
return report;
|
||||
}
|
||||
|
||||
bool BattlePayMgr::BuildCatalogFromDB(std::vector<uint8>& outBlob, std::unordered_map<uint32, uint32>& outRouting) const
|
||||
{
|
||||
BattlePayCatalog catalog;
|
||||
catalog.Result = 0;
|
||||
catalog.CurrencyID = 0;
|
||||
|
||||
std::string const placeholderName = std::string(sConfigMgr->GetStringDefault("Shop.PlaceholderName", "Currently unavailable"));
|
||||
|
||||
// Collect all unique groupIds
|
||||
std::set<uint32> groupIds;
|
||||
for (auto const& [id, product] : _products)
|
||||
if (product.Enabled)
|
||||
groupIds.insert(product.GroupId);
|
||||
|
||||
// Build deliverable map: productId -> vector of deliverable IDs in the catalog
|
||||
std::unordered_map<uint32, std::vector<uint32>> productDeliverableIds;
|
||||
|
||||
// Build deliverables ? mirror the type mapping in BuildDeliverable (BattlePayHandler.cpp:717).
|
||||
// DB types: 1=item, 2=mount/spell, 3=WoW Token, 5=service.
|
||||
// Catalog wire types: 14=Item/Toy, 3=Mount, 4=WowToken, service-id passthrough for type 5.
|
||||
uint32 deliverableId = 1;
|
||||
for (auto const& [id, product] : _products)
|
||||
{
|
||||
for (size_t i = 0; i < product.Deliverables.size(); ++i)
|
||||
{
|
||||
ShopDeliverable const& dv = product.Deliverables[i];
|
||||
BattlePayDeliverable catalogDv;
|
||||
catalogDv.DeliverableID = deliverableId;
|
||||
catalogDv.Name = product.Name;
|
||||
catalogDv.AlreadyOwns = 0;
|
||||
catalogDv.HasPetResult = 0;
|
||||
catalogDv.PetResult = 0;
|
||||
catalogDv.PadBits = 0;
|
||||
|
||||
switch (dv.Type)
|
||||
{
|
||||
case 1: // item / toy
|
||||
catalogDv.Type = 14;
|
||||
catalogDv.ItemID = dv.Id;
|
||||
catalogDv.Quantity = dv.Count;
|
||||
break;
|
||||
case 2: // mount / toy spell
|
||||
catalogDv.Type = 3;
|
||||
catalogDv.MountSpellID = dv.Id;
|
||||
break;
|
||||
case 3: // WoW Token
|
||||
catalogDv.Type = 4;
|
||||
catalogDv.Quantity = dv.Count;
|
||||
break;
|
||||
case 5: // service ? id is the catalog service type (1=Boost, 5=NameChange, etc.)
|
||||
catalogDv.Type = dv.Id;
|
||||
if (dv.Id == 1 /* SHOP_SERVICE_CHARACTER_BOOST */)
|
||||
catalogDv.BoostID = 11;
|
||||
break;
|
||||
default:
|
||||
catalogDv.Type = dv.Type;
|
||||
catalogDv.ItemID = dv.Id;
|
||||
catalogDv.Quantity = dv.Count;
|
||||
break;
|
||||
}
|
||||
|
||||
catalog.Deliverables.push_back(catalogDv);
|
||||
productDeliverableIds[id].push_back(deliverableId);
|
||||
++deliverableId;
|
||||
}
|
||||
|
||||
// If no deliverables, add an empty one so the product is structurally valid
|
||||
if (product.Deliverables.empty())
|
||||
{
|
||||
BattlePayDeliverable catalogDv;
|
||||
catalogDv.DeliverableID = deliverableId;
|
||||
catalogDv.Type = 0;
|
||||
catalogDv.ItemID = 0;
|
||||
catalogDv.Quantity = 0;
|
||||
catalogDv.Name = product.Name;
|
||||
catalog.Deliverables.push_back(catalogDv);
|
||||
productDeliverableIds[id].push_back(deliverableId);
|
||||
++deliverableId;
|
||||
}
|
||||
}
|
||||
|
||||
// Build groups
|
||||
for (uint32 gid : groupIds)
|
||||
{
|
||||
BattlePayProductGroup group;
|
||||
group.GroupID = gid;
|
||||
group.Ordering = gid;
|
||||
group.Flags = 0;
|
||||
group.ParentGroupID = 0;
|
||||
group.DisplayType = 0;
|
||||
group.IconFileDataID = 0;
|
||||
|
||||
// Name the groups based on typical WoW store categories
|
||||
switch (gid)
|
||||
{
|
||||
case 0: group.Name = "Services"; break;
|
||||
case 13: group.Name = "Pets"; break;
|
||||
case 15: group.Name = "Mounts"; break;
|
||||
case 30: group.Name = "WoW Token"; break;
|
||||
case 138: group.Name = "Toys"; break;
|
||||
case 139: group.Name = "Transmog"; break;
|
||||
case 158: group.Name = "Subscriptions"; break;
|
||||
case 195: group.Name = "Hunter Pets"; break;
|
||||
case 197: group.Name = "Vendor Mounts"; break;
|
||||
case 209: group.Name = "Expansions"; break;
|
||||
case 225: group.Name = "Bundles"; break;
|
||||
default: group.Name = "Category " + std::to_string(gid); break;
|
||||
}
|
||||
|
||||
group.DisabledDescription = "";
|
||||
catalog.Groups.push_back(group);
|
||||
}
|
||||
|
||||
// Build products + shop entries
|
||||
uint32 entryId = 1;
|
||||
for (auto const& [id, product] : _products)
|
||||
{
|
||||
if (!product.Enabled)
|
||||
continue;
|
||||
|
||||
BattlePayCatalogProduct catalogProd;
|
||||
catalogProd.ProductID = product.ProductID;
|
||||
catalogProd.Flags = 10; // bits 1+3 drive buyableHere
|
||||
catalogProd.Type = 0;
|
||||
catalogProd.Eligibility = 0;
|
||||
catalogProd.PmtProductID = product.ProductID;
|
||||
catalogProd.RequiredDeliverableID = 0;
|
||||
catalogProd.DeliverableIDs = productDeliverableIds[id];
|
||||
catalogProd.BundledProductIDs = {};
|
||||
catalogProd.PadBits = 0;
|
||||
|
||||
// Price
|
||||
uint64 displayPrice;
|
||||
if (product.HasDisplayPrice)
|
||||
displayPrice = product.DisplayPrice;
|
||||
else if (product.Currency == 1)
|
||||
displayPrice = (product.Price / 10000) * 100000;
|
||||
else
|
||||
displayPrice = 0;
|
||||
|
||||
catalogProd.NormalPriceFixedPoint = displayPrice;
|
||||
catalogProd.CurrentPriceFixedPoint = displayPrice;
|
||||
|
||||
// DisplayInfo
|
||||
catalogProd.DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(product.Name, product.Description);
|
||||
|
||||
catalog.Products.push_back(catalogProd);
|
||||
|
||||
// ShopEntry: one tile per product
|
||||
BattlePayShopEntry entry;
|
||||
entry.EntryID = entryId++;
|
||||
entry.GroupID = product.GroupId;
|
||||
entry.ProductID = product.ProductID;
|
||||
entry.Ordering = product.Ordering;
|
||||
entry.Flags = 0;
|
||||
entry.BannerType = 0;
|
||||
entry.DisplayInfo = BattlePayCatalogWriter::MakeDisplayInfo(product.Name, product.Description);
|
||||
entry.PadBits = 0;
|
||||
|
||||
catalog.Entries.push_back(entry);
|
||||
|
||||
// Every product routes to itself
|
||||
outRouting[product.ProductID] = product.ProductID;
|
||||
}
|
||||
|
||||
outBlob = BattlePayCatalogWriter::Serialize(catalog);
|
||||
TC_LOG_INFO("server.loading", "BattlePay: built catalog from DB ({} bytes, {} products, {} deliverables, {} groups, {} entries).",
|
||||
outBlob.size(), catalog.Products.size(), catalog.Deliverables.size(),
|
||||
catalog.Groups.size(), catalog.Entries.size());
|
||||
|
||||
// Hex dump first 64 bytes for format verification
|
||||
if (outBlob.size() >= 64)
|
||||
{
|
||||
std::string hex;
|
||||
hex.reserve(192);
|
||||
for (size_t i = 0; i < 64; ++i)
|
||||
{
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%02x ", outBlob[i]);
|
||||
hex += buf;
|
||||
if (i == 15 || i == 31 || i == 47)
|
||||
hex += "| ";
|
||||
}
|
||||
TC_LOG_INFO("server.loading", "BattlePay: blob hex (first 64): {}", hex);
|
||||
}
|
||||
|
||||
// Self-check: Parse the blob back and re-serialize to verify round-trip integrity
|
||||
{
|
||||
BattlePayCatalog reparsed;
|
||||
std::string parseError;
|
||||
if (!BattlePayCatalogWriter::Parse(outBlob, reparsed, &parseError))
|
||||
{
|
||||
TC_LOG_ERROR("server.loading", "BattlePay: SelfCheck FAILED - parse error: {}", parseError);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<uint8> reSerialized = BattlePayCatalogWriter::Serialize(reparsed);
|
||||
if (reSerialized != outBlob)
|
||||
{
|
||||
TC_LOG_ERROR("server.loading", "BattlePay: SelfCheck FAILED - round-trip mismatch! Original {} bytes, re-serialized {} bytes.",
|
||||
outBlob.size(), reSerialized.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
TC_LOG_INFO("server.loading", "BattlePay: SelfCheck OK - round-trip matches ({} bytes, {} products, {} deliverables, {} groups, {} entries).",
|
||||
reSerialized.size(), reparsed.Products.size(), reparsed.Deliverables.size(),
|
||||
reparsed.Groups.size(), reparsed.Entries.size());
|
||||
|
||||
// Log first product details
|
||||
if (!reparsed.Products.empty())
|
||||
{
|
||||
auto const& p = reparsed.Products.front();
|
||||
TC_LOG_INFO("server.loading", "BattlePay: First product: ID={}, Type={}, Flags={}, Eligibility={}, Price={}, CurrencyID={}, DeliverableIDs={}",
|
||||
p.ProductID, p.Type, p.Flags, p.Eligibility, p.NormalPriceFixedPoint,
|
||||
reparsed.CurrencyID, p.DeliverableIDs.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -273,6 +273,9 @@ private:
|
||||
bool AssembleCatalog(std::vector<uint8>& outBlob, std::unordered_map<uint32, uint32>& outRouting,
|
||||
std::string* report) const;
|
||||
|
||||
// Builds a complete catalog blob from DB data alone, without the retail template blob.
|
||||
bool BuildCatalogFromDB(std::vector<uint8>& outBlob, std::unordered_map<uint32, uint32>& outRouting) const;
|
||||
|
||||
std::vector<uint8> _templateBlob;
|
||||
std::vector<uint8> _productListBlob;
|
||||
std::vector<uint8> _distributionListBlob;
|
||||
|
||||
@@ -783,7 +783,14 @@ void WorldSession::SendBattlePayDistributionList()
|
||||
}
|
||||
|
||||
if (!sBattlePayMgr->HasDistributionList())
|
||||
{
|
||||
// Build an empty distribution list to unblock the client's shop panel loading state.
|
||||
WorldPackets::BattlePay::GetDistributionListResponse response;
|
||||
response.BuildFromObjects = true;
|
||||
response.Result = RESULT_OK;
|
||||
SendPacket(response.Write());
|
||||
return;
|
||||
}
|
||||
|
||||
WorldPackets::BattlePay::GetDistributionListResponse response;
|
||||
response.RawData = &sBattlePayMgr->GetDistributionListBlob();
|
||||
|
||||
Reference in New Issue
Block a user