opcode(vas): parse START_VAS_PURCHASE transfer intent; correct ASSIGN understanding (P8)

This commit is contained in:
luis
2026-09-04 14:52:38 -03:00
parent 93cf1815dd
commit 7bbc68ef5b
3 changed files with 48 additions and 20 deletions
+27 -13
View File
@@ -1677,13 +1677,15 @@ void WorldSession::HandleVasCheckTransferOk(WorldPackets::BattlePay::VasCheckTra
void WorldSession::HandleBattlePayDistributionAssignVas(WorldPackets::BattlePay::BattlePayDistributionAssignVas& packet)
{
// Assign a purchased VAS (transfer/rename/faction-change) entitlement to a character. VAS entitlements are
// created only by the web-side purchase, which this realm has none of, so there is nothing to assign -
// answer honestly with "distribution not found" rather than drop the packet and leave the client's VAS UI
// waiting. The cross-realm move itself IS implemented (VasTransferMgr / .character transfer); wiring this
// opcode to it needs a live capture to prove which request field carries the target realm.
TC_LOG_INFO("network", "BattlePay: DistributionAssignVas from {}: token={} - no VAS entitlement to assign.",
GetPlayerInfo(), packet.Token);
// Assign an ALREADY-PURCHASED VAS distribution to a target. RE of the client serializer (sub_7FF72907F4B0)
// showed this opcode carries a u64 DistributionID (+0x28) and target guids but NO realm address - the
// realm is chosen earlier, at START_VAS_PURCHASE. The distribution it references only exists after a
// web-side purchase settles, which this realm has none of, so there is nothing to assign: answer honestly
// with "distribution not found" rather than drop the packet. (The paid-character-transfer flow itself does
// NOT use this opcode - it drives START_VAS_PURCHASE, handled above; this is the shop's assign-a-bought-
// distribution path.)
TC_LOG_INFO("network", "BattlePay: DistributionAssignVas from {}: distributionId-token={} - no VAS "
"distribution to assign.", GetPlayerInfo(), packet.Token);
WorldPackets::BattlePay::BattlePayDistributionAssignVasResponse response;
response.Field1 = packet.Token;
@@ -1693,12 +1695,24 @@ void WorldSession::HandleBattlePayDistributionAssignVas(WorldPackets::BattlePay:
void WorldSession::HandleBattlePayStartVasPurchase(WorldPackets::BattlePay::BattlePayStartVasPurchase& packet)
{
// Begin a paid VAS purchase. Like every real-money purchase in this core the settlement is web-side, so the
// realm sends no game response (the client drives the web checkout); the token is logged so the reading can
// be confirmed against a real client. The delivered entitlement, when a payment backend confirms it, is what
// later reaches the assign path above.
TC_LOG_INFO("network", "BattlePay: StartVasPurchase from {}: token={} (settled web-side, no game response).",
GetPlayerInfo(), packet.Token);
// This is the request the paid-character-transfer flow sends (Lua AssignPCTDistribution): it carries the
// selected character, the VAS service type and the TARGET realm's wowRealmAddress. Resolve the target realm
// against the realms this server fronts and log the full transfer intent - this is the client-driven entry
// point to VasTransferMgr.
//
// It is not executed here yet for one honest reason: the flow calls twice - a validation pass then a commit
// pass - distinguished by an isValidationOnly bool that lives in this packet's bit-packed tail. Reading it
// needs the per-string bit-length widths (a small targeted RE still outstanding); executing on the
// validation pass would move the character prematurely. Until that bit is read, the reliable execution path
// is the .character transfer GM command, which drives the same VasTransferMgr.
uint32 targetRealmId = 0;
if (packet.TargetRealmAddress)
targetRealmId = Battlenet::RealmHandle(packet.TargetRealmAddress).Realm;
TC_LOG_INFO("network", "BattlePay: StartVasPurchase from {}: seq={} serviceType={} character={} "
"targetRealmAddress={:#x} (realm {}) - transfer intent parsed; execution gated on the isValidationOnly bit.",
GetPlayerInfo(), packet.SequenceId, packet.ServiceType, packet.Character.ToString(),
packet.TargetRealmAddress, targetRealmId);
}
void WorldSession::HandleCharacterCheckUpgrade(WorldPackets::BattlePay::CharacterCheckUpgrade& /*packet*/)
@@ -508,8 +508,15 @@ namespace WorldPackets::BattlePay
void BattlePayStartVasPurchase::Read()
{
_worldPacket >> Token;
_worldPacket.rfinish(); // the remaining fields are not modelled (see header); consume them
// Wire order from the client builder sub_7FF72AE6E2F0: seq, serviceType, guid, context, targetRealmAddress,
// then name/blob strings and further guids we do not need. Read up to the realm address, then consume the
// rest so the stream stays aligned.
_worldPacket >> SequenceId;
_worldPacket >> ServiceType;
_worldPacket >> Character;
_worldPacket >> Context;
_worldPacket >> TargetRealmAddress;
_worldPacket.rfinish();
}
void BattlePayDistributionAssignVas::Read()
@@ -969,10 +969,13 @@ namespace WorldPackets
void Read() override { _worldPacket.rfinish(); }
};
// CMSG_BATTLE_PAY_START_VAS_PURCHASE (0x400122) - a large nested struct beginning the paid VAS
// purchase (product, target guids, region/appearance choices). Only the leading uint32 (a client token)
// is modelled; the rest is consumed. Like a real-money checkout, the purchase itself is settled web-side,
// so the realm sends no game response - it is not a request the client blocks a game answer on.
// CMSG_BATTLE_PAY_START_VAS_PURCHASE (0x400122) - begins a paid VAS purchase. The leading fields are
// resolved from the client builder sub_7FF72AE6E2F0: a request sequence id, the VAS service type, a
// guid (the selected character/product), a context uint32, then the TARGET REALM's wowRealmAddress
// (copied from the picked JamCliVASTargetRealm[+0]). The tail (name/blob strings, more guids) is not
// needed to identify the transfer and is consumed. The per-guid role is not offline-provable, but the
// first guid is the selected character in the transfer flow; downstream validation (VasTransferMgr)
// rejects a wrong guid safely.
class BattlePayStartVasPurchase final : public ClientPacket
{
public:
@@ -980,7 +983,11 @@ namespace WorldPackets
void Read() override;
uint32 Token = 0;
uint32 SequenceId = 0;
uint32 ServiceType = 0;
ObjectGuid Character; // first guid (selected character in the PCT flow)
uint32 Context = 0;
uint32 TargetRealmAddress = 0; // wowRealmAddress of the picked target realm (JamCliVASTargetRealm[+0])
};
// CMSG_BATTLE_PAY_DISTRIBUTION_ASSIGN_VAS (0x400167) - a large nested struct (client token, several