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.
144 lines
4.4 KiB
Go
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")
|
|
}
|
|
}
|