Files
bd2/go/internal/server/world/pack_purchase_test.go
T
Flechazo 5b5a219dc2 feat(server): add special loot and data-driven gameplay rules
Generate equipment loot atomically, grant random boxes and prevent repeat claims.
Read gacha pools, pity, products, tickets, missions, presets, chapter rewards
and inventory costs from GameData; extend both collaboration UR ticket pools.
Sync owned recipes, configure startup state and calendars in versioned seeds,
and validate unlock tickets and revival talents.
2026-10-04 19:15:59 +08:00

144 lines
4.4 KiB
Go

package world
import (
"bd2server/internal/server/accountstate"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"errors"
"path/filepath"
"testing"
)
func TestPaidPackUsesDesignCostAndCurrenciesOnce(t *testing.T) {
storage := stateio.NewMemory()
s := testService()
var err error
s.inventory, err = player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
s.collection, err = player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
s.wallet, err = player.OpenWallet(storage, player.Currency{FreeJewelry: 101})
if err != nil {
t.Fatal(err)
}
s.storyCatalog.Packs[709] = gamedata.StoryPack{ID: 709, BuyType: 3, BuyPrice: 37, BuyRewards: []gamedata.Reward{{Type: 2, Count: 8}, {Type: 20, Count: 11}}}
for attempt := 0; attempt < 2; attempt++ {
if _, err := s.purchaseStoryPack(709, false); err != nil {
t.Fatal(err)
}
}
got := s.wallet.Snapshot()
if got.FreeJewelry != 64 || got.Jewelry != 8 || got.Mileage != 11 {
t.Fatalf("currency=%+v", got)
}
s.storyCatalog.Packs[710] = gamedata.StoryPack{ID: 710, BuyType: 3, BuyPrice: 37, BuyRewards: []gamedata.Reward{{Type: 8, ID: 4, Count: 1}}}
if _, err := s.purchaseStoryPack(710, false); err == nil {
t.Fatal("invalid reward accepted")
}
if s.wallet.Snapshot().FreeJewelry != 64 {
t.Fatal("invalid reward charged")
}
}
type purchaseFailStore struct {
*accountstate.Repository
fail bool
}
func (s *purchaseFailStore) SaveWithEntries(domain string, core []byte, changes []stateio.EntryMutation) error {
if s.fail && domain == "collection" {
return errors.New("forced purchase marker failure")
}
return s.Repository.SaveWithEntries(domain, core, changes)
}
func TestPackPurchaseFailureRollsBackSQLiteCurrencyTicketAndMarker(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.db")
repo, err := accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer func() { repo.Close() }()
storage := &purchaseFailStore{Repository: repo}
s := testService()
s.seed.PackID = 709
s.inventory, err = player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
s.wallet, err = player.OpenWallet(storage, player.Currency{Catalyst: 5})
if err != nil {
t.Fatal(err)
}
s.collection, err = player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
if _, err := s.wallet.GrantQuestOnce("test:baseline", nil); err != nil {
t.Fatal(err)
}
s.storyCatalog = &gamedata.StoryCatalog{Packs: map[int]gamedata.StoryPack{709: {ID: 709, BuyRewards: []gamedata.Reward{{Type: 12, Count: 17}, {Type: 19, ID: 88, Count: 1}}}}}
op, err := repo.BeginOperation()
if err != nil {
t.Fatal(err)
}
storage.fail = true
request := wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 709)
if _, _, _, err := s.Handle("/PackBuy", request); err == nil {
t.Fatal("forced collection failure returned success")
}
if err := op.Rollback(); err != nil && !errors.Is(err, stateio.ErrStateRecoveryRequired) {
t.Fatal(err)
}
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
storage.Repository = repo
storage.fail = false
// Requests reconstruct domains following a rollback; in-memory snapshots
// that participated in the aborted operation must not be reused.
wallet, err := player.OpenWallet(storage, player.Currency{})
if err != nil {
t.Fatal(err)
}
inventory, err := player.OpenInventory(storage, &player.Starter{Version: "2.35.10"})
if err != nil {
t.Fatal(err)
}
collection, err := player.OpenCollectionStore(storage, nil)
if err != nil {
t.Fatal(err)
}
if wallet.CatalystBalance() != 5 || wallet.WasGranted("pack-purchase:709:currency") || len(inventory.All()) != 0 {
t.Fatal("aborted purchase persisted partial awards")
}
if _, found := collection.Grant("pack-purchase:709"); found {
t.Fatal("aborted purchase persisted marker")
}
s.wallet, s.inventory, s.collection = wallet, inventory, collection
op, err = repo.BeginOperation()
if err != nil {
t.Fatal(err)
}
if _, _, _, err := s.Handle("/PackBuy", request); err != nil {
op.Rollback()
t.Fatal(err)
}
if err := op.Commit(); err != nil {
t.Fatal(err)
}
if wallet.CatalystBalance() != 22 || len(inventory.All()) != 1 {
t.Fatal("retried purchase did not grant exactly once")
}
}