Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling. Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata. Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks. Improve automatic login and recovery diagnostics; document configuration and release flows.
130 lines
4.7 KiB
Go
130 lines
4.7 KiB
Go
package gacha
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"testing"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func fixtureSchedule(t *testing.T) (*ScheduleSeed, error) {
|
|
golden, err := base64.StdEncoding.DecodeString("ChEIpgEQgMi/3Iw0GJiIvcaRNAoSCLrqARCA8PTEiDQYmIi9xpE0ChIIu+oBEIDw9MSINBiYiL3GkTQKEQjOARCA8PTEiDQYmIi9xpE0ChEIzQEQgPD0xIg0GJiIvcaRNAoRCNABEIDIv9yMNBiYiL3GkTQKEQiZARCAyL/cjDQYmIi9xpE0ChAISBCAyL/cjDQYmIi9xpE0ChAIRxCAyL/cjDQYmIi9xpE0ChEIzwEQgMi/3Iw0GJiIvcaRNAoRCPEHEIDo2beLNBiA+L34jzQSBAgCGAo6EAgdEIDw9MSINBiYiL3GkTQ6EAgeEIDIv9yMNBiYiL3GkTRKUAiTCUJLGgMQvR4aBRCFBzABGgUQ0ggwAhoGEOn7AzADGgYQzeMDMAQaBhDF7gMwBRoGEJnsAzAGGgYQvfkDMAcaBhDu6QMwCBoGEKnvAzAJ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &ScheduleSeed{ClientVersion: "2.35.10", Schedules: collectScheduleWindows(t, golden, 1), StepUps: collectScheduleWindows(t, golden, 7)}, nil
|
|
}
|
|
|
|
func TestActivePickupCostumesUsesHalfOpenScheduleWindows(t *testing.T) {
|
|
character := gamedata.CharacterDesign{ID: 1, HP: 1, CostumeMaxLevel: 5}
|
|
catalog, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{
|
|
11: {ID: 11, Count: 1, PriceType: 3, Price: 1, Pool: []gamedata.WeightedCostume{{ID: 101, Weight: 1}}},
|
|
12: {ID: 12, Count: 1, PriceType: 3, Price: 1, Pool: []gamedata.WeightedCostume{{ID: 102, Weight: 1}}},
|
|
}, map[uint64]gamedata.CharacterDesign{101: character, 102: character})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, group := range []gamedata.GachaGroupDesign{
|
|
{ID: 1, GachaType: 1, PointCount: 1, PickUpCostumeID: 101, OneTimeGachaID: 11},
|
|
{ID: 2, GachaType: 1, PointCount: 1, PickUpCostumeID: 102, OneTimeGachaID: 12},
|
|
} {
|
|
if err := catalog.AddGroupDesign(group, gamedata.GachaFixedDesign{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
seed := &ScheduleSeed{Schedules: []ScheduleWindow{
|
|
{GroupID: 1, StartTime: 100, EndTime: 200},
|
|
{GroupID: 2, StartTime: 200, EndTime: 300},
|
|
}}
|
|
if got := ActivePickupCostumes(catalog, seed, 100); !got[101] || got[102] || len(got) != 1 {
|
|
t.Fatalf("at start active=%v", got)
|
|
}
|
|
if got := ActivePickupCostumes(catalog, seed, 200); got[101] || !got[102] || len(got) != 1 {
|
|
t.Fatalf("at end active=%v", got)
|
|
}
|
|
}
|
|
|
|
func TestGachaInfoUsesInjectedScheduleAndEmptyAccountHasNoPreview(t *testing.T) {
|
|
seed, err := fixtureSchedule(t)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
character := gamedata.CharacterDesign{ID: 1, HP: 1, CostumeMaxLevel: 5, OverflowItemType: 20, OverflowItemCount: 1}
|
|
design, err := fixtureInfiniteGachaDesign(10, []uint64{11}, map[uint64]gamedata.CharacterDesign{11: character})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular, err := fixtureRegularCatalog(map[uint64]gamedata.RegularGacha{1: {ID: 1, Count: 1, PriceType: 3, Price: 1, Pool: []gamedata.WeightedCostume{{ID: 11, Weight: 1}}}}, map[uint64]gamedata.CharacterDesign{11: character})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
storage := stateio.NewMemory()
|
|
collection, err := player.OpenCollectionStore(storage, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
service, err := NewService(design, regular, collection, wallet)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.AttachSchedule(seed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, response, handled, err := service.Handle("/GachaInfo", wire.AppendVarint(nil, 1, 1))
|
|
if err != nil || !handled || code != 145 {
|
|
t.Fatalf("code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
regularWindows := collectScheduleWindows(t, response, 1)
|
|
stepWindows := collectScheduleWindows(t, response, 7)
|
|
if len(regularWindows) != 11 || len(stepWindows) != 2 || countFields(response, 9) != 0 {
|
|
t.Fatalf("schedule=%d step=%d preview=%d", len(regularWindows), len(stepWindows), countFields(response, 9))
|
|
}
|
|
for i := range seed.Schedules {
|
|
if regularWindows[i] != seed.Schedules[i] {
|
|
t.Fatalf("schedule %d response=%+v seed=%+v", i, regularWindows[i], seed.Schedules[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func collectScheduleWindows(t *testing.T, proto []byte, number int) []ScheduleWindow {
|
|
t.Helper()
|
|
var result []ScheduleWindow
|
|
if err := wire.Walk(proto, func(field wire.Field) error {
|
|
if field.Number != number {
|
|
return nil
|
|
}
|
|
group, _, err := wire.Varint(field.Value, 1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
start, _, err := wire.Varint(field.Value, 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
end, _, err := wire.Varint(field.Value, 3)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
free, _, err := wire.Varint(field.Value, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cash, _, err := wire.Varint(field.Value, 5)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result = append(result, ScheduleWindow{GroupID: group, StartTime: start, EndTime: end, FreeCountBonus: free != 0, CashCountBonus: cash != 0})
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result
|
|
}
|