[econ] fix consumable/skin boxes

This commit is contained in:
Devin Zuczek
2026-09-24 14:18:06 -04:00
parent a1e89d6948
commit 72fd75e29e
3 changed files with 35 additions and 9 deletions
+21 -5
View File
@@ -1099,6 +1099,21 @@ function truncateGiftMessage(message: string): string {
* with nothing but the row to say who sent it or why. They default to Coach and the drop's
* own context — a box the server handed over on nobody's behalf.
*/
/**
* `AvatarItemType` as a gift box carries it: the drop's own for an avatar item, NULL for
* anything else — a consumable, a skin, a box, a token bundle.
*
* The client routes a box by this field before it looks at what else the box names: a 0
* reads as "an avatar item of type 0", and a consumable or skin box sent with 0 fails with
* "can't find avatar item" because there is no such item to find. The 2025 store lists every
* non-avatar drop with `AvatarItemType: null` for exactly that reason, and this is what the
* old `?? 0` coalescing used to erase. Applied at every surface a box is serialized on — the
* stored box, both purchase responses and the hub frame — so they cannot disagree.
*/
function boxAvatarItemType(drop: StoreGiftDrop): number | null {
return drop.AvatarItemDesc !== '' ? (drop.AvatarItemType ?? 0) : null
}
function toGiftContent(
giftDrop: StoreGiftDrop,
message: string,
@@ -1116,7 +1131,7 @@ function toGiftContent(
ConsumableMappingId: consumableMappingId,
ConsumablePreExistingCount: consumablePreExistingCount,
AvatarItemDesc: giftDrop.AvatarItemDesc,
AvatarItemType: giftDrop.AvatarItemType,
AvatarItemType: boxAvatarItemType(giftDrop),
CurrencyType: giftDrop.CurrencyType,
Currency: giftDrop.Currency,
Xp: giftDrop.Xp ?? 0,
@@ -1197,7 +1212,7 @@ async function pushGiftReceived(
FromPlayerId: fromPlayerId,
ConsumableItemDesc: gift.drop.ConsumableItemDesc,
AvatarItemDesc: gift.drop.AvatarItemDesc,
AvatarItemType: gift.drop.AvatarItemType ?? 0,
AvatarItemType: boxAvatarItemType(gift.drop),
EquipmentPrefabName: gift.drop.EquipmentPrefabName,
EquipmentModificationGuid: gift.drop.EquipmentModificationGuid,
CurrencyType: gift.drop.CurrencyType,
@@ -1347,7 +1362,8 @@ function toSkinStoreItem(row: CatalogRow): StoreItem {
Tooltip: row.tooltip ?? '',
ConsumableItemDesc: '',
AvatarItemDesc: '',
AvatarItemType: 0,
// A skin is no avatar item: null, as the store lists every skin.
AvatarItemType: null,
EquipmentPrefabName: row.prefab_name ?? '',
EquipmentModificationGuid: row.item_key,
Rarity: row.rarity,
@@ -1668,7 +1684,7 @@ function toBalanceUpdateData(
FromPlayerId: fromPlayerId,
ConsumableItemDesc: drop.ConsumableItemDesc,
AvatarItemDesc: drop.AvatarItemDesc,
AvatarItemType: drop.AvatarItemType ?? 0,
AvatarItemType: boxAvatarItemType(drop),
EquipmentPrefabName: drop.EquipmentPrefabName,
EquipmentModificationGuid: drop.EquipmentModificationGuid,
CurrencyType: drop.CurrencyType,
@@ -1816,7 +1832,7 @@ function toGiftPackage(
PlayerId: playerId,
FromPlayerId: fromPlayerId,
ConsumableItemDesc: drop.ConsumableItemDesc,
AvatarItemType: drop.AvatarItemType ?? 0,
AvatarItemType: boxAvatarItemType(drop),
AvatarItemDesc: drop.AvatarItemDesc,
CustomAvatarItemId: null,
EquipmentPrefabName: drop.EquipmentPrefabName,
+7 -4
View File
@@ -2827,7 +2827,7 @@ describe('econ endpoints', () => {
Data: Array<{
ConsumableItemDesc: string
AvatarItemDesc: string
AvatarItemType: number
AvatarItemType: number | null
FromPlayerId: number
}>
}>
@@ -2837,8 +2837,9 @@ describe('econ endpoints', () => {
const drop = body.BalanceUpdates[0].Data[0]
expect(drop.ConsumableItemDesc).toBe(consumableDesc)
expect(drop.AvatarItemDesc).toBe('')
// A consumable's AvatarItemType is null in the catalog; the response coalesces it to 0.
expect(drop.AvatarItemType).toBe(0)
// A consumable's AvatarItemType is NULL, as the store lists it — never 0, which the
// client reads as an avatar item and fails on ("can't find avatar item").
expect(drop.AvatarItemType).toBeNull()
// A self-buy is attributed to the "Coach" system account (id 1).
expect(drop.FromPlayerId).toBe(1)
@@ -4888,7 +4889,9 @@ describe('econ endpoints', () => {
FromPlayerId: 1,
ConsumableItemDesc: '',
AvatarItemDesc: gift.AvatarItemDesc,
AvatarItemType: gift.AvatarItemType,
// The week's reward is a SKIN, so this is null whatever the block says: a 0 would
// send the client looking for an avatar item that does not exist.
AvatarItemType: gift.AvatarItemDesc === '' ? null : gift.AvatarItemType,
EquipmentPrefabName: gift.EquipmentPrefabName,
EquipmentModificationGuid: gift.EquipmentModificationGuid,
CurrencyType: 0,