feat: split client tooling and add OAuth server login
This commit is contained in:
@@ -0,0 +1,624 @@
|
||||
// Package deck owns local deck, field-party, waypoint, and selected-costume
|
||||
// state. It stores typed JSON, never captured protobuf/base64 envelopes.
|
||||
package deck
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/versionconfig"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
type DeckEntry struct {
|
||||
CharacterInvenIndex uint64 `json:"character_inven_index"`
|
||||
// CostumeInvenIndex is retained as the persisted Go/JSON name for the
|
||||
// development save format. On the wire DeckDBInfo field 2 is Position: a
|
||||
// zero-based battle-grid cell (or -1 while unassigned), not a costume
|
||||
// inventory index.
|
||||
CostumeInvenIndex uint64 `json:"costume_inven_index"`
|
||||
Slot uint64 `json:"slot"`
|
||||
}
|
||||
type FieldEntry struct {
|
||||
Slot uint64 `json:"slot"`
|
||||
CharacterInvenIndex uint64 `json:"character_inven_index"`
|
||||
CostumeInvenIndex uint64 `json:"costume_inven_index"`
|
||||
}
|
||||
type Seed struct {
|
||||
Version string `json:"version"`
|
||||
FieldDeck []FieldEntry `json:"field_deck"`
|
||||
FieldCharControlDeckType uint64 `json:"field_char_control_deck_type"`
|
||||
AutoReviveCatalyst uint64 `json:"auto_revive_catalyst,omitempty"`
|
||||
}
|
||||
type state struct {
|
||||
Version string `json:"version"`
|
||||
Deck []DeckEntry `json:"deck"`
|
||||
FieldDeck []FieldEntry `json:"field_deck"`
|
||||
FieldCharControlDeckType uint64 `json:"field_char_control_deck_type"`
|
||||
Waypoints map[uint64]uint64 `json:"waypoints"`
|
||||
Costumes map[uint64]uint64 `json:"costumes"`
|
||||
Packs map[uint64]uint64 `json:"packs"`
|
||||
HighestTotalBattlePower uint64 `json:"highest_total_battle_power"`
|
||||
PortraitCostumeID uint64 `json:"portrait_costume_id"`
|
||||
AutoReviveCatalyst uint64 `json:"auto_revive_catalyst"`
|
||||
}
|
||||
type Store struct {
|
||||
mu sync.RWMutex
|
||||
storage stateio.AtomicEntryStore
|
||||
state state
|
||||
presets map[uint64]Preset
|
||||
presetSlots uint64
|
||||
costumeSettings map[uint64]CostumeSetting
|
||||
wallet *player.Wallet
|
||||
characters *player.CharacterStore
|
||||
equipment *player.EquipmentInventory
|
||||
collection *player.CollectionStore
|
||||
sessionID string
|
||||
replies map[string]deckReply
|
||||
}
|
||||
|
||||
type deckReply struct {
|
||||
code int
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (s *Store) CurrentDeck() []DeckEntry {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return append([]DeckEntry(nil), s.state.Deck...)
|
||||
}
|
||||
|
||||
func LoadSeed(path string) (Seed, error) {
|
||||
b, e := os.ReadFile(path)
|
||||
if e != nil {
|
||||
return Seed{}, fmt.Errorf("deck: read seed: %w", e)
|
||||
}
|
||||
var s Seed
|
||||
if e = json.Unmarshal(b, &s); e != nil {
|
||||
return Seed{}, fmt.Errorf("deck: decode seed: %w", e)
|
||||
}
|
||||
if e = s.validate(); e != nil {
|
||||
return Seed{}, e
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
func (s Seed) validate() error {
|
||||
if s.Version != versionconfig.State() {
|
||||
return errors.New("deck: wrong seed version")
|
||||
}
|
||||
return validField(s.FieldDeck)
|
||||
}
|
||||
func validField(entries []FieldEntry) error {
|
||||
if len(entries) == 0 || len(entries) > 5 {
|
||||
return errors.New("deck: invalid field deck size")
|
||||
}
|
||||
characters := map[uint64]bool{}
|
||||
costumes := map[uint64]bool{}
|
||||
sequences := map[uint64]bool{}
|
||||
for _, e := range entries {
|
||||
if e.Slot == 0 || e.Slot > 5 || e.CharacterInvenIndex == 0 ||
|
||||
characters[e.CharacterInvenIndex] || sequences[e.Slot] ||
|
||||
(e.CostumeInvenIndex != 0 && costumes[e.CostumeInvenIndex]) {
|
||||
return errors.New("deck: invalid field deck")
|
||||
}
|
||||
characters[e.CharacterInvenIndex] = true
|
||||
sequences[e.Slot] = true
|
||||
if e.CostumeInvenIndex != 0 {
|
||||
costumes[e.CostumeInvenIndex] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validDeck(entries []DeckEntry) error {
|
||||
if len(entries) == 0 || len(entries) > 5 {
|
||||
return errors.New("deck: invalid battle deck size")
|
||||
}
|
||||
characters := map[uint64]bool{}
|
||||
positions := map[uint64]bool{}
|
||||
sequences := map[uint64]bool{}
|
||||
for _, entry := range entries {
|
||||
position := entry.CostumeInvenIndex
|
||||
unassigned := position == ^uint64(0) // int32 -1 sign-extends in protobuf varints.
|
||||
if entry.CharacterInvenIndex == 0 || (!unassigned && position > 11) || entry.Slot == 0 || entry.Slot > 5 ||
|
||||
characters[entry.CharacterInvenIndex] || (!unassigned && positions[position]) || sequences[entry.Slot] {
|
||||
return errors.New("deck: invalid battle deck")
|
||||
}
|
||||
characters[entry.CharacterInvenIndex] = true
|
||||
if !unassigned {
|
||||
positions[position] = true
|
||||
}
|
||||
sequences[entry.Slot] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func NewStore(seed Seed) (*Store, error) {
|
||||
if e := seed.validate(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return &Store{state: state{Version: versionconfig.State(), FieldDeck: append([]FieldEntry(nil), seed.FieldDeck...), FieldCharControlDeckType: seed.FieldCharControlDeckType, AutoReviveCatalyst: seed.AutoReviveCatalyst, Waypoints: map[uint64]uint64{}, Costumes: map[uint64]uint64{}, Packs: map[uint64]uint64{}}, presets: map[uint64]Preset{}, presetSlots: presetBaseCount, costumeSettings: map[uint64]CostumeSetting{}, replies: map[string]deckReply{}}, nil
|
||||
}
|
||||
func OpenStore(storage stateio.Store, seed Seed) (*Store, error) {
|
||||
s, e := NewStore(seed)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
entries, ok := storage.(stateio.AtomicEntryStore)
|
||||
if storage == nil || !ok {
|
||||
return nil, errors.New("deck: nil storage")
|
||||
}
|
||||
s.storage = entries
|
||||
b, e := storage.Load("deck")
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("deck: load state: %w", e)
|
||||
}
|
||||
if b == nil {
|
||||
if e = stateio.RequireNoEntries(entries, "deck", "presets", "preset_config", "costume_settings"); e != nil {
|
||||
return nil, fmt.Errorf("deck: invalid entry storage: %w", e)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
if e = stateio.RequireExactJSONObject(b, "version", "deck", "field_deck", "field_char_control_deck_type", "waypoints", "costumes", "packs", "highest_total_battle_power", "portrait_costume_id", "auto_revive_catalyst"); e != nil {
|
||||
return nil, fmt.Errorf("deck: incompatible state layout: %w", e)
|
||||
}
|
||||
var loaded state
|
||||
if e = json.Unmarshal(b, &loaded); e != nil {
|
||||
return nil, fmt.Errorf("deck: malformed state: %w", e)
|
||||
}
|
||||
if loaded.Version != versionconfig.State() || (len(loaded.Deck) != 0 && validDeck(loaded.Deck) != nil) || validField(loaded.FieldDeck) != nil || loaded.Waypoints == nil || loaded.Costumes == nil || loaded.Packs == nil {
|
||||
return nil, errors.New("deck: invalid saved state")
|
||||
}
|
||||
s.state = loaded
|
||||
if e = s.loadPresetEntries(); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
func (s *Store) EnsurePersisted() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
b, e := s.storage.Load("deck")
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if b != nil {
|
||||
return nil
|
||||
}
|
||||
return s.commit(clone(s.state))
|
||||
}
|
||||
func (s *Store) commit(next state) error {
|
||||
if s.storage != nil {
|
||||
b, e := json.MarshalIndent(next, "", " ")
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
if e = s.storage.Save("deck", append(b, '\n')); e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
s.state = next
|
||||
return nil
|
||||
}
|
||||
func clone(x state) state {
|
||||
y := x
|
||||
y.Deck = append([]DeckEntry(nil), x.Deck...)
|
||||
y.FieldDeck = append([]FieldEntry(nil), x.FieldDeck...)
|
||||
y.Waypoints = map[uint64]uint64{}
|
||||
for k, v := range x.Waypoints {
|
||||
y.Waypoints[k] = v
|
||||
}
|
||||
y.Costumes = map[uint64]uint64{}
|
||||
for k, v := range x.Costumes {
|
||||
y.Costumes[k] = v
|
||||
}
|
||||
y.Packs = map[uint64]uint64{}
|
||||
for k, v := range x.Packs {
|
||||
y.Packs[k] = v
|
||||
}
|
||||
return y
|
||||
}
|
||||
func checkSeq(req []byte) error {
|
||||
v, ok, e := wire.Varint(req, 1)
|
||||
if e != nil || !ok || v == 0 {
|
||||
return errors.New("deck: invalid request sequence")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func triples(req []byte) ([]DeckEntry, error) {
|
||||
var out []DeckEntry
|
||||
e := wire.Walk(req, func(f wire.Field) error {
|
||||
if f.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
if f.Type != 2 {
|
||||
return errors.New("deck: deck field")
|
||||
}
|
||||
a, aok, e := wire.Varint(f.Value, 1)
|
||||
if e != nil || !aok || a == 0 {
|
||||
return errors.New("deck: deck character")
|
||||
}
|
||||
position, _, e := wire.Varint(f.Value, 2)
|
||||
if e != nil || (position > 11 && position != ^uint64(0)) {
|
||||
return errors.New("deck: invalid deck position")
|
||||
}
|
||||
sequence, sequenceOK, e := wire.Varint(f.Value, 3)
|
||||
if e != nil || !sequenceOK || sequence == 0 || sequence > 5 {
|
||||
return errors.New("deck: invalid deck sequence")
|
||||
}
|
||||
out = append(out, DeckEntry{CharacterInvenIndex: a, CostumeInvenIndex: position, Slot: sequence})
|
||||
return nil
|
||||
})
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, errors.New("deck: empty deck")
|
||||
}
|
||||
if e := validDeck(out); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
func fieldEntries(req []byte) ([]FieldEntry, error) {
|
||||
var out []FieldEntry
|
||||
e := wire.Walk(req, func(f wire.Field) error {
|
||||
if f.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
if f.Type != 2 {
|
||||
return errors.New("deck: field deck entry is not a message")
|
||||
}
|
||||
slot, slotOK, err := wire.Varint(f.Value, 1)
|
||||
if err != nil || !slotOK || slot == 0 {
|
||||
return errors.New("deck: invalid field deck slot")
|
||||
}
|
||||
character, characterOK, err := wire.Varint(f.Value, 2)
|
||||
if err != nil || !characterOK || character == 0 {
|
||||
return errors.New("deck: invalid field deck character")
|
||||
}
|
||||
costume, _, err := wire.Varint(f.Value, 3)
|
||||
if err != nil {
|
||||
return errors.New("deck: invalid field deck costume")
|
||||
}
|
||||
out = append(out, FieldEntry{Slot: slot, CharacterInvenIndex: character, CostumeInvenIndex: costume})
|
||||
return nil
|
||||
})
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, errors.New("deck: empty field deck")
|
||||
}
|
||||
return out, validField(out)
|
||||
}
|
||||
|
||||
func (s *Store) validateOwnedDeckLocked(entries []DeckEntry) error {
|
||||
if s.characters == nil {
|
||||
return nil
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if _, found := s.characters.Find(entry.CharacterInvenIndex); !found {
|
||||
return fmt.Errorf("deck: battle deck references unknown character %d", entry.CharacterInvenIndex)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) validateOwnedFieldDeckLocked(entries []FieldEntry) error {
|
||||
if s.characters == nil || s.collection == nil {
|
||||
return nil
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if _, found := s.characters.Find(entry.CharacterInvenIndex); !found {
|
||||
return fmt.Errorf("deck: field deck references unknown character %d", entry.CharacterInvenIndex)
|
||||
}
|
||||
if entry.CostumeInvenIndex == 0 {
|
||||
continue
|
||||
}
|
||||
costume, found := s.collection.CostumeByIndex(entry.CostumeInvenIndex)
|
||||
if !found {
|
||||
return fmt.Errorf("deck: field deck references unknown costume %d", entry.CostumeInvenIndex)
|
||||
}
|
||||
if costume.UseChar != entry.CharacterInvenIndex {
|
||||
return fmt.Errorf("deck: costume %d does not belong to character %d", entry.CostumeInvenIndex, entry.CharacterInvenIndex)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle implements session.Handler. Every mutation validates its complete
|
||||
// typed request before committing a replacement JSON state.
|
||||
func (s *Store) Handle(path string, req []byte) (int, []byte, bool, error) {
|
||||
switch path {
|
||||
case "/PresetInfo":
|
||||
return s.handlePresetInfo(req)
|
||||
case "/PresetSave":
|
||||
return s.handlePresetSave(req)
|
||||
case "/PresetAddSlot":
|
||||
return s.handlePresetAddSlot(req)
|
||||
case "/PresetInfoChange":
|
||||
return s.handlePresetInfoChange(req)
|
||||
case "/PresetDelete":
|
||||
return s.handlePresetDelete(req)
|
||||
case "/PresetUse":
|
||||
return s.handlePresetUse(req)
|
||||
case "/DeckCostumeSettingInfo":
|
||||
return s.handleCostumeSettingInfo(req)
|
||||
case "/DeckCostumeSettingSave":
|
||||
return s.handleCostumeSettingSave(req)
|
||||
case "/DeckInfo":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
slog.Info("team trace: deliver saved battle deck", "deck", s.state.Deck)
|
||||
return 8, encodeDeck(s.state.Deck), true, nil
|
||||
case "/FieldDeckInfo":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return 273, encodeField(s.state.FieldDeck), true, nil
|
||||
case "/DeckCharAutoRevive":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
caster, _, e := wire.Varint(req, 2)
|
||||
if e != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: invalid auto-revive caster: %w", e)
|
||||
}
|
||||
if caster != 0 {
|
||||
// The observed story flow has no caster and only refreshes the
|
||||
// formation. A nonzero caster would require authoritative revive
|
||||
// targets, talent experience, and catalyst consumption.
|
||||
return 0, nil, true, fmt.Errorf("deck: auto-revive caster %d is not verified", caster)
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.state.Deck) == 0 {
|
||||
return 0, nil, true, errors.New("deck: auto-revive has no current battle deck")
|
||||
}
|
||||
var response []byte
|
||||
for _, entry := range s.state.Deck {
|
||||
deck := wire.AppendVarint(nil, 1, entry.CharacterInvenIndex)
|
||||
if entry.CostumeInvenIndex != 0 {
|
||||
deck = wire.AppendVarint(deck, 2, entry.CostumeInvenIndex)
|
||||
}
|
||||
deck = wire.AppendVarint(deck, 3, entry.Slot)
|
||||
response = wire.AppendBytes(response, 1, deck)
|
||||
}
|
||||
for _, entry := range s.state.FieldDeck {
|
||||
field := wire.AppendVarint(nil, 1, entry.Slot)
|
||||
field = wire.AppendVarint(field, 2, entry.CharacterInvenIndex)
|
||||
if entry.CostumeInvenIndex != 0 {
|
||||
field = wire.AppendVarint(field, 3, entry.CostumeInvenIndex)
|
||||
}
|
||||
response = wire.AppendBytes(response, 2, field)
|
||||
}
|
||||
response = wire.AppendVarint(response, 4, 2) // Define_AutoReviveCharType.CHANGE.
|
||||
if s.state.AutoReviveCatalyst != 0 {
|
||||
response = wire.AppendVarint(response, 7, s.state.AutoReviveCatalyst)
|
||||
}
|
||||
return 373, response, true, nil
|
||||
case "/WaypointInfo":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return 31, encodeWaypoints(s.state.Waypoints), true, nil
|
||||
case "/DeckSave":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
x, e := triples(req)
|
||||
if e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if e = s.validateOwnedDeckLocked(x); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
seq, _, _ := wire.Varint(req, 1)
|
||||
slog.Info("team trace: client requested battle deck replacement", "seq", seq, "before", s.state.Deck, "after", x)
|
||||
n := clone(s.state)
|
||||
n.Deck = x
|
||||
e = s.commit(n)
|
||||
if e != nil {
|
||||
slog.Error("team trace: deck replacement failed", "seq", seq, "error", e)
|
||||
}
|
||||
return 10, nil, true, e
|
||||
case "/FieldDeckSave":
|
||||
if e := checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
x, e := fieldEntries(req)
|
||||
if e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if e = s.validateOwnedFieldDeckLocked(x); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
n := clone(s.state)
|
||||
n.FieldDeck = x
|
||||
e = s.commit(n)
|
||||
return 274, nil, true, e
|
||||
case "/SaveFieldCharControlDeckType":
|
||||
// Define_FieldCharControllDeckType is a proto3 enum whose valid values
|
||||
// are BATTLE=0, FIELD=1 and STORY=2. BATTLE is the protobuf default, so
|
||||
// the generated client deliberately omits field 2 when it switches out
|
||||
// of story mode after the final quest. An absent field is therefore a
|
||||
// real value 0, not a malformed request.
|
||||
v, _, e := wire.Varint(req, 2)
|
||||
if e != nil || v > 2 {
|
||||
return 0, nil, true, errors.New("deck: invalid field control type")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
n.FieldCharControlDeckType = v
|
||||
e = s.commit(n)
|
||||
return 288, nil, true, e
|
||||
case "/WaypointSave":
|
||||
pack, ok, e := wire.Varint(req, 2)
|
||||
if e != nil || !ok || pack == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid waypoint pack")
|
||||
}
|
||||
way, ok, e := wire.Varint(req, 3)
|
||||
if e != nil || !ok || way == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid waypoint")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
n.Waypoints[pack] = way
|
||||
e = s.commit(n)
|
||||
return 32, nil, true, e
|
||||
case "/CostumeUse":
|
||||
raw, ok, e := wire.Bytes(req, 2)
|
||||
if e != nil || !ok {
|
||||
return 0, nil, true, errors.New("deck: invalid costume use")
|
||||
}
|
||||
cost, ok, e := wire.Varint(raw, 1)
|
||||
if e != nil || !ok || cost == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid costume")
|
||||
}
|
||||
char, ok, e := wire.Varint(raw, 2)
|
||||
if e != nil || !ok || char == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid costume character")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
n.Costumes[char] = cost
|
||||
e = s.commit(n)
|
||||
return 41, nil, true, e
|
||||
case "/PackBuy":
|
||||
pack, ok, e := wire.Varint(req, 2)
|
||||
if e != nil || !ok || pack == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid pack")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
// Purchasing the story pack is idempotent. Login/bootstrap may repeat
|
||||
// the request after a client restart before all local state is restored.
|
||||
n.Packs[pack] = 1
|
||||
e = s.commit(n)
|
||||
if e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
packInfo := wire.AppendVarint(nil, 1, pack)
|
||||
packInfo = wire.AppendVarint(packInfo, 8, n.Packs[pack])
|
||||
response := wire.AppendBytes(nil, 1, packInfo)
|
||||
// Starter pack purchase grants the local story-pack entitlement and
|
||||
// starter currency. These are semantic RewardDBInfoBundle fields, not
|
||||
// recorded response bytes.
|
||||
entitlement := wire.AppendVarint(nil, 2, 1)
|
||||
entitlement = wire.AppendVarint(entitlement, 3, 19)
|
||||
entitlement = wire.AppendVarint(entitlement, 4, 1)
|
||||
entitlement = wire.AppendVarint(entitlement, 8, ^uint64(0)-32400000+1)
|
||||
currency := wire.AppendVarint(nil, 3, 12)
|
||||
currency = wire.AppendVarint(currency, 4, 200)
|
||||
tracking := wire.AppendVarint(nil, 2, 1)
|
||||
tracking = wire.AppendVarint(tracking, 3, 19)
|
||||
tracking = wire.AppendVarint(tracking, 4, 1)
|
||||
bundle := wire.AppendBytes(nil, 1, entitlement)
|
||||
bundle = wire.AppendBytes(bundle, 1, currency)
|
||||
bundle = wire.AppendBytes(bundle, 6, tracking)
|
||||
return 6, wire.AppendBytes(response, 2, bundle), true, nil
|
||||
case "/SaveTotalBattlePower":
|
||||
power, ok, e := wire.Varint(req, 2)
|
||||
if e != nil || !ok || power == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid total battle power")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
if power > n.HighestTotalBattlePower {
|
||||
n.HighestTotalBattlePower = power
|
||||
}
|
||||
if e = s.commit(n); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
return 258, wire.AppendVarint(nil, 1, n.HighestTotalBattlePower), true, nil
|
||||
case "/UserPortraitChange":
|
||||
costumeID, ok, e := wire.Varint(req, 2)
|
||||
if e != nil || !ok || costumeID == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid portrait costume")
|
||||
}
|
||||
if e = checkSeq(req); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
n := clone(s.state)
|
||||
n.PortraitCostumeID = costumeID
|
||||
if e = s.commit(n); e != nil {
|
||||
return 0, nil, true, e
|
||||
}
|
||||
return 75, wire.AppendVarint(nil, 1, costumeID), true, nil
|
||||
}
|
||||
return 0, nil, false, nil
|
||||
}
|
||||
func encodeDeck(xs []DeckEntry) []byte {
|
||||
var b []byte
|
||||
for _, x := range xs {
|
||||
v := wire.AppendVarint(nil, 1, x.CharacterInvenIndex)
|
||||
v = wire.AppendVarint(v, 2, x.CostumeInvenIndex)
|
||||
v = wire.AppendVarint(v, 3, x.Slot)
|
||||
b = wire.AppendBytes(b, 1, v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
func encodeField(xs []FieldEntry) []byte {
|
||||
var b []byte
|
||||
for _, x := range xs {
|
||||
v := wire.AppendVarint(nil, 1, x.Slot)
|
||||
v = wire.AppendVarint(v, 2, x.CharacterInvenIndex)
|
||||
v = wire.AppendVarint(v, 3, x.CostumeInvenIndex)
|
||||
b = wire.AppendBytes(b, 1, v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
func encodeWaypoints(xs map[uint64]uint64) []byte {
|
||||
keys := make([]uint64, 0, len(xs))
|
||||
for k := range xs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
||||
var b []byte
|
||||
for _, k := range keys {
|
||||
v := wire.AppendVarint(nil, 1, k)
|
||||
v = wire.AppendVarint(v, 2, xs[k])
|
||||
b = wire.AppendBytes(b, 1, v)
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package deck
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/versionconfig"
|
||||
"bd2server/internal/server/wire"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func seeded(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
x, e := LoadSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "decks.json"))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
s, e := NewStore(x)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
return s
|
||||
}
|
||||
func req(seq uint64, fields ...[]byte) []byte {
|
||||
b := wire.AppendVarint(nil, 1, seq)
|
||||
for _, f := range fields {
|
||||
b = append(b, f...)
|
||||
}
|
||||
return b
|
||||
}
|
||||
func triple(a, b, c uint64) []byte {
|
||||
v := wire.AppendVarint(nil, 1, a)
|
||||
v = wire.AppendVarint(v, 2, b)
|
||||
v = wire.AppendVarint(v, 3, c)
|
||||
return wire.AppendBytes(nil, 2, v)
|
||||
}
|
||||
|
||||
func attachFormationOwnership(t *testing.T, store *Store) {
|
||||
t.Helper()
|
||||
starter := &player.Starter{
|
||||
Version: versionconfig.State(),
|
||||
Characters: []player.Character{
|
||||
{InvenIndex: 101, ID: 350, Level: 1},
|
||||
{InvenIndex: 102, ID: 351, Level: 1},
|
||||
{InvenIndex: 103, ID: 352, Level: 1},
|
||||
{InvenIndex: 104, ID: 353, Level: 1},
|
||||
{InvenIndex: 105, ID: 354, Level: 1},
|
||||
},
|
||||
Costumes: []player.Costume{
|
||||
{InvenIndex: 201, ID: 60101, UseChar: 101},
|
||||
{InvenIndex: 202, ID: 60201, UseChar: 102},
|
||||
{InvenIndex: 203, ID: 60301, UseChar: 103},
|
||||
{InvenIndex: 204, ID: 60401, UseChar: 104},
|
||||
{InvenIndex: 205, ID: 60501, UseChar: 105},
|
||||
},
|
||||
}
|
||||
inventory, err := player.OpenInventory(stateio.NewMemory(), starter)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
characters, err := player.OpenCharacterStore(stateio.NewMemory(), starter.Characters, inventory, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
collection, err := player.OpenCollectionStore(stateio.NewMemory(), starter.Costumes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.characters = characters
|
||||
store.collection = collection
|
||||
}
|
||||
func TestFieldDeckSeedAndSave(t *testing.T) {
|
||||
s := seeded(t)
|
||||
code, b, ok, e := s.Handle("/FieldDeckInfo", req(1))
|
||||
if e != nil || !ok || code != 273 {
|
||||
t.Fatalf("info %d %t %v", code, ok, e)
|
||||
}
|
||||
n := 0
|
||||
if e = wire.Walk(b, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
n++
|
||||
}
|
||||
return nil
|
||||
}); e != nil || n != 5 {
|
||||
t.Fatalf("seed field deck: %d %v", n, e)
|
||||
}
|
||||
// FieldDeckDBInfo is sequence#1, character#2, costume#3.
|
||||
field := triple(1, 99, 199)
|
||||
code, _, ok, e = s.Handle("/FieldDeckSave", req(2, field))
|
||||
if e != nil || !ok || code != 274 {
|
||||
t.Fatalf("save: %d %t %v", code, ok, e)
|
||||
}
|
||||
_, b, _, _ = s.Handle("/FieldDeckInfo", req(3))
|
||||
var got uint64
|
||||
_ = wire.Walk(b, func(f wire.Field) error {
|
||||
if f.Number == 1 {
|
||||
got, _, _ = wire.Varint(f.Value, 2)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if got != 99 {
|
||||
t.Fatalf("saved char=%d", got)
|
||||
}
|
||||
}
|
||||
func TestDeckPersistenceAndCommands(t *testing.T) {
|
||||
seed, e := LoadSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "decks.json"))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
storage := stateio.NewMemory()
|
||||
s, e := OpenStore(storage, seed)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
code, _, _, e := s.Handle("/DeckSave", req(1, triple(100, 2, 1)))
|
||||
if e != nil || code != 10 {
|
||||
t.Fatalf("deck save: %d %v", code, e)
|
||||
}
|
||||
way := wire.AppendVarint(nil, 2, 21)
|
||||
way = wire.AppendVarint(way, 3, 1)
|
||||
if code, _, _, e = s.Handle("/WaypointSave", req(2, way)); e != nil || code != 32 {
|
||||
t.Fatalf("way: %d %v", code, e)
|
||||
}
|
||||
use := wire.AppendVarint(nil, 1, 200)
|
||||
use = wire.AppendVarint(use, 2, 100)
|
||||
if code, _, _, e = s.Handle("/CostumeUse", req(3, wire.AppendBytes(nil, 2, use))); e != nil || code != 41 {
|
||||
t.Fatalf("use: %d %v", code, e)
|
||||
}
|
||||
pack := wire.AppendVarint(nil, 2, 21)
|
||||
code, b, _, e := s.Handle("/PackBuy", req(4, pack))
|
||||
if e != nil || code != 6 {
|
||||
t.Fatalf("buy: %d %v", code, e)
|
||||
}
|
||||
p, _, _ := wire.Bytes(b, 1)
|
||||
id, _, _ := wire.Varint(p, 1)
|
||||
if id != 21 {
|
||||
t.Fatalf("pack response=%d", id)
|
||||
}
|
||||
reopened, e := OpenStore(storage, seed)
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
_, b, _, e = reopened.Handle("/DeckInfo", req(5))
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
entry, _, _ := wire.Bytes(b, 1)
|
||||
id, _, _ = wire.Varint(entry, 1)
|
||||
if id != 100 {
|
||||
t.Fatalf("persist deck=%d", id)
|
||||
}
|
||||
}
|
||||
func TestRejectsInvalidMutations(t *testing.T) {
|
||||
s := seeded(t)
|
||||
if _, _, _, e := s.Handle("/DeckSave", req(1, triple(1, 2, 1), triple(3, 4, 1))); e == nil {
|
||||
t.Fatal("duplicate slots accepted")
|
||||
}
|
||||
if _, _, _, e := s.Handle("/WaypointSave", req(1, wire.AppendVarint(nil, 2, 21))); e == nil {
|
||||
t.Fatal("waypoint missing id accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeckSaveValidatesFormationShapeAndOwnership(t *testing.T) {
|
||||
valid := [][]byte{
|
||||
triple(101, 0, 1), triple(102, 1, 2), triple(103, 2, 3),
|
||||
triple(104, 3, 4), triple(105, 4, 5),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
entries [][]byte
|
||||
}{
|
||||
{name: "six characters", entries: append(append([][]byte{}, valid...), triple(101, 5, 1))},
|
||||
{name: "duplicate character", entries: [][]byte{triple(101, 0, 1), triple(101, 1, 2)}},
|
||||
{name: "duplicate position", entries: [][]byte{triple(101, 0, 1), triple(102, 0, 2)}},
|
||||
{name: "duplicate sequence", entries: [][]byte{triple(101, 0, 1), triple(102, 1, 1)}},
|
||||
{name: "position outside twelve cells", entries: [][]byte{triple(101, 12, 1)}},
|
||||
{name: "sequence outside formation", entries: [][]byte{triple(101, 0, 6)}},
|
||||
{name: "unknown character", entries: [][]byte{triple(999, 0, 1)}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := seeded(t)
|
||||
attachFormationOwnership(t, s)
|
||||
if _, _, handled, err := s.Handle("/DeckSave", req(1, test.entries...)); err == nil || !handled {
|
||||
t.Fatalf("invalid deck accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
if len(s.state.Deck) != 0 {
|
||||
t.Fatalf("invalid deck mutated state: %+v", s.state.Deck)
|
||||
}
|
||||
})
|
||||
}
|
||||
s := seeded(t)
|
||||
attachFormationOwnership(t, s)
|
||||
if code, _, handled, err := s.Handle("/DeckSave", req(2, valid...)); err != nil || !handled || code != 10 || len(s.state.Deck) != 5 {
|
||||
t.Fatalf("valid deck rejected code=%d handled=%v deck=%+v err=%v", code, handled, s.state.Deck, err)
|
||||
}
|
||||
unassigned := [][]byte{
|
||||
triple(101, ^uint64(0), 1), triple(102, ^uint64(0), 2), triple(103, ^uint64(0), 3),
|
||||
triple(104, ^uint64(0), 4), triple(105, ^uint64(0), 5),
|
||||
}
|
||||
if code, _, handled, err := s.Handle("/DeckSave", req(3, unassigned...)); err != nil || !handled || code != 10 {
|
||||
t.Fatalf("official unassigned positions rejected code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFieldDeckSaveValidatesFormationShapeAndOwnership(t *testing.T) {
|
||||
valid := [][]byte{
|
||||
triple(1, 101, 201), triple(2, 102, 202), triple(3, 103, 203),
|
||||
triple(4, 104, 204), triple(5, 105, 205),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
entries [][]byte
|
||||
}{
|
||||
{name: "six characters", entries: append(append([][]byte{}, valid...), triple(1, 101, 201))},
|
||||
{name: "duplicate character", entries: [][]byte{triple(1, 101, 201), triple(2, 101, 202)}},
|
||||
{name: "duplicate costume", entries: [][]byte{triple(1, 101, 201), triple(2, 102, 201)}},
|
||||
{name: "duplicate sequence", entries: [][]byte{triple(1, 101, 201), triple(1, 102, 202)}},
|
||||
{name: "sequence outside formation", entries: [][]byte{triple(6, 101, 201)}},
|
||||
{name: "unknown character", entries: [][]byte{triple(1, 999, 201)}},
|
||||
{name: "unknown costume", entries: [][]byte{triple(1, 101, 999)}},
|
||||
{name: "costume owned by another character", entries: [][]byte{triple(1, 101, 202)}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
s := seeded(t)
|
||||
attachFormationOwnership(t, s)
|
||||
before := append([]FieldEntry(nil), s.state.FieldDeck...)
|
||||
if _, _, handled, err := s.Handle("/FieldDeckSave", req(1, test.entries...)); err == nil || !handled {
|
||||
t.Fatalf("invalid field deck accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
if len(s.state.FieldDeck) != len(before) || s.state.FieldDeck[0] != before[0] {
|
||||
t.Fatalf("invalid field deck mutated state: %+v", s.state.FieldDeck)
|
||||
}
|
||||
})
|
||||
}
|
||||
s := seeded(t)
|
||||
attachFormationOwnership(t, s)
|
||||
if code, _, handled, err := s.Handle("/FieldDeckSave", req(2, valid...)); err != nil || !handled || code != 274 || len(s.state.FieldDeck) != 5 {
|
||||
t.Fatalf("valid field deck rejected code=%d handled=%v deck=%+v err=%v", code, handled, s.state.FieldDeck, err)
|
||||
}
|
||||
// A zero costume index means to use the character's current costume; the
|
||||
// client explicitly supports this fallback when resolving its field leader.
|
||||
if code, _, handled, err := s.Handle("/FieldDeckSave", req(3, triple(1, 101, 0))); err != nil || !handled || code != 274 {
|
||||
t.Fatalf("current-costume fallback rejected code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeckSavesRequireRequestSequence(t *testing.T) {
|
||||
s := seeded(t)
|
||||
if _, _, handled, err := s.Handle("/DeckSave", triple(101, 0, 1)); err == nil || !handled {
|
||||
t.Fatalf("DeckSave without seq accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
if _, _, handled, err := s.Handle("/FieldDeckSave", triple(1, 101, 201)); err == nil || !handled {
|
||||
t.Fatalf("FieldDeckSave without seq accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveFieldCharControlDeckTypeAcceptsProtoDefaultBattle(t *testing.T) {
|
||||
s := seeded(t)
|
||||
// FCCD_BATTLE=0 is omitted by proto3 serialization. This is the exact
|
||||
// shape sent when the client leaves story control after the final quest.
|
||||
code, _, handled, err := s.Handle("/SaveFieldCharControlDeckType", req(1))
|
||||
if err != nil || !handled || code != 288 || s.state.FieldCharControlDeckType != 0 {
|
||||
t.Fatalf("battle save code=%d handled=%v type=%d err=%v", code, handled, s.state.FieldCharControlDeckType, err)
|
||||
}
|
||||
for seq, value := range []uint64{1, 2} {
|
||||
request := req(uint64(seq+2), wire.AppendVarint(nil, 2, value))
|
||||
if code, _, handled, err = s.Handle("/SaveFieldCharControlDeckType", request); err != nil || !handled || code != 288 || s.state.FieldCharControlDeckType != value {
|
||||
t.Fatalf("type %d save code=%d handled=%v stored=%d err=%v", value, code, handled, s.state.FieldCharControlDeckType, err)
|
||||
}
|
||||
}
|
||||
if _, _, handled, err = s.Handle("/SaveFieldCharControlDeckType", req(9, wire.AppendVarint(nil, 2, 3))); !handled || err == nil {
|
||||
t.Fatalf("unknown enum accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalBattlePowerKeepsHighest(t *testing.T) {
|
||||
s := seeded(t)
|
||||
for seq, power := range []uint64{1300, 900} {
|
||||
request := req(uint64(seq+1), wire.AppendVarint(nil, 2, power))
|
||||
code, response, ok, err := s.Handle("/SaveTotalBattlePower", request)
|
||||
if err != nil || !ok || code != 258 {
|
||||
t.Fatalf("save power: code=%d ok=%v err=%v", code, ok, err)
|
||||
}
|
||||
highest, found, err := wire.Varint(response, 1)
|
||||
if err != nil || !found || highest != 1300 {
|
||||
t.Fatalf("highest=%d found=%v err=%v", highest, found, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortraitChangeEchoesAndStoresCostume(t *testing.T) {
|
||||
s := seeded(t)
|
||||
request := req(1, wire.AppendVarint(nil, 2, 3501))
|
||||
code, response, ok, err := s.Handle("/UserPortraitChange", request)
|
||||
if err != nil || !ok || code != 75 {
|
||||
t.Fatalf("portrait: code=%d ok=%v err=%v", code, ok, err)
|
||||
}
|
||||
id, found, err := wire.Varint(response, 1)
|
||||
if err != nil || !found || id != 3501 || s.state.PortraitCostumeID != 3501 {
|
||||
t.Fatalf("portrait response=%d found=%v stored=%d err=%v", id, found, s.state.PortraitCostumeID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeckCharAutoReviveUsesCurrentFormationWithoutInventingRevives(t *testing.T) {
|
||||
s := seeded(t)
|
||||
if code, _, handled, err := s.Handle("/DeckSave", req(1, triple(535607162, 10, 1), triple(535604120, 1, 2))); code != 10 || !handled || err != nil {
|
||||
t.Fatalf("save code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
code, response, handled, err := s.Handle("/DeckCharAutoRevive", req(2))
|
||||
if err != nil || !handled || code != 373 {
|
||||
t.Fatalf("auto revive code=%d handled=%v err=%v", code, handled, err)
|
||||
}
|
||||
var battle, field, revive int
|
||||
if err := wire.Walk(response, func(f wire.Field) error {
|
||||
switch f.Number {
|
||||
case 1:
|
||||
battle++
|
||||
if battle == 1 {
|
||||
index, _, _ := wire.Varint(f.Value, 1)
|
||||
if index != 535607162 {
|
||||
t.Errorf("first battle character %d", index)
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
field++
|
||||
case 3, 5:
|
||||
revive++
|
||||
}
|
||||
return nil
|
||||
}); err != nil || battle != 2 || field != 5 || revive != 0 {
|
||||
t.Fatalf("formation battle=%d field=%d invented revive=%d err=%v", battle, field, revive, err)
|
||||
}
|
||||
mode, _, _ := wire.Varint(response, 4)
|
||||
catalyst, _, _ := wire.Varint(response, 7)
|
||||
if mode != 2 || catalyst != 200 {
|
||||
t.Fatalf("official formation mode=%d catalyst=%d", mode, catalyst)
|
||||
}
|
||||
if _, _, handled, err := s.Handle("/DeckCharAutoRevive", req(3, wire.AppendVarint(nil, 2, 535604120))); !handled || err == nil {
|
||||
t.Fatalf("unverified caster incorrectly accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
if _, _, handled, err := s.Handle("/DeckCharAutoRevive", nil); !handled || err == nil {
|
||||
t.Fatalf("missing sequence incorrectly accepted handled=%v err=%v", handled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeckCharAutoRevivePreservesExplicitZeroCatalyst(t *testing.T) {
|
||||
seed, err := LoadSeed(filepath.Join("..", "..", "..", "seed", "v2_35_10", "decks.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
storage := stateio.NewMemory()
|
||||
s, err := OpenStore(storage, seed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, err := s.Handle("/DeckSave", req(1, triple(88, 11, 1))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.state.AutoReviveCatalyst = 0
|
||||
if err := s.commit(s.state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reopened, err := OpenStore(storage, seed)
|
||||
if err != nil || reopened.state.AutoReviveCatalyst != 0 || reopened.state.Deck[0].CharacterInvenIndex != 88 {
|
||||
t.Fatalf("reloaded state=%+v err=%v", reopened.state, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
package deck
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
const (
|
||||
presetBaseCount = 5
|
||||
presetMaxCount = 12
|
||||
presetSlotPrice = 2000
|
||||
)
|
||||
|
||||
type Preset struct {
|
||||
Name string `json:"name"`
|
||||
ResourceID uint64 `json:"resource_id"`
|
||||
ResourceColor uint64 `json:"resource_color"`
|
||||
Slot uint64 `json:"slot"`
|
||||
Decks []PresetDeck `json:"decks"`
|
||||
Blesses []PresetBless `json:"blesses"`
|
||||
}
|
||||
|
||||
type PresetDeck struct {
|
||||
Deck DeckEntry `json:"deck"`
|
||||
CostumeIndex uint64 `json:"costume_index"`
|
||||
Equipment []PresetEquipmentItem `json:"equipment"`
|
||||
Team uint64 `json:"team"`
|
||||
}
|
||||
|
||||
type PresetEquipmentItem struct {
|
||||
Type uint64 `json:"type"`
|
||||
Index uint64 `json:"index"`
|
||||
}
|
||||
|
||||
type PresetBless struct {
|
||||
DeckType uint64 `json:"deck_type"`
|
||||
IDs []uint64 `json:"ids"`
|
||||
}
|
||||
|
||||
type CostumeSetting struct {
|
||||
CharacterIndex uint64 `json:"character_index"`
|
||||
Sequence []CostumeSettingItem `json:"sequence"`
|
||||
BattleMode uint64 `json:"battle_mode"`
|
||||
MonsterID uint64 `json:"monster_id"`
|
||||
}
|
||||
|
||||
type CostumeSettingItem struct {
|
||||
CostumeIndex int64 `json:"costume_index"`
|
||||
BurstLevel uint64 `json:"burst_level"`
|
||||
}
|
||||
|
||||
func (s *Store) AttachPresetRuntime(wallet *player.Wallet, characters *player.CharacterStore, equipment *player.EquipmentInventory, collection *player.CollectionStore) error {
|
||||
if wallet == nil || characters == nil || equipment == nil || collection == nil {
|
||||
return errors.New("deck: incomplete preset runtime")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.wallet, s.characters, s.equipment, s.collection = wallet, characters, equipment, collection
|
||||
return s.validatePresetOwnershipLocked()
|
||||
}
|
||||
|
||||
func (s *Store) BeginSession(id string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.sessionID = id
|
||||
s.replies = map[string]deckReply{}
|
||||
}
|
||||
|
||||
func (s *Store) PresetSlotCount() uint64 {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.presetSlots
|
||||
}
|
||||
|
||||
func (s *Store) loadPresetEntries() error {
|
||||
rawConfig, found, err := s.storage.LoadEntry("deck", "preset_config", "slots")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
if err := json.Unmarshal(rawConfig, &s.presetSlots); err != nil || s.presetSlots < presetBaseCount || s.presetSlots > presetMaxCount {
|
||||
return errors.New("deck: invalid preset slot configuration")
|
||||
}
|
||||
}
|
||||
raw, err := s.storage.ListEntries("deck", "presets")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, payload := range raw {
|
||||
slot, err := strconv.ParseUint(key, 10, 64)
|
||||
if err != nil || key != strconv.FormatUint(slot, 10) {
|
||||
return fmt.Errorf("deck: invalid preset key %q", key)
|
||||
}
|
||||
var preset Preset
|
||||
if err := json.Unmarshal(payload, &preset); err != nil || preset.Slot != slot {
|
||||
return fmt.Errorf("deck: invalid preset %q", key)
|
||||
}
|
||||
if err := validatePresetShape(preset, s.presetSlots); err != nil {
|
||||
return fmt.Errorf("deck: invalid preset %q: %w", key, err)
|
||||
}
|
||||
s.presets[slot] = preset
|
||||
}
|
||||
rawSettings, err := s.storage.ListEntries("deck", "costume_settings")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for key, payload := range rawSettings {
|
||||
index, err := strconv.ParseUint(key, 10, 64)
|
||||
if err != nil || index == 0 || key != strconv.FormatUint(index, 10) {
|
||||
return fmt.Errorf("deck: invalid costume setting key %q", key)
|
||||
}
|
||||
var setting CostumeSetting
|
||||
if err := json.Unmarshal(payload, &setting); err != nil || setting.CharacterIndex != index {
|
||||
return fmt.Errorf("deck: invalid costume setting %q", key)
|
||||
}
|
||||
s.costumeSettings[index] = setting
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) validatePresetOwnershipLocked() error {
|
||||
if s.characters == nil || s.collection == nil || s.equipment == nil {
|
||||
return nil
|
||||
}
|
||||
for _, preset := range s.presets {
|
||||
if err := s.validatePresetOwnedLocked(preset); err != nil {
|
||||
return fmt.Errorf("deck: saved preset %d: %w", preset.Slot, err)
|
||||
}
|
||||
}
|
||||
for _, setting := range s.costumeSettings {
|
||||
if _, found := s.characters.Find(setting.CharacterIndex); !found {
|
||||
return fmt.Errorf("deck: costume setting references unknown character %d", setting.CharacterIndex)
|
||||
}
|
||||
for _, item := range setting.Sequence {
|
||||
if item.CostumeIndex > 0 {
|
||||
if _, found := s.collection.CostumeByIndex(uint64(item.CostumeIndex)); !found {
|
||||
return fmt.Errorf("deck: costume setting references unknown costume %d", item.CostumeIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePresetShape(p Preset, slotCount uint64) error {
|
||||
if p.Slot >= slotCount || p.ResourceID > 21 || p.ResourceColor > 5 || !validPresetName(p.Name) || len(p.Decks) > 5 {
|
||||
return errors.New("invalid metadata or deck count")
|
||||
}
|
||||
characters, positions, sequences := map[uint64]bool{}, map[uint64]bool{}, map[uint64]bool{}
|
||||
for _, deck := range p.Decks {
|
||||
if deck.Deck.CharacterInvenIndex == 0 || deck.Deck.CostumeInvenIndex > 11 || deck.Deck.Slot == 0 || deck.Deck.Slot > 5 || deck.Team != 0 ||
|
||||
characters[deck.Deck.CharacterInvenIndex] || positions[deck.Deck.CostumeInvenIndex] || sequences[deck.Deck.Slot] {
|
||||
return errors.New("invalid deck entry")
|
||||
}
|
||||
characters[deck.Deck.CharacterInvenIndex] = true
|
||||
positions[deck.Deck.CostumeInvenIndex] = true
|
||||
sequences[deck.Deck.Slot] = true
|
||||
if len(deck.Equipment) != 5 {
|
||||
return errors.New("preset deck requires five equipment slots")
|
||||
}
|
||||
seen := map[uint64]bool{}
|
||||
for _, item := range deck.Equipment {
|
||||
if item.Type >= 5 || seen[item.Type] {
|
||||
return errors.New("invalid preset equipment slot")
|
||||
}
|
||||
seen[item.Type] = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validPresetName(value string) bool {
|
||||
if !utf8.ValidString(value) || utf8.RuneCountInString(value) < 1 || utf8.RuneCountInString(value) > 16 || strings.Contains(value, "<") || strings.Contains(value, ">") {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if unicode.IsControl(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Store) validatePresetOwnedLocked(p Preset) error {
|
||||
if err := validatePresetShape(p, s.presetSlots); err != nil {
|
||||
return err
|
||||
}
|
||||
ownedEquipment := make(map[uint64]player.Equipment)
|
||||
for _, item := range s.equipment.All() {
|
||||
ownedEquipment[item.InvenIndex] = item
|
||||
}
|
||||
seenEquipment := map[uint64]bool{}
|
||||
for _, deck := range p.Decks {
|
||||
if _, found := s.characters.Find(deck.Deck.CharacterInvenIndex); !found {
|
||||
return fmt.Errorf("unknown character %d", deck.Deck.CharacterInvenIndex)
|
||||
}
|
||||
if deck.CostumeIndex != 0 {
|
||||
costume, found := s.collection.CostumeByIndex(deck.CostumeIndex)
|
||||
if !found || costume.UseChar != deck.Deck.CharacterInvenIndex {
|
||||
return fmt.Errorf("unknown costume %d", deck.CostumeIndex)
|
||||
}
|
||||
}
|
||||
for _, reference := range deck.Equipment {
|
||||
if reference.Index == 0 {
|
||||
continue
|
||||
}
|
||||
if seenEquipment[reference.Index] || ownedEquipment[reference.Index].InvenIndex == 0 {
|
||||
return fmt.Errorf("invalid or repeated equipment %d", reference.Index)
|
||||
}
|
||||
seenEquipment[reference.Index] = true
|
||||
}
|
||||
binding := player.PresetEquipmentBinding{CharacterIndex: deck.Deck.CharacterInvenIndex, Equipment: make([]uint64, 5)}
|
||||
for _, reference := range deck.Equipment {
|
||||
binding.Equipment[reference.Type] = reference.Index
|
||||
}
|
||||
if err := s.equipment.ValidatePresetEquipment([]player.PresetEquipmentBinding{binding}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodePreset(data []byte) (Preset, error) {
|
||||
var p Preset
|
||||
name, _, err := wire.Bytes(data, 1)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.Name = string(name)
|
||||
p.ResourceID, _, err = wire.Varint(data, 2)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ResourceColor, _, err = wire.Varint(data, 3)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.Slot, _, err = wire.Varint(data, 4)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
err = wire.Walk(data, func(field wire.Field) error {
|
||||
if field.Type != 2 {
|
||||
return nil
|
||||
}
|
||||
switch field.Number {
|
||||
case 5:
|
||||
deck, err := decodePresetDeck(field.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Decks = append(p.Decks, deck)
|
||||
case 6:
|
||||
bless, err := decodePresetBless(field.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Blesses = append(p.Blesses, bless)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return p, err
|
||||
}
|
||||
|
||||
func decodePresetDeck(data []byte) (PresetDeck, error) {
|
||||
var result PresetDeck
|
||||
base, found, err := wire.Bytes(data, 1)
|
||||
if err != nil || !found {
|
||||
return result, errors.New("deck: preset missing deck base")
|
||||
}
|
||||
character, ok, err := wire.Varint(base, 1)
|
||||
if err != nil || !ok || character == 0 {
|
||||
return result, errors.New("deck: invalid preset character")
|
||||
}
|
||||
position, _, err := wire.Varint(base, 2)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
sequence, ok, err := wire.Varint(base, 3)
|
||||
if err != nil || !ok || sequence == 0 {
|
||||
return result, errors.New("deck: invalid preset sequence")
|
||||
}
|
||||
result.Deck = DeckEntry{CharacterInvenIndex: character, CostumeInvenIndex: position, Slot: sequence}
|
||||
result.CostumeIndex, _, err = wire.Varint(data, 2)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.Team, _, err = wire.Varint(data, 4)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
err = wire.Walk(data, func(field wire.Field) error {
|
||||
if field.Number != 3 {
|
||||
return nil
|
||||
}
|
||||
if field.Type != 2 {
|
||||
return errors.New("deck: invalid preset equipment")
|
||||
}
|
||||
typeID, _, err := wire.Varint(field.Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, _, err := wire.Varint(field.Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Equipment = append(result.Equipment, PresetEquipmentItem{Type: typeID, Index: index})
|
||||
return nil
|
||||
})
|
||||
sort.Slice(result.Equipment, func(i, j int) bool { return result.Equipment[i].Type < result.Equipment[j].Type })
|
||||
return result, err
|
||||
}
|
||||
|
||||
func decodePresetBless(data []byte) (PresetBless, error) {
|
||||
var result PresetBless
|
||||
result.DeckType, _, _ = wire.Varint(data, 1)
|
||||
err := wire.Walk(data, func(field wire.Field) error {
|
||||
if field.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
values, err := repeatedUint64(field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.IDs = append(result.IDs, values...)
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func repeatedUint64(field wire.Field) ([]uint64, error) {
|
||||
if field.Type == 0 {
|
||||
value, count := binary.Uvarint(field.Value)
|
||||
if count <= 0 {
|
||||
return nil, wire.ErrMalformed
|
||||
}
|
||||
return []uint64{value}, nil
|
||||
}
|
||||
if field.Type != 2 {
|
||||
return nil, wire.ErrMalformed
|
||||
}
|
||||
var out []uint64
|
||||
for offset := 0; offset < len(field.Value); {
|
||||
value, count := binary.Uvarint(field.Value[offset:])
|
||||
if count <= 0 {
|
||||
return nil, wire.ErrMalformed
|
||||
}
|
||||
out = append(out, value)
|
||||
offset += count
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func presetWire(p Preset) []byte {
|
||||
var out []byte
|
||||
if p.Name != "" {
|
||||
out = wire.AppendString(out, 1, p.Name)
|
||||
}
|
||||
if p.ResourceID != 0 {
|
||||
out = wire.AppendVarint(out, 2, p.ResourceID)
|
||||
}
|
||||
if p.ResourceColor != 0 {
|
||||
out = wire.AppendVarint(out, 3, p.ResourceColor)
|
||||
}
|
||||
if p.Slot != 0 {
|
||||
out = wire.AppendVarint(out, 4, p.Slot)
|
||||
}
|
||||
for _, deck := range p.Decks {
|
||||
out = wire.AppendBytes(out, 5, presetDeckWire(deck))
|
||||
}
|
||||
for _, bless := range p.Blesses {
|
||||
var b []byte
|
||||
if bless.DeckType != 0 {
|
||||
b = wire.AppendVarint(b, 1, bless.DeckType)
|
||||
}
|
||||
for _, id := range bless.IDs {
|
||||
b = wire.AppendVarint(b, 2, id)
|
||||
}
|
||||
out = wire.AppendBytes(out, 6, b)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func presetDeckWire(deck PresetDeck) []byte {
|
||||
base := wire.AppendVarint(nil, 1, deck.Deck.CharacterInvenIndex)
|
||||
if deck.Deck.CostumeInvenIndex != 0 {
|
||||
base = wire.AppendVarint(base, 2, deck.Deck.CostumeInvenIndex)
|
||||
}
|
||||
base = wire.AppendVarint(base, 3, deck.Deck.Slot)
|
||||
out := wire.AppendBytes(nil, 1, base)
|
||||
if deck.CostumeIndex != 0 {
|
||||
out = wire.AppendVarint(out, 2, deck.CostumeIndex)
|
||||
}
|
||||
for _, item := range deck.Equipment {
|
||||
var b []byte
|
||||
if item.Type != 0 {
|
||||
b = wire.AppendVarint(b, 1, item.Type)
|
||||
}
|
||||
if item.Index != 0 {
|
||||
b = wire.AppendVarint(b, 2, item.Index)
|
||||
}
|
||||
out = wire.AppendBytes(out, 3, b)
|
||||
}
|
||||
if deck.Team != 0 {
|
||||
out = wire.AppendVarint(out, 4, deck.Team)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) presetCacheKey(kind string, seq uint64) string {
|
||||
return kind + ":" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
|
||||
}
|
||||
|
||||
func (s *Store) persistPresetLocked(p Preset) error {
|
||||
payload, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
core, err := s.corePayloadLocked()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{{Bucket: "presets", Key: strconv.FormatUint(p.Slot, 10), Payload: payload}})
|
||||
}
|
||||
|
||||
func (s *Store) corePayloadLocked() ([]byte, error) {
|
||||
payload, err := json.MarshalIndent(s.state, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(payload, '\n'), nil
|
||||
}
|
||||
|
||||
func (s *Store) cachedReplyLocked(kind string, seq uint64) (deckReply, bool) {
|
||||
reply, found := s.replies[s.presetCacheKey(kind, seq)]
|
||||
if found {
|
||||
reply.body = append([]byte(nil), reply.body...)
|
||||
}
|
||||
return reply, found
|
||||
}
|
||||
|
||||
func (s *Store) rememberReplyLocked(kind string, seq uint64, code int, body []byte) {
|
||||
s.replies[s.presetCacheKey(kind, seq)] = deckReply{code: code, body: append([]byte(nil), body...)}
|
||||
}
|
||||
|
||||
func requestSequence(request []byte) (uint64, error) {
|
||||
seq, found, err := wire.Varint(request, 1)
|
||||
if err != nil || !found || seq == 0 {
|
||||
return 0, errors.New("deck: invalid request sequence")
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetInfo(request []byte) (int, []byte, bool, error) {
|
||||
if _, err := requestSequence(request); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
slots := make([]uint64, 0, len(s.presets))
|
||||
for slot := range s.presets {
|
||||
if slot < s.presetSlots {
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
}
|
||||
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
|
||||
var response []byte
|
||||
for _, slot := range slots {
|
||||
response = wire.AppendBytes(response, 1, presetWire(s.presets[slot]))
|
||||
}
|
||||
return 178, response, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetSave(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
raw, found, err := wire.Bytes(request, 2)
|
||||
if err != nil || !found {
|
||||
return 0, nil, true, errors.New("deck: PresetSave missing preset")
|
||||
}
|
||||
preset, err := decodePreset(raw)
|
||||
if err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: decode preset: %w", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("save", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
if s.characters == nil || s.collection == nil || s.equipment == nil {
|
||||
return 0, nil, true, errors.New("deck: preset runtime unavailable")
|
||||
}
|
||||
if err := s.validatePresetOwnedLocked(preset); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: invalid preset: %w", err)
|
||||
}
|
||||
if err := s.persistPresetLocked(preset); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: persist preset: %w", err)
|
||||
}
|
||||
s.presets[preset.Slot] = preset
|
||||
s.rememberReplyLocked("save", seq, 179, nil)
|
||||
return 179, nil, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetAddSlot(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
count, found, err := wire.Varint(request, 2)
|
||||
if err != nil || !found || count == 0 {
|
||||
return 0, nil, true, errors.New("deck: PresetAddSlot invalid count")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("add-slot", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
if s.wallet == nil {
|
||||
return 0, nil, true, errors.New("deck: preset wallet unavailable")
|
||||
}
|
||||
if s.presetSlots > presetMaxCount || count > presetMaxCount-s.presetSlots {
|
||||
return 0, nil, true, errors.New("deck: preset slot limit exceeded")
|
||||
}
|
||||
if count > ^uint64(0)/presetSlotPrice {
|
||||
return 0, nil, true, errors.New("deck: preset slot price overflow")
|
||||
}
|
||||
identity := "preset-slot:" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
|
||||
if _, err := s.wallet.SpendGoldOnce(identity, count*presetSlotPrice); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: buy preset slot: %w", err)
|
||||
}
|
||||
next := s.presetSlots + count
|
||||
payload, err := json.Marshal(next)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
core, err := s.corePayloadLocked()
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
if err := s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{{Bucket: "preset_config", Key: "slots", Payload: payload}}); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: persist preset slots: %w", err)
|
||||
}
|
||||
s.presetSlots = next
|
||||
s.rememberReplyLocked("add-slot", seq, 180, nil)
|
||||
return 180, nil, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetInfoChange(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
nameBytes, _, err := wire.Bytes(request, 2)
|
||||
if err != nil {
|
||||
return 0, nil, true, errors.New("deck: invalid preset name")
|
||||
}
|
||||
resourceID, _, err := wire.Varint(request, 3)
|
||||
if err != nil {
|
||||
return 0, nil, true, errors.New("deck: invalid preset icon")
|
||||
}
|
||||
color, _, err := wire.Varint(request, 4)
|
||||
if err != nil {
|
||||
return 0, nil, true, errors.New("deck: invalid preset color")
|
||||
}
|
||||
// Ordinary preset slots are zero-based. Proto3 omits slot=0, so an absent
|
||||
// field 5 is the first slot rather than a malformed request.
|
||||
slot, _, err := wire.Varint(request, 5)
|
||||
if err != nil {
|
||||
return 0, nil, true, errors.New("deck: invalid preset slot")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("info-change", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
preset, exists := s.presets[slot]
|
||||
if !exists {
|
||||
preset = Preset{Slot: slot, Decks: []PresetDeck{}, Blesses: []PresetBless{}}
|
||||
}
|
||||
preset.Name, preset.ResourceID, preset.ResourceColor = string(nameBytes), resourceID, color
|
||||
if err := validatePresetShape(preset, s.presetSlots); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: invalid preset metadata: %w", err)
|
||||
}
|
||||
if err := s.persistPresetLocked(preset); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
s.presets[slot] = preset
|
||||
s.rememberReplyLocked("info-change", seq, 278, nil)
|
||||
return 278, nil, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetDelete(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
var slots []uint64
|
||||
if err := wire.Walk(request, func(field wire.Field) error {
|
||||
if field.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
values, err := repeatedUint64(field)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slots = append(slots, values...)
|
||||
return nil
|
||||
}); err != nil || len(slots) == 0 {
|
||||
return 0, nil, true, errors.New("deck: invalid preset delete slots")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("delete", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
seen := make(map[uint64]bool, len(slots))
|
||||
changes := make([]stateio.EntryMutation, 0, len(slots))
|
||||
for _, slot := range slots {
|
||||
if slot >= s.presetSlots || seen[slot] {
|
||||
return 0, nil, true, errors.New("deck: invalid or duplicate preset delete slot")
|
||||
}
|
||||
seen[slot] = true
|
||||
changes = append(changes, stateio.EntryMutation{Bucket: "presets", Key: strconv.FormatUint(slot, 10), Delete: true})
|
||||
}
|
||||
core, err := s.corePayloadLocked()
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
if err := s.storage.SaveWithEntries("deck", core, changes); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
for slot := range seen {
|
||||
delete(s.presets, slot)
|
||||
}
|
||||
// 2.35.10 contains the request/response classes but no PacketCode enum
|
||||
// member. Code zero is the same compatibility fallback used for other
|
||||
// unnumbered local endpoints; it must not be treated as an official value.
|
||||
s.rememberReplyLocked("delete", seq, 0, nil)
|
||||
return 0, nil, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handlePresetUse(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
// PresetUse is also zero-based and slot=0 is omitted by proto3.
|
||||
slot, _, err := wire.Varint(request, 2)
|
||||
if err != nil {
|
||||
return 0, nil, true, errors.New("deck: PresetUse missing slot")
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("use", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
preset, found := s.presets[slot]
|
||||
if !found || len(preset.Decks) == 0 {
|
||||
return 0, nil, true, errors.New("deck: PresetUse references an empty slot")
|
||||
}
|
||||
if s.characters == nil || s.collection == nil || s.equipment == nil {
|
||||
return 0, nil, true, errors.New("deck: preset runtime unavailable")
|
||||
}
|
||||
if err := s.validatePresetOwnedLocked(preset); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: stale preset: %w", err)
|
||||
}
|
||||
deckEntries := make([]DeckEntry, 0, len(preset.Decks))
|
||||
assignments := make(map[uint64]uint64, len(preset.Decks))
|
||||
bindings := make([]player.PresetEquipmentBinding, 0, len(preset.Decks))
|
||||
for _, entry := range preset.Decks {
|
||||
deckEntries = append(deckEntries, entry.Deck)
|
||||
assignments[entry.Deck.CharacterInvenIndex] = entry.CostumeIndex
|
||||
equipment := make([]uint64, 5)
|
||||
for _, item := range entry.Equipment {
|
||||
equipment[item.Type] = item.Index
|
||||
}
|
||||
bindings = append(bindings, player.PresetEquipmentBinding{CharacterIndex: entry.Deck.CharacterInvenIndex, Equipment: equipment})
|
||||
}
|
||||
if _, err := s.characters.ApplyPresetCostumes(assignments); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: apply preset costumes: %w", err)
|
||||
}
|
||||
affected, err := s.equipment.ApplyPresetEquipment(bindings)
|
||||
if err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: apply preset equipment: %w", err)
|
||||
}
|
||||
next := clone(s.state)
|
||||
next.Deck = append([]DeckEntry(nil), deckEntries...)
|
||||
for character, costume := range assignments {
|
||||
next.Costumes[character] = costume
|
||||
}
|
||||
if err := s.commit(next); err != nil {
|
||||
return 0, nil, true, fmt.Errorf("deck: persist applied preset: %w", err)
|
||||
}
|
||||
var response []byte
|
||||
for _, entry := range deckEntries {
|
||||
base := wire.AppendVarint(nil, 1, entry.CharacterInvenIndex)
|
||||
if entry.CostumeInvenIndex != 0 {
|
||||
base = wire.AppendVarint(base, 2, entry.CostumeInvenIndex)
|
||||
}
|
||||
base = wire.AppendVarint(base, 3, entry.Slot)
|
||||
response = wire.AppendBytes(response, 1, base)
|
||||
}
|
||||
characterSet := make(map[uint64]bool, len(assignments)+len(affected))
|
||||
for index := range assignments {
|
||||
characterSet[index] = true
|
||||
}
|
||||
for _, character := range affected {
|
||||
characterSet[character.InvenIndex] = true
|
||||
}
|
||||
indices := make([]uint64, 0, len(characterSet))
|
||||
for index := range characterSet {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
|
||||
for _, index := range indices {
|
||||
if character, found := s.characters.Find(index); found {
|
||||
response = wire.AppendBytes(response, 2, player.CharacterWire(character))
|
||||
}
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
info := wire.AppendVarint(nil, 1, binding.CharacterIndex)
|
||||
for _, index := range binding.Equipment {
|
||||
info = wire.AppendVarint(info, 2, index)
|
||||
}
|
||||
response = wire.AppendBytes(response, 3, info)
|
||||
}
|
||||
s.rememberReplyLocked("use", seq, 409, response)
|
||||
return 409, response, true, nil
|
||||
}
|
||||
|
||||
func costumeSettingWire(setting CostumeSetting) []byte {
|
||||
out := wire.AppendVarint(nil, 1, setting.CharacterIndex)
|
||||
for _, item := range setting.Sequence {
|
||||
entry := wire.AppendVarint(nil, 1, uint64(item.CostumeIndex))
|
||||
if item.BurstLevel != 0 {
|
||||
entry = wire.AppendVarint(entry, 2, item.BurstLevel)
|
||||
}
|
||||
out = wire.AppendBytes(out, 2, entry)
|
||||
}
|
||||
if setting.BattleMode != 0 {
|
||||
out = wire.AppendVarint(out, 3, setting.BattleMode)
|
||||
}
|
||||
if setting.MonsterID != 0 {
|
||||
out = wire.AppendVarint(out, 4, setting.MonsterID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeCostumeSetting(data []byte) (CostumeSetting, error) {
|
||||
var result CostumeSetting
|
||||
character, found, err := wire.Varint(data, 1)
|
||||
if err != nil || !found || character == 0 {
|
||||
return result, errors.New("deck: costume setting missing character")
|
||||
}
|
||||
result.CharacterIndex = character
|
||||
result.BattleMode, _, err = wire.Varint(data, 3)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
result.MonsterID, _, err = wire.Varint(data, 4)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
err = wire.Walk(data, func(field wire.Field) error {
|
||||
if field.Number != 2 {
|
||||
return nil
|
||||
}
|
||||
if field.Type != 2 {
|
||||
return errors.New("deck: invalid costume setting sequence")
|
||||
}
|
||||
raw, found, err := wire.Varint(field.Value, 1)
|
||||
if err != nil || !found {
|
||||
return errors.New("deck: costume setting entry missing costume")
|
||||
}
|
||||
burst, _, err := wire.Varint(field.Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Sequence = append(result.Sequence, CostumeSettingItem{CostumeIndex: int64(raw), BurstLevel: burst})
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *Store) validateCostumeSettingLocked(setting CostumeSetting) error {
|
||||
if s.characters == nil || s.collection == nil {
|
||||
return errors.New("deck: costume setting runtime unavailable")
|
||||
}
|
||||
if setting.BattleMode != 0 || setting.MonsterID != 0 {
|
||||
return errors.New("deck: ordinary costume setting requires normal battle mode")
|
||||
}
|
||||
if _, found := s.characters.Find(setting.CharacterIndex); !found {
|
||||
return fmt.Errorf("deck: unknown costume setting character %d", setting.CharacterIndex)
|
||||
}
|
||||
for _, item := range setting.Sequence {
|
||||
if item.CostumeIndex <= 0 {
|
||||
if item.CostumeIndex != 0 && item.CostumeIndex != -1 {
|
||||
return fmt.Errorf("deck: invalid costume setting sentinel %d", item.CostumeIndex)
|
||||
}
|
||||
continue
|
||||
}
|
||||
costume, found := s.collection.CostumeByIndex(uint64(item.CostumeIndex))
|
||||
if !found || costume.UseChar != setting.CharacterIndex {
|
||||
return fmt.Errorf("deck: costume %d does not belong to character %d", item.CostumeIndex, setting.CharacterIndex)
|
||||
}
|
||||
if item.BurstLevel > costume.BurstLevel {
|
||||
return fmt.Errorf("deck: costume %d burst level exceeds owned level", item.CostumeIndex)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) handleCostumeSettingInfo(request []byte) (int, []byte, bool, error) {
|
||||
if _, err := requestSequence(request); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
indices := make([]uint64, 0, len(s.costumeSettings))
|
||||
for index := range s.costumeSettings {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
|
||||
var response []byte
|
||||
for _, index := range indices {
|
||||
response = wire.AppendBytes(response, 1, costumeSettingWire(s.costumeSettings[index]))
|
||||
}
|
||||
return 397, response, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) handleCostumeSettingSave(request []byte) (int, []byte, bool, error) {
|
||||
seq, err := requestSequence(request)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
raw, found, err := wire.Bytes(request, 2)
|
||||
if err != nil || !found {
|
||||
return 0, nil, true, errors.New("deck: DeckCostumeSettingSave missing setting")
|
||||
}
|
||||
setting, err := decodeCostumeSetting(raw)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if reply, found := s.cachedReplyLocked("costume-setting-save", seq); found {
|
||||
return reply.code, reply.body, true, nil
|
||||
}
|
||||
if err := s.validateCostumeSettingLocked(setting); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
payload, err := json.Marshal(setting)
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
change := stateio.EntryMutation{Bucket: "costume_settings", Key: strconv.FormatUint(setting.CharacterIndex, 10), Payload: payload}
|
||||
core, err := s.corePayloadLocked()
|
||||
if err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
if err := s.storage.SaveWithEntries("deck", core, []stateio.EntryMutation{change}); err != nil {
|
||||
return 0, nil, true, err
|
||||
}
|
||||
s.costumeSettings[setting.CharacterIndex] = setting
|
||||
s.rememberReplyLocked("costume-setting-save", seq, 398, nil)
|
||||
return 398, nil, true, nil
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package deck
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
)
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user