Web-Shop2
This commit is contained in:
@@ -445,6 +445,10 @@ void WorldSession::SendBattlePayDeliveryNotifications(ShopProduct const& product
|
||||
SendPacket(notification.Write());
|
||||
}
|
||||
|
||||
// Shop 2 learns ownership from SMSG_CATALOG_SHOP_OBTAIN_LICENSE (one license id per packet).
|
||||
// The product id is the license id we answer in CATALOG_SHOP_LICENSE_DATA.
|
||||
SendCatalogShopObtainLicense(product.ProductID);
|
||||
|
||||
WorldPackets::BattlePay::DeliveryEnded ended;
|
||||
ended.PurchaseID = purchaseID;
|
||||
ended.Products.emplace_back().ProductID = product.ProductID;
|
||||
@@ -655,29 +659,73 @@ void WorldSession::HandleBattlePayOpenCheckout(WorldPackets::BattlePay::OpenChec
|
||||
}
|
||||
}
|
||||
|
||||
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. The client sends this mid-checkout carrying its own
|
||||
// license / game data (variable body: 876/140/52/36/24 bytes in the 12.1.0.69382 capture). Retail answers
|
||||
// on SMSG opcode 0x4202C1 - which our opcode map currently mis-names SMSG_VAS_GET_QUEUE_MINUTES_RESPONSE
|
||||
// (the sniff's 0x4502C1 is that opcode plus the sniff's fixed 0x030000 server-opcode bias) - with an
|
||||
// 11465/1025/423/403/40-byte payload.
|
||||
//
|
||||
// That response is a nested reflection bitstream whose FIELD LAYOUT is not recovered - the capture gives
|
||||
// only the sizes, not the member types. Emitting a guessed layout on that opcode would risk the client's
|
||||
// reflection parser rejecting it and disconnecting, which is exactly the "do not invent wire" hazard. So
|
||||
// this handler does the honest, safe thing: it makes the opcode an EXPECTED, real handler (no more
|
||||
// STATUS_UNHANDLED / Handle_NULL), logs the body size so a future capture can be matched to a variant, and
|
||||
// sends nothing. The 69382 capture shows the checkout proceeds (the client drives its own web overlay from
|
||||
// OPEN_CHECKOUT) without the realm answering this, so withholding the un-modeled response does not stall
|
||||
// the client. When the response wire is recovered, this is where it is sent.
|
||||
namespace
|
||||
{
|
||||
// A shop product the client asked about by license id. Our catalog uses the product id as the
|
||||
// license id (see SendCatalogShopObtainLicense). Only an item deliverable has a 49-byte type-14
|
||||
// record that matches the 12.1.0.69933 capture; every other id is reported missing, which is the
|
||||
// form retail itself uses for licenses it has no game data for.
|
||||
bool BuildCatalogShopLicense(uint32 licenseId, WorldPackets::BattlePay::CatalogShopLicenseEntry& entry)
|
||||
{
|
||||
ShopProduct const* product = sBattlePayMgr->GetProduct(licenseId);
|
||||
if (!product)
|
||||
product = sBattlePayMgr->GetProductByAdvertisedId(licenseId);
|
||||
if (!product)
|
||||
return false;
|
||||
|
||||
for (ShopDeliverable const& deliverable : product->Deliverables)
|
||||
{
|
||||
if (deliverable.Type != 1 || !deliverable.Id)
|
||||
continue;
|
||||
|
||||
entry = {};
|
||||
entry.LicenseID = licenseId;
|
||||
entry.Type = 14; // Item/Toy, the catalog deliverable vocabulary
|
||||
entry.PrimaryID = deliverable.Id;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. The 12.1 client sends batches of license ids (255, then
|
||||
// the ones the first answer did not cover). Retail answers on SMSG_CATALOG_SHOP_LICENSE_DATA with the
|
||||
// licenses it can describe and a 5-byte "missing" record for the rest. Answering is what lets the
|
||||
// Shop 2 frame finish resolving product cards; a license we do not sell is reported missing, exactly
|
||||
// as the 36-byte all-missing response in the 69933 capture.
|
||||
void WorldSession::HandleCatalogShopLicenseGameDataRequest(WorldPackets::BattlePay::CatalogShopLicenseGameDataRequest& request)
|
||||
{
|
||||
if (!sWorld->getBoolConfig(CONFIG_SHOP_ENABLED))
|
||||
return;
|
||||
|
||||
TC_LOG_INFO("network", "BattlePay: CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST from {} ({} body bytes). The paired "
|
||||
"response (SMSG 0x4202C1) is an un-modeled reflection payload and is deliberately not answered - see the "
|
||||
"handler note. If the client stalls waiting for it, capture the response bytes and model them here.",
|
||||
GetPlayerInfo(), request.Data.size());
|
||||
WorldPackets::BattlePay::CatalogShopLicenseData response;
|
||||
response.Found.reserve(request.LicenseIDs.size());
|
||||
response.Missing.reserve(request.LicenseIDs.size());
|
||||
|
||||
for (uint32 licenseId : request.LicenseIDs)
|
||||
{
|
||||
WorldPackets::BattlePay::CatalogShopLicenseEntry entry;
|
||||
if (BuildCatalogShopLicense(licenseId, entry))
|
||||
response.Found.push_back(std::move(entry));
|
||||
else
|
||||
response.Missing.push_back(licenseId);
|
||||
}
|
||||
|
||||
TC_LOG_DEBUG("network", "BattlePay: CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST from {} ({} licenses, {} resolved).",
|
||||
GetPlayerInfo(), request.LicenseIDs.size(), response.Found.size());
|
||||
|
||||
SendPacket(response.Write());
|
||||
}
|
||||
|
||||
void WorldSession::SendCatalogShopObtainLicense(uint32 licenseId)
|
||||
{
|
||||
if (!sWorld->getBoolConfig(CONFIG_SHOP_ENABLED) || !licenseId)
|
||||
return;
|
||||
|
||||
WorldPackets::BattlePay::CatalogShopObtainLicense packet;
|
||||
packet.LicenseID = licenseId;
|
||||
SendPacket(packet.Write());
|
||||
}
|
||||
|
||||
namespace
|
||||
@@ -1515,6 +1563,7 @@ void WorldSession::HandleBattlePayGetPurchaseList(WorldPackets::BattlePay::GetPu
|
||||
|
||||
if (result)
|
||||
{
|
||||
std::set<uint32> ownedLicenses;
|
||||
do
|
||||
{
|
||||
Field* fields = result->Fetch();
|
||||
@@ -1526,7 +1575,12 @@ void WorldSession::HandleBattlePayGetPurchaseList(WorldPackets::BattlePay::GetPu
|
||||
rec.BasePrice = fields[4].GetUInt64();
|
||||
rec.UserPrice = fields[5].GetUInt64();
|
||||
rec.TimeCreated = fields[6].GetInt64();
|
||||
if (rec.Status == STATUS_DONE && rec.ProductID)
|
||||
ownedLicenses.insert(rec.ProductID);
|
||||
} while (result->NextRow());
|
||||
|
||||
for (uint32 licenseId : ownedLicenses)
|
||||
SendCatalogShopObtainLicense(licenseId);
|
||||
}
|
||||
|
||||
SendPacket(response.Write());
|
||||
|
||||
@@ -183,13 +183,65 @@ namespace WorldPackets::BattlePay
|
||||
|
||||
void CatalogShopLicenseGameDataRequest::Read()
|
||||
{
|
||||
// The response wire is not yet modeled, so this body is retained for diagnostics only (its size
|
||||
// identifies which of the captured request variants it is). Consume the whole body so the packet
|
||||
// is not reported as under-read.
|
||||
size_t const len = _worldPacket.size();
|
||||
Data.resize(len);
|
||||
if (len)
|
||||
_worldPacket.read(Data.data(), len);
|
||||
uint32 count = 0;
|
||||
_worldPacket >> count;
|
||||
|
||||
// 255 and 65 are the batches in the 69933 capture. Anything larger than the remaining body,
|
||||
// or an absurd count, is not a request we can answer honestly.
|
||||
constexpr uint32 MAX_LICENSES = 4096;
|
||||
if (count > MAX_LICENSES || _worldPacket.rpos() + size_t(count) * sizeof(uint32) > _worldPacket.size())
|
||||
{
|
||||
LicenseIDs.clear();
|
||||
_worldPacket.rfinish();
|
||||
return;
|
||||
}
|
||||
|
||||
LicenseIDs.resize(count);
|
||||
for (uint32& licenseId : LicenseIDs)
|
||||
_worldPacket >> licenseId;
|
||||
}
|
||||
|
||||
WorldPacket const* CatalogShopLicenseData::Write()
|
||||
{
|
||||
_worldPacket << Result;
|
||||
_worldPacket << uint32(Found.size());
|
||||
_worldPacket << uint32(Missing.size());
|
||||
_worldPacket << Unknown;
|
||||
|
||||
for (CatalogShopLicenseEntry const& entry : Found)
|
||||
{
|
||||
_worldPacket << entry.Flags;
|
||||
_worldPacket << entry.LicenseID;
|
||||
_worldPacket << entry.Type;
|
||||
_worldPacket << entry.PrimaryID;
|
||||
_worldPacket << entry.Param2;
|
||||
_worldPacket << entry.Param3;
|
||||
_worldPacket << uint32(entry.Children.size());
|
||||
_worldPacket << entry.SecondaryID;
|
||||
_worldPacket << entry.Param6;
|
||||
_worldPacket << entry.Param7;
|
||||
_worldPacket << entry.Param8;
|
||||
_worldPacket << entry.Param9;
|
||||
_worldPacket << entry.Param10;
|
||||
for (uint32 child : entry.Children)
|
||||
_worldPacket << child;
|
||||
for (uint32 extra : entry.Tail)
|
||||
_worldPacket << extra;
|
||||
}
|
||||
|
||||
for (uint32 licenseId : Missing)
|
||||
{
|
||||
_worldPacket << licenseId;
|
||||
_worldPacket << uint8(0);
|
||||
}
|
||||
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* CatalogShopObtainLicense::Write()
|
||||
{
|
||||
_worldPacket << LicenseID;
|
||||
return &_worldPacket;
|
||||
}
|
||||
|
||||
WorldPacket const* StartPurchaseResponse::Write()
|
||||
|
||||
@@ -47,11 +47,8 @@ namespace WorldPackets
|
||||
void Read() override {}
|
||||
};
|
||||
|
||||
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. Sent by the client mid-checkout carrying its own
|
||||
// license / game data. The body is variable (876/140/52/36/24 bytes across the 12.1.0.69382
|
||||
// capture). We keep the whole body so the handler can log its size for future response modeling;
|
||||
// the paired response wire (SMSG 0x4202C1) is not yet recovered - see
|
||||
// WorldSession::HandleCatalogShopLicenseGameDataRequest.
|
||||
// CMSG_CATALOG_SHOP_LICENSE_GAME_DATA_REQUEST. Body is a uint32 count followed by that many
|
||||
// license ids. Proven on 12.1.0.69933: 1024 bytes = 1 + 255 ids, 264 = 1 + 65, 20 = 1 + 4.
|
||||
class CatalogShopLicenseGameDataRequest final : public ClientPacket
|
||||
{
|
||||
public:
|
||||
@@ -61,7 +58,62 @@ namespace WorldPackets
|
||||
|
||||
void Read() override;
|
||||
|
||||
std::vector<uint8> Data; // raw request body, retained for diagnostics only
|
||||
std::vector<uint32> LicenseIDs;
|
||||
};
|
||||
|
||||
// One resolved license inside SMSG_CATALOG_SHOP_LICENSE_DATA.
|
||||
//
|
||||
// Layout recovered from the 12.1.0.69933 capture (newshop.pkt). Every found record is
|
||||
// u8 flags, u32 licenseId, then 11 uint32 slots, then ChildCount extra uint32s, then any
|
||||
// type-specific tail dwords.
|
||||
// Slot 0 is the deliverable type (14 = item). Slot 4 is the child count and is written from
|
||||
// Children.size(), not stored twice. A type-14 item with no children is 49 bytes, which is
|
||||
// the size of every item record in that capture.
|
||||
struct CatalogShopLicenseEntry
|
||||
{
|
||||
uint8 Flags = 0;
|
||||
uint32 LicenseID = 0;
|
||||
uint32 Type = 0;
|
||||
uint32 PrimaryID = 0;
|
||||
uint32 Param2 = 0;
|
||||
uint32 Param3 = 0;
|
||||
uint32 SecondaryID = 0;
|
||||
uint32 Param6 = 0;
|
||||
uint32 Param7 = 0;
|
||||
uint32 Param8 = 0;
|
||||
uint32 Param9 = 0;
|
||||
uint32 Param10 = 0;
|
||||
std::vector<uint32> Children;
|
||||
std::vector<uint32> Tail;
|
||||
};
|
||||
|
||||
// SMSG_CATALOG_SHOP_LICENSE_DATA. Header proven on all three 69933 responses:
|
||||
// u32 result (1), u32 foundCount, u32 missingCount, u32 unknown (1).
|
||||
// Found records follow, then missing licenses as (u32 licenseId, u8 0) each. The 36-byte
|
||||
// response in that capture is this header plus four missing licenses and nothing else.
|
||||
class CatalogShopLicenseData final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
explicit CatalogShopLicenseData() : ServerPacket(SMSG_CATALOG_SHOP_LICENSE_DATA) {}
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 Result = 1;
|
||||
uint32 Unknown = 1;
|
||||
std::vector<CatalogShopLicenseEntry> Found;
|
||||
std::vector<uint32> Missing;
|
||||
};
|
||||
|
||||
// SMSG_CATALOG_SHOP_OBTAIN_LICENSE. Four bytes, one license id. Retail pushes one packet per
|
||||
// license the account holds (69933 char-select and login both send two of these).
|
||||
class CatalogShopObtainLicense final : public ServerPacket
|
||||
{
|
||||
public:
|
||||
explicit CatalogShopObtainLicense() : ServerPacket(SMSG_CATALOG_SHOP_OBTAIN_LICENSE, 4) {}
|
||||
|
||||
WorldPacket const* Write() override;
|
||||
|
||||
uint32 LicenseID = 0;
|
||||
};
|
||||
|
||||
// The 12.0.7 catalog is a nested reflection bitstream that cannot be re-serialized field-by-field
|
||||
|
||||
@@ -2245,6 +2245,7 @@ public:
|
||||
void HandleBattlePayStartPurchase(WorldPackets::BattlePay::StartPurchase& startPurchase);
|
||||
void HandleBattlePayOpenCheckout(WorldPackets::BattlePay::OpenCheckout & openCheckout);
|
||||
void HandleCatalogShopLicenseGameDataRequest(WorldPackets::BattlePay::CatalogShopLicenseGameDataRequest& request);
|
||||
void SendCatalogShopObtainLicense(uint32 licenseId);
|
||||
void BattlePayProcessPurchase(uint32 productID);
|
||||
void HandleBattlePayConfirmPurchaseResponse(WorldPackets::BattlePay::ConfirmPurchaseResponse& confirmPurchaseResponse);
|
||||
void SendBattlePayDistributionList();
|
||||
|
||||
@@ -231,7 +231,19 @@ void WriteProduct(JsonWriter& writer, ShopProduct const& product, CatalogEntry c
|
||||
writer.Bool(true); // BattlePayMgr::IsPurchasable is the authority
|
||||
WriteKeyUint(writer, "termTypeId", 0);
|
||||
WriteKeyUint(writer, "termDuration", 0);
|
||||
WriteKeyUint(writer, "serviceItemId", 0);
|
||||
|
||||
// The parser requires a number here. An item product publishes its item id so the Shop 2 card
|
||||
// can resolve a preview; everything else stays 0, which is what an empty product already sent.
|
||||
uint32 serviceItemId = 0;
|
||||
for (ShopDeliverable const& deliverable : product.Deliverables)
|
||||
{
|
||||
if (deliverable.Type == 1 && deliverable.Id)
|
||||
{
|
||||
serviceItemId = deliverable.Id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
WriteKeyUint(writer, "serviceItemId", serviceItemId);
|
||||
|
||||
WriteKey(writer, "licenses");
|
||||
writer.StartArray();
|
||||
|
||||
Reference in New Issue
Block a user