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.
381 lines
13 KiB
Go
381 lines
13 KiB
Go
package deck
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func TestPresetChangedDesignControlsBaseLimitAndCurrency(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
design := gamedata.PresetDesign{BaseCount: 2, Maximum: 3, PriceType: 3, Price: 17}
|
|
d, err := OpenStore(f.storage, f.seed, design)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w, err := player.OpenWallet(stateio.NewMemory(), player.Currency{FreeJewelry: 100})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d.wallet = w
|
|
d.BeginSession("changed-design")
|
|
if d.PresetSlotCount() != 2 {
|
|
t.Fatal("base count ignored")
|
|
}
|
|
request := wire.AppendVarint(wire.AppendVarint(nil, 1, 1), 2, 1)
|
|
if _, _, _, err := d.Handle("/PresetAddSlot", request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.PresetSlotCount() != 3 || w.Snapshot().FreeJewelry != 83 || w.Snapshot().Gold != 0 {
|
|
t.Fatal("changed price/currency ignored")
|
|
}
|
|
request = wire.AppendVarint(wire.AppendVarint(nil, 1, 2), 2, 1)
|
|
if _, _, _, err := d.Handle("/PresetAddSlot", request); err == nil {
|
|
t.Fatal("changed limit ignored")
|
|
}
|
|
if w.Snapshot().FreeJewelry != 83 {
|
|
t.Fatal("invalid purchase charged")
|
|
}
|
|
}
|
|
|
|
type presetFixture struct {
|
|
storage *stateio.Memory
|
|
seed Seed
|
|
deck *Store
|
|
wallet *player.Wallet
|
|
characters *player.CharacterStore
|
|
equipment *player.EquipmentInventory
|
|
collection *player.CollectionStore
|
|
weapon player.Equipment
|
|
}
|
|
|
|
func newPresetFixture(t *testing.T) *presetFixture {
|
|
t.Helper()
|
|
storage := stateio.NewMemory()
|
|
seed, err := LoadSeed("../../../seed/v2_35_10/decks.json")
|
|
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, []player.Costume{
|
|
{InvenIndex: 1001, ID: 3501, UseChar: 100, BurstLevel: 3},
|
|
{InvenIndex: 2001, ID: 3601, UseChar: 200, BurstLevel: 1},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
characters, err := player.OpenCharacterStore(storage, []player.Character{
|
|
{InvenIndex: 100, ID: 350, Level: 20, UseCostume: 1001, CostumeID: 3501},
|
|
{InvenIndex: 200, ID: 360, Level: 20, UseCostume: 2001, CostumeID: 3601},
|
|
}, inventory, "", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := characters.AttachCollection(collection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
equipment, err := player.OpenEquipmentInventory(storage)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := equipment.AttachSlots(map[uint64]uint64{10: 0, 11: 1, 12: 2, 13: 3, 14: 4}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := equipment.AttachCharacters(characters); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
weapon, err := equipment.GrantOnce("preset-test-weapon", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wallet, err := player.OpenWallet(storage, player.Currency{Gold: 20000})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decks, err := OpenStore(storage, seed, testPresetDesign)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := decks.AttachPresetRuntime(wallet, characters, equipment, collection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
decks.BeginSession("preset-test")
|
|
return &presetFixture{storage: storage, seed: seed, deck: decks, wallet: wallet, characters: characters, equipment: equipment, collection: collection, weapon: weapon}
|
|
}
|
|
|
|
func presetRequest(slot, character, costume, equipment uint64, name string) []byte {
|
|
base := wire.AppendVarint(nil, 1, character)
|
|
base = wire.AppendVarint(base, 2, 0)
|
|
base = wire.AppendVarint(base, 3, 1)
|
|
deck := wire.AppendBytes(nil, 1, base)
|
|
deck = wire.AppendVarint(deck, 2, costume)
|
|
for equipmentType := uint64(0); equipmentType < 5; equipmentType++ {
|
|
entry := wire.AppendVarint(nil, 1, equipmentType)
|
|
if equipmentType == 0 {
|
|
entry = wire.AppendVarint(entry, 2, equipment)
|
|
}
|
|
deck = wire.AppendBytes(deck, 3, entry)
|
|
}
|
|
preset := wire.AppendString(nil, 1, name)
|
|
preset = wire.AppendVarint(preset, 2, 1)
|
|
preset = wire.AppendVarint(preset, 4, slot)
|
|
preset = wire.AppendBytes(preset, 5, deck)
|
|
return wire.AppendBytes(nil, 2, preset)
|
|
}
|
|
|
|
func TestPresetSaveInfoMetadataDeleteAndRestart(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
for seq, slot := range []uint64{2, 0} {
|
|
request := req(uint64(seq+1), presetRequest(slot, 100, 1001, f.weapon.InvenIndex, "编队"))
|
|
if code, _, handled, err := f.deck.Handle("/PresetSave", request); err != nil || !handled || code != 179 {
|
|
t.Fatalf("save slot %d code=%d handled=%v err=%v", slot, code, handled, err)
|
|
}
|
|
}
|
|
code, response, handled, err := f.deck.Handle("/PresetInfo", req(10))
|
|
if err != nil || !handled || code != 178 {
|
|
t.Fatalf("info code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
var slots []uint64
|
|
if err := wire.Walk(response, func(field wire.Field) error {
|
|
if field.Number == 1 {
|
|
slot, _, err := wire.Varint(field.Value, 4)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
slots = append(slots, slot)
|
|
}
|
|
return nil
|
|
}); err != nil || !reflect.DeepEqual(slots, []uint64{0, 2}) {
|
|
t.Fatalf("ordered slots=%v err=%v", slots, err)
|
|
}
|
|
|
|
change := wire.AppendString(nil, 2, "主力队")
|
|
change = wire.AppendVarint(change, 3, 21)
|
|
change = wire.AppendVarint(change, 4, 5)
|
|
// slot=0 is absent on the real proto3 wire.
|
|
if code, _, _, err := f.deck.Handle("/PresetInfoChange", req(11, change)); err != nil || code != 278 {
|
|
t.Fatalf("metadata change code=%d err=%v", code, err)
|
|
}
|
|
if got := f.deck.presets[0]; got.Name != "主力队" || got.ResourceID != 21 || got.ResourceColor != 5 || len(got.Decks) != 1 {
|
|
t.Fatalf("metadata change lost content: %+v", got)
|
|
}
|
|
|
|
restarted, err := OpenStore(f.storage, f.seed, testPresetDesign)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(restarted.presets) != 2 || restarted.presets[0].Name != "主力队" {
|
|
t.Fatalf("restarted presets=%+v", restarted.presets)
|
|
}
|
|
restarted.BeginSession("restart")
|
|
deleteRequest := req(12, wire.AppendVarint(nil, 2, 0), wire.AppendVarint(nil, 2, 2))
|
|
if code, _, handled, err := restarted.Handle("/PresetDelete", deleteRequest); err != nil || !handled || code != 0 {
|
|
t.Fatalf("delete code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
if len(restarted.presets) != 0 || restarted.presetSlots != 5 {
|
|
t.Fatalf("delete changed slots/content: slots=%d presets=%+v", restarted.presetSlots, restarted.presets)
|
|
}
|
|
}
|
|
|
|
func TestPresetMetadataCanCreateEmptySlot(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
change := wire.AppendString(nil, 2, "备用")
|
|
change = wire.AppendVarint(change, 3, 2)
|
|
change = wire.AppendVarint(change, 4, 1)
|
|
change = wire.AppendVarint(change, 5, 4)
|
|
if code, _, _, err := f.deck.Handle("/PresetInfoChange", req(1, change)); err != nil || code != 278 {
|
|
t.Fatalf("upsert code=%d err=%v", code, err)
|
|
}
|
|
if got := f.deck.presets[4]; got.Name != "备用" || len(got.Decks) != 0 {
|
|
t.Fatalf("empty metadata preset=%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestPresetAddSlotChargesOncePersistsAndCaps(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
add := req(20, wire.AppendVarint(nil, 2, 1))
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
if code, _, _, err := f.deck.Handle("/PresetAddSlot", add); err != nil || code != 180 {
|
|
t.Fatalf("add attempt %d code=%d err=%v", attempt, code, err)
|
|
}
|
|
}
|
|
if f.deck.PresetSlotCount() != 6 || f.wallet.Snapshot().Gold != 18000 {
|
|
t.Fatalf("slots=%d gold=%d", f.deck.PresetSlotCount(), f.wallet.Snapshot().Gold)
|
|
}
|
|
restarted, err := OpenStore(f.storage, f.seed, testPresetDesign)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if restarted.PresetSlotCount() != 6 {
|
|
t.Fatalf("restarted slots=%d", restarted.PresetSlotCount())
|
|
}
|
|
if err := restarted.AttachPresetRuntime(f.wallet, f.characters, f.equipment, f.collection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted.BeginSession("cap")
|
|
if _, _, _, err := restarted.Handle("/PresetAddSlot", req(21, wire.AppendVarint(nil, 2, 6))); err != nil {
|
|
t.Fatalf("buy remaining slots: %v", err)
|
|
}
|
|
if _, _, _, err := restarted.Handle("/PresetAddSlot", req(22, wire.AppendVarint(nil, 2, 1))); err == nil {
|
|
t.Fatal("accepted slot beyond maximum")
|
|
}
|
|
}
|
|
|
|
func TestPresetUseAppliesDeckCostumeEquipmentWithoutChangingFieldDeck(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
wantField := append([]FieldEntry(nil), f.deck.state.FieldDeck...)
|
|
if _, _, _, err := f.deck.Handle("/PresetSave", req(1, presetRequest(0, 100, 1001, f.weapon.InvenIndex, "应用"))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
code, response, handled, err := f.deck.Handle("/PresetUse", req(2, wire.AppendVarint(nil, 3, 999999999)))
|
|
if err != nil || !handled || code != 409 {
|
|
t.Fatalf("use code=%d handled=%v err=%v", code, handled, err)
|
|
}
|
|
if len(f.deck.state.Deck) != 1 || f.deck.state.Deck[0].CharacterInvenIndex != 100 || !reflect.DeepEqual(f.deck.state.FieldDeck, wantField) {
|
|
t.Fatalf("deck=%+v field changed=%v", f.deck.state.Deck, !reflect.DeepEqual(f.deck.state.FieldDeck, wantField))
|
|
}
|
|
owned := f.equipment.All()
|
|
if len(owned) != 1 || owned[0].UseChar != 100 {
|
|
t.Fatalf("equipment=%+v", owned)
|
|
}
|
|
var deckCount, characterCount int
|
|
var equipmentValues []uint64
|
|
if err := wire.Walk(response, func(field wire.Field) error {
|
|
switch field.Number {
|
|
case 1:
|
|
deckCount++
|
|
case 2:
|
|
characterCount++
|
|
case 3:
|
|
return wire.Walk(field.Value, func(nested wire.Field) error {
|
|
if nested.Number == 2 {
|
|
values, err := repeatedUint64(nested)
|
|
equipmentValues = append(equipmentValues, values...)
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if deckCount != 1 || characterCount == 0 || !reflect.DeepEqual(equipmentValues, []uint64{f.weapon.InvenIndex, 0, 0, 0, 0}) {
|
|
t.Fatalf("response deck=%d chars=%d equipment=%v", deckCount, characterCount, equipmentValues)
|
|
}
|
|
// The client-supplied power is deliberately ignored; replay returns the
|
|
// exact same response and does not repeat cross-domain state transitions.
|
|
if replayCode, replay, _, replayErr := f.deck.Handle("/PresetUse", req(2)); replayErr != nil || replayCode != 409 || !reflect.DeepEqual(replay, response) {
|
|
t.Fatalf("replay code=%d equal=%v err=%v", replayCode, reflect.DeepEqual(replay, response), replayErr)
|
|
}
|
|
}
|
|
|
|
func TestPresetSaveRejectsForgedOwnershipAndEquipmentSlot(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
tests := []struct {
|
|
name string
|
|
character uint64
|
|
costume uint64
|
|
equipment uint64
|
|
}{
|
|
{name: "character", character: 999, costume: 1001, equipment: f.weapon.InvenIndex},
|
|
{name: "costume owner", character: 100, costume: 2001, equipment: f.weapon.InvenIndex},
|
|
{name: "equipment", character: 100, costume: 1001, equipment: 999999},
|
|
}
|
|
for seq, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, _, _, err := f.deck.Handle("/PresetSave", req(uint64(seq+1), presetRequest(0, test.character, test.costume, test.equipment, "伪造"))); err == nil {
|
|
t.Fatal("forged preset accepted")
|
|
}
|
|
})
|
|
}
|
|
if len(f.deck.presets) != 0 {
|
|
t.Fatalf("rejected save mutated presets=%+v", f.deck.presets)
|
|
}
|
|
}
|
|
|
|
func TestDeckCostumeSettingSentinelsClearAndRestart(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
setting := wire.AppendVarint(nil, 1, 100)
|
|
for _, item := range []struct {
|
|
index int64
|
|
burst uint64
|
|
}{{1001, 3}, {-1, 0}, {0, 0}} {
|
|
entry := wire.AppendVarint(nil, 1, uint64(item.index))
|
|
entry = wire.AppendVarint(entry, 2, item.burst)
|
|
setting = wire.AppendBytes(setting, 2, entry)
|
|
}
|
|
if code, _, _, err := f.deck.Handle("/DeckCostumeSettingSave", req(1, wire.AppendBytes(nil, 2, setting))); err != nil || code != 398 {
|
|
t.Fatalf("save setting code=%d err=%v", code, err)
|
|
}
|
|
restarted, err := OpenStore(f.storage, f.seed, testPresetDesign)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := restarted.AttachPresetRuntime(f.wallet, f.characters, f.equipment, f.collection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restarted.BeginSession("costume-restart")
|
|
code, response, _, err := restarted.Handle("/DeckCostumeSettingInfo", req(2))
|
|
if err != nil || code != 397 {
|
|
t.Fatalf("info setting code=%d err=%v", code, err)
|
|
}
|
|
var got []int64
|
|
if err := wire.Walk(response, func(field wire.Field) error {
|
|
if field.Number != 1 {
|
|
return nil
|
|
}
|
|
return wire.Walk(field.Value, func(nested wire.Field) error {
|
|
if nested.Number == 2 {
|
|
value, _, err := wire.Varint(nested.Value, 1)
|
|
got = append(got, int64(value))
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
}); err != nil || !reflect.DeepEqual(got, []int64{1001, -1, 0}) {
|
|
t.Fatalf("setting sequence=%v err=%v", got, err)
|
|
}
|
|
|
|
clear := wire.AppendVarint(nil, 1, 100)
|
|
if _, _, _, err := restarted.Handle("/DeckCostumeSettingSave", req(3, wire.AppendBytes(nil, 2, clear))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(restarted.costumeSettings[100].Sequence) != 0 {
|
|
t.Fatalf("setting was not cleared: %+v", restarted.costumeSettings[100])
|
|
}
|
|
}
|
|
|
|
func TestCostumeSettingInfoIsSorted(t *testing.T) {
|
|
f := newPresetFixture(t)
|
|
for seq, character := range []uint64{200, 100} {
|
|
setting := wire.AppendVarint(nil, 1, character)
|
|
if _, _, _, err := f.deck.Handle("/DeckCostumeSettingSave", req(uint64(seq+1), wire.AppendBytes(nil, 2, setting))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_, response, _, err := f.deck.Handle("/DeckCostumeSettingInfo", req(8))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var indices []uint64
|
|
_ = wire.Walk(response, func(field wire.Field) error {
|
|
if field.Number == 1 {
|
|
index, _, _ := wire.Varint(field.Value, 1)
|
|
indices = append(indices, index)
|
|
}
|
|
return nil
|
|
})
|
|
if !sort.SliceIsSorted(indices, func(i, j int) bool { return indices[i] < indices[j] }) || !reflect.DeepEqual(indices, []uint64{100, 200}) {
|
|
t.Fatalf("setting order=%v", indices)
|
|
}
|
|
}
|