Files
bd2/go/internal/server/commerce/installed_native_test.go
T

236 lines
8.2 KiB
Go

package commerce
import (
"bd2server/internal/server/gameconfig"
"bd2server/internal/server/readonly"
"bd2server/internal/server/wire"
"bytes"
"fmt"
"os"
"testing"
"time"
"bd2server/internal/server/events"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/hunting"
"bd2server/internal/server/mail"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
)
// Covers static native goods independently of their current publication
// windows. The account is an isolated memory store; no player saves are read.
func TestInstalledAllNativeProductRewardAndPriceDomains(t *testing.T) {
root := os.Getenv("BD2_REAL_GAMEDATA")
if root == "" {
t.Skip("BD2_REAL_GAMEDATA not configured")
}
const version = "20260923193640"
catalog, err := gamedata.LoadCashCatalog(root, version)
if err != nil {
t.Fatal(err)
}
graph, err := gamedata.LoadRewardGraph(root, version)
if err != nil {
t.Fatal(err)
}
costumes, err := gamedata.LoadRewardCostumeCatalog(root, version)
if err != nil {
t.Fatal(err)
}
options, err := gamedata.LoadRewardEquipmentCatalog(root, version)
if err != nil {
t.Fatal(err)
}
owned, err := gamedata.LoadOwnedEventItemDesign(root, version)
if err != nil {
t.Fatal(err)
}
avatars, err := gamedata.LoadAvatarRewardDesign(root, version)
if err != nil {
t.Fatal(err)
}
skins, err := gamedata.LoadPrestigeSkins(root, version)
if err != nil {
t.Fatal(err)
}
newAccount := func() (*events.Economy, *player.Inventory, *player.Wallet, stateio.Store, *hunting.Service) {
store := stateio.NewMemory()
items, err := player.OpenInventory(store, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
wallet, err := player.OpenWallet(store, player.Currency{})
if err != nil {
t.Fatal(err)
}
collection, err := player.OpenCollectionStore(store, nil)
if err != nil {
t.Fatal(err)
}
equipment, err := player.OpenEquipmentInventory(store)
if err != nil {
t.Fatal(err)
}
economy, err := events.NewEconomy(store, items, wallet, collection, equipment, costumes, options, graph, nil)
if err != nil {
t.Fatal(err)
}
economy.AttachOwnedItemDesign(owned)
economy.AttachAvatarRewards(avatars)
economy.AttachPrestigeSkins(skins)
ap, err := hunting.Open(store, root, version, items, wallet, func() (int, error) { return 21, nil }, 0, 0)
if err != nil {
t.Fatal(err)
}
economy.AttachHuntingAP(ap)
return economy, items, wallet, store, ap
}
economy, _, _, _, _ := newAccount()
families := map[uint64]int{}
free := 0
native := 0
// Each product reward is settled once through the actual shared economy.
// Prices are funded in that same typed domain and spent once separately,
// avoiding historical publication or predecessor constraints in this audit.
for _, product := range catalog.Products {
if product.PriceType == 1 {
continue
}
native++
if native%50 == 0 {
economy, _, _, _, _ = newAccount()
}
families[product.PriceType]++
identity := fmt.Sprintf("native-audit:%d:%d:%d", product.Key.GroupID, product.Key.ProductID, product.Key.SaleGroup)
var cost []gamedata.Reward
if product.PriceType == 0 {
free++
if product.PriceID != 0 || product.PriceCount != 0 {
t.Errorf("free product has nonzero price %+v", product.Key)
}
} else {
cost = []gamedata.Reward{{Type: product.PriceType, ID: product.PriceID, Count: product.PriceCount}}
if _, err = economy.Apply(identity+":fund", nil, cost); err != nil {
t.Errorf("native price domain %+v failed: %v", product.Key, err)
continue
}
}
rewards := []gamedata.Reward{{Type: 9, ID: product.RandomBoxID, Count: 1}}
if product.BonusRandomBoxID != 0 {
rewards = append(rewards, gamedata.Reward{Type: 9, ID: product.BonusRandomBoxID, Count: 1})
}
if _, err = economy.Apply(identity, cost, rewards); err != nil {
t.Errorf("native product %+v real reward/debit failed: %v", product.Key, err)
}
}
if native != 2274 || free != 100 || len(families) != 14 {
t.Fatalf("native design coverage changed: goods=%d free=%d families=%v", native, free, families)
}
t.Run("native dispatcher quantity free and total diamonds", func(t *testing.T) {
base, items, wallet, store, ap := newAccount()
entitlementDesign, err := gamedata.LoadCashEntitlementDesign(root, version)
if err != nil {
t.Fatal(err)
}
resolver, err := gamedata.LoadCashRewardResolver(root, version, graph)
if err != nil {
t.Fatal(err)
}
entitlement, err := NewEntitlementEconomy(store, base, resolver, items, entitlementDesign)
if err != nil {
t.Fatal(err)
}
mailbox, err := mail.OpenService(store, &mail.Starter{Version: "2.35.10", MailCount: 1}, items, wallet)
if err != nil {
t.Fatal(err)
}
templates, err := gamedata.LoadCashMailTemplates(root, version)
if err != nil {
t.Fatal(err)
}
if err = mailbox.AttachCashRewards(entitlement, templates); err != nil {
t.Fatal(err)
}
if err = entitlement.AttachCashMail(mailbox); err != nil {
t.Fatal(err)
}
policy, err := NewCatalog("2.35.10", catalog, gameconfig.Default().Purchases)
if err != nil {
t.Fatal(err)
}
service, err := NewService(policy, store, entitlement)
if err != nil {
t.Fatal(err)
}
field := func(n int, v uint64) readonly.Field { return readonly.Field{Number: n, Type: 0, Varint: v} }
seed := &readonly.Seed{Responses: map[string]readonly.Response{"/CashShopInfo": {Fields: []readonly.Field{{Number: 1, Type: 2, Fields: []readonly.Field{field(1, 1700001), field(2, 1), field(3, 0), field(4, 1), field(5, uint64(time.Now().Add(time.Hour).UnixMilli()))}}}}}}
if err = service.AttachShopSeed(seed); err != nil {
t.Fatal(err)
}
if _, err = base.Apply("fund diamonds", nil, []gamedata.Reward{{Type: 3, Count: 1000}, {Type: 2, Count: 50000}}); err != nil {
t.Fatal(err)
}
makeBuy := func(seq, group, id, count uint64) []byte {
r := wire.AppendVarint(nil, 1, seq)
r = wire.AppendVarint(r, 3, group)
line := wire.AppendVarint(nil, 1, id)
line = wire.AppendVarint(line, 3, count)
return wire.AppendBytes(r, 4, line)
}
request := makeBuy(1, 400001, 2, 300)
_, bonusBefore, err := ap.HuntingAP()
if err != nil {
t.Fatal(err)
}
code, response, handled, err := service.HandleSession("/CashShopBuy", request, "native-audit")
if err != nil || !handled || code != 61 {
t.Fatalf("native quantity300 code%d handled%t err%v", code, handled, err)
}
balance := wallet.Snapshot()
_, bonus, err := ap.HuntingAP()
if err != nil || balance.FreeJewelry != 0 || balance.Jewelry != 0 || bonus != bonusBefore+18000 {
t.Fatal("total diamond split or AP grant wrong", balance, bonus, err)
}
_, replay, _, err := service.HandleSession("/CashShopBuy", request, "native-audit")
_, againBonus, _ := ap.HuntingAP()
if err != nil || !bytes.Equal(response, replay) || againBonus != bonus || wallet.Snapshot() != balance {
t.Fatal("bulk native replay duplicated", err)
}
found := false
for _, raw := range service.PurchaseCountDBInfos() {
g, _, _ := wire.Varint(raw, 1)
id, _, _ := wire.Varint(raw, 2)
count, _, _ := wire.Varint(raw, 4)
if g == 400001 && id == 2 && count == 300 {
found = true
}
}
if !found {
t.Fatal("quantity300 count missing")
}
free := makeBuy(2, 1700001, 1, 1)
if code, _, handled, err = service.HandleSession("/CashShopBuy", free, "native-audit"); err != nil || !handled || code != 61 {
t.Fatal("free native goods failed", err)
}
if wallet.Snapshot().Jewelry != balance.Jewelry || wallet.Snapshot().FreeJewelry != balance.FreeJewelry {
t.Fatal("free goods charged currency")
}
if _, _, _, err = service.HandleSession("/CashShopBuy", makeBuy(3, 1700001, 1, 1), "native-audit"); err == nil {
t.Fatal("free limit bypassed")
}
if _, err = base.Apply("fund resource", nil, []gamedata.Reward{{Type: 8, ID: 1002, Count: 3000}}); err != nil {
t.Fatal(err)
}
if code, _, handled, err = service.HandleSession("/CashShopBuy", makeBuy(4, 700001, 2, 300), "native-audit"); err != nil || !handled || code != 61 {
t.Fatal("native resource quantity300 failed", err)
}
for _, item := range items.All() {
if item.Type == 8 && item.ID == 1002 && item.Count != 0 {
t.Fatal("resource price not consumed", item)
}
}
})
t.Logf("validated %d native goods across %d typed price domains, including %d free goods", native, len(families), free)
}