170 lines
6.4 KiB
Go
170 lines
6.4 KiB
Go
package gacha
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestVersionedScheduleMatchesOfficial23510GachaInfo(t *testing.T) {
|
|
seed, err := LoadScheduleSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "gacha_schedule.json"), "2.35.10")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Official 2.35.10 GachaInfo, captured 2026-09-23. It is evidence used by
|
|
// this test only; production loads the semantic JSON seed above.
|
|
golden, err := base64.StdEncoding.DecodeString("ChEIpgEQgMi/3Iw0GJiIvcaRNAoSCLrqARCA8PTEiDQYmIi9xpE0ChIIu+oBEIDw9MSINBiYiL3GkTQKEQjOARCA8PTEiDQYmIi9xpE0ChEIzQEQgPD0xIg0GJiIvcaRNAoRCNABEIDIv9yMNBiYiL3GkTQKEQiZARCAyL/cjDQYmIi9xpE0ChAISBCAyL/cjDQYmIi9xpE0ChAIRxCAyL/cjDQYmIi9xpE0ChEIzwEQgMi/3Iw0GJiIvcaRNAoRCPEHEIDo2beLNBiA+L34jzQSBAgCGAo6EAgdEIDw9MSINBiYiL3GkTQ6EAgeEIDIv9yMNBiYiL3GkTRKUAiTCUJLGgMQvR4aBRCFBzABGgUQ0ggwAhoGEOn7AzADGgYQzeMDMAQaBhDF7gMwBRoGEJnsAzAGGgYQvfkDMAcaBhDu6QMwCBoGEKnvAzAJ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular := collectScheduleWindows(t, golden, 1)
|
|
steps := collectScheduleWindows(t, golden, 7)
|
|
if len(regular) != len(seed.Schedules) || len(steps) != len(seed.StepUps) {
|
|
t.Fatalf("golden schedules=%d/%d seed=%d/%d", len(regular), len(steps), len(seed.Schedules), len(seed.StepUps))
|
|
}
|
|
for i := range seed.Schedules {
|
|
if regular[i] != seed.Schedules[i] {
|
|
t.Fatalf("schedule %d golden=%+v seed=%+v", i, regular[i], seed.Schedules[i])
|
|
}
|
|
}
|
|
for i := range seed.StepUps {
|
|
if steps[i] != seed.StepUps[i] {
|
|
t.Fatalf("step-up %d golden=%+v seed=%+v", i, steps[i], seed.StepUps[i])
|
|
}
|
|
}
|
|
if countFields(golden, 9) != 1 {
|
|
t.Fatal("official golden must retain the player preview used to verify field 9 separately")
|
|
}
|
|
}
|
|
|
|
func TestScheduleSeedStrictValidation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "gacha.json")
|
|
bad := `{"client_version":"2.35.10","schedules":[{"group_id":1,"start_time":1,"end_time":2}],"step_ups":[{"group_id":2,"start_time":1,"end_time":2}],"unknown":true}`
|
|
if err := os.WriteFile(path, []byte(bad), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadScheduleSeed(path, "2.35.10"); err == nil {
|
|
t.Fatal("unknown schedule field was accepted")
|
|
}
|
|
if _, err := LoadScheduleSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "gacha_schedule.json"), "0.0.0"); err == nil {
|
|
t.Fatal("wrong client version was accepted")
|
|
}
|
|
}
|
|
|
|
func TestActivePickupCostumesUsesHalfOpenScheduleWindows(t *testing.T) {
|
|
character := gamedata.CharacterDesign{ID: 1, HP: 1, CostumeMaxLevel: 5}
|
|
catalog, err := gamedata.NewRegularGachaCatalog(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 := LoadScheduleSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "gacha_schedule.json"), "2.35.10")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
character := gamedata.CharacterDesign{ID: 1, HP: 1, CostumeMaxLevel: 5, OverflowItemType: 20, OverflowItemCount: 1}
|
|
design, err := gamedata.NewInfiniteGachaDesign(10, []uint64{11}, map[uint64]gamedata.CharacterDesign{11: character})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
regular, err := gamedata.NewRegularGachaCatalog(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
|
|
}
|