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.
200 lines
7.6 KiB
Go
200 lines
7.6 KiB
Go
package gacha
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestGachaPointExchangeGrantsUpgradesOverflowsAndRetries(t *testing.T) {
|
|
const (
|
|
groupID = uint64(205)
|
|
gachaID = uint64(11000145)
|
|
rollID = uint64(60901)
|
|
pickupID = uint64(21201)
|
|
cost = uint64(200)
|
|
)
|
|
characters := map[uint64]gamedata.CharacterDesign{
|
|
rollID: {ID: 6090, HP: 253, CostumeMaxLevel: 100, OverflowItemType: 20, OverflowItemCount: 2},
|
|
pickupID: {ID: 2120, HP: 300, CostumeMaxLevel: 5, OverflowItemType: 20, OverflowItemCount: 2},
|
|
}
|
|
infinite, err := fixtureInfiniteGachaDesign(10, []uint64{rollID}, characters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{
|
|
gachaID: {ID: gachaID, Count: 10, PriceType: 3, Price: 2000, Pool: []gamedata.WeightedCostume{{ID: rollID, Weight: 1}}},
|
|
}, characters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
group := gamedata.GachaGroupDesign{
|
|
ID: groupID, GachaType: 1, PointCount: 140,
|
|
PickUpExchangeCost: cost, PickUpCostumeID: pickupID, TenTimeGachaID: gachaID,
|
|
}
|
|
if err := regular.AddGroupDesign(group, gamedata.GachaFixedDesign{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storage := stateio.NewMemory()
|
|
collection, err := player.OpenCollectionStore(storage, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seed := make([]uint64, 10)
|
|
for i := range seed {
|
|
seed[i] = rollID
|
|
}
|
|
if _, err := collection.GrantRegularPurchase("point-seed", seed, regular, player.GachaPurchase{Group: group, BuyType: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := NewService(infinite, regular, collection, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service.BeginSession("point-costume-login")
|
|
|
|
var lastRequest, lastResponse []byte
|
|
for exchange := uint64(0); exchange < 7; exchange++ {
|
|
request := wire.AppendVarint(nil, 1, 100+exchange)
|
|
request = wire.AppendVarint(request, 2, groupID)
|
|
code, response, handled, err := service.Handle("/GachaPointExchange", request)
|
|
if err != nil || !handled || code != 147 {
|
|
t.Fatalf("exchange %d code=%d handled=%v err=%v", exchange, code, handled, err)
|
|
}
|
|
bundle, found, err := wire.Bytes(response, 1)
|
|
if err != nil || !found {
|
|
t.Fatalf("exchange %d reward bundle found=%v err=%v", exchange, found, err)
|
|
}
|
|
switch {
|
|
case exchange == 0:
|
|
if countFields(bundle, 2) != 1 || countFields(bundle, 3) != 1 || countFields(bundle, 9) != 0 {
|
|
t.Fatalf("new pickup reward characters=%d costumes=%d upgrades=%d bundle=%x", countFields(bundle, 2), countFields(bundle, 3), countFields(bundle, 9), bundle)
|
|
}
|
|
case exchange < 6:
|
|
if countFields(bundle, 3) != 0 || countFields(bundle, 9) != 1 || countFields(bundle, 8) != 0 {
|
|
t.Fatalf("upgrade %d costumes=%d upgrades=%d exchanges=%d bundle=%x", exchange, countFields(bundle, 3), countFields(bundle, 9), countFields(bundle, 8), bundle)
|
|
}
|
|
default:
|
|
if countFields(bundle, 9) != 1 || countFields(bundle, 8) != 1 || countFields(bundle, 10) != 1 {
|
|
t.Fatalf("overflow upgrades=%d exchanges=%d repaid=%d bundle=%x", countFields(bundle, 9), countFields(bundle, 8), countFields(bundle, 10), bundle)
|
|
}
|
|
}
|
|
lastRequest, lastResponse = request, response
|
|
}
|
|
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 || user.ExchangeMileageCount != 0 {
|
|
t.Fatalf("gacha user after exchanges=%+v", user)
|
|
}
|
|
if got := wallet.Snapshot().Mileage; got != 2 {
|
|
t.Fatalf("overflow mileage=%d want=2", got)
|
|
}
|
|
owned := collection.Costumes()
|
|
if len(owned) != 2 || owned[1].ID != pickupID || owned[1].Level != 5 {
|
|
t.Fatalf("owned costumes=%+v", owned)
|
|
}
|
|
|
|
// A byte-identical transport retry must recover the persisted grant and
|
|
// wallet credit without another debit, level change, or exchange count.
|
|
code, replay, handled, err := service.Handle("/GachaPointExchange", lastRequest)
|
|
if err != nil || !handled || code != 147 || string(replay) != string(lastResponse) {
|
|
t.Fatalf("retry code=%d handled=%v err=%v response=%x want=%x", code, handled, err, replay, lastResponse)
|
|
}
|
|
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 {
|
|
t.Fatalf("retry changed gacha user=%+v", user)
|
|
}
|
|
if got := wallet.Snapshot().Mileage; got != 2 {
|
|
t.Fatalf("retry changed mileage=%d", got)
|
|
}
|
|
|
|
restored, err := player.OpenCollectionStore(storage, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted, err := NewService(infinite, regular, restored, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted.BeginSession("point-costume-login")
|
|
_, replay, _, err = restarted.Handle("/GachaPointExchange", lastRequest)
|
|
if err != nil || string(replay) != string(lastResponse) {
|
|
t.Fatalf("restart retry err=%v response=%x want=%x", err, replay, lastResponse)
|
|
}
|
|
if user := restored.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 7 {
|
|
t.Fatalf("restored gacha user=%+v", user)
|
|
}
|
|
}
|
|
|
|
func TestGachaPointExchangeUsesOnlySavedSingleSelection(t *testing.T) {
|
|
const (
|
|
groupID = uint64(1001)
|
|
gachaID = uint64(11000001)
|
|
rollID = uint64(60901)
|
|
selected = uint64(64901)
|
|
)
|
|
characters := map[uint64]gamedata.CharacterDesign{
|
|
rollID: fixtureCharacter(6090, 253),
|
|
selected: fixtureCharacter(6490, 300),
|
|
}
|
|
infinite, err := fixtureInfiniteGachaDesign(10, []uint64{rollID}, characters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{
|
|
gachaID: {ID: gachaID, Count: 1, PriceType: 3, Price: 200, Pool: []gamedata.WeightedCostume{{ID: rollID, Weight: 1}}},
|
|
}, characters)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
group := gamedata.GachaGroupDesign{ID: groupID, GachaType: 1, PointCount: 200, PickUpExchangeCost: 200, OneTimeGachaID: gachaID, SelectCount: 1}
|
|
if err := regular.AddGroupDesign(group, gamedata.GachaFixedDesign{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storage := stateio.NewMemory()
|
|
collection, err := player.OpenCollectionStore(storage, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := collection.GrantRegularPurchase("selection-point-seed", []uint64{rollID}, regular, player.GachaPurchase{Group: group, BuyType: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := collection.SaveGachaSelections(groupID, []player.GachaSelection{{GroupID: groupID, Slot: 1, ItemID: selected}}, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := NewService(infinite, regular, collection, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service.BeginSession("selection-point-login")
|
|
|
|
bad := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, groupID), 3, rollID)
|
|
if code, _, handled, err := service.Handle("/GachaPointExchange", bad); err == nil || !handled || code != 147 {
|
|
t.Fatalf("unsaved selection code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
if user := collection.GachaUser(groupID); user.Point != 200 || user.ExchangeItemCount != 0 {
|
|
t.Fatalf("rejected selection changed user=%+v", user)
|
|
}
|
|
|
|
request := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 1, 2), 2, groupID), 3, selected)
|
|
code, response, handled, err := service.Handle("/GachaPointExchange", request)
|
|
if err != nil || !handled || code != 147 {
|
|
t.Fatalf("saved selection code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
bundle, found, err := wire.Bytes(response, 1)
|
|
if err != nil || !found || countFields(bundle, 3) != 1 {
|
|
t.Fatalf("saved selection reward found=%v costumes=%d err=%v bundle=%x", found, countFields(bundle, 3), err, bundle)
|
|
}
|
|
if user := collection.GachaUser(groupID); user.Point != 0 || user.ExchangeItemCount != 1 {
|
|
t.Fatalf("saved selection user=%+v", user)
|
|
}
|
|
}
|