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.
569 lines
18 KiB
Go
569 lines
18 KiB
Go
// 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"
|
|
"sync"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"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
|
|
presetDesign gamedata.PresetDesign
|
|
costumeSettings map[uint64]CostumeSetting
|
|
wallet *player.Wallet
|
|
characters *player.CharacterStore
|
|
equipment *player.EquipmentInventory
|
|
collection *player.CollectionStore
|
|
sessionID string
|
|
replies map[string]deckReply
|
|
waypointDesign func(uint64) (gamedata.WaypointPack, error)
|
|
waypointPack func(uint64, bool) error
|
|
}
|
|
|
|
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, designs ...gamedata.PresetDesign) (*Store, error) {
|
|
if e := seed.validate(); e != nil {
|
|
return nil, e
|
|
}
|
|
var design gamedata.PresetDesign
|
|
if len(designs) > 1 {
|
|
return nil, errors.New("deck: multiple preset designs")
|
|
}
|
|
if len(designs) == 1 {
|
|
design = designs[0]
|
|
if err := design.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &Store{presetDesign: design, 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: design.BaseCount, costumeSettings: map[uint64]CostumeSetting{}, replies: map[string]deckReply{}}, nil
|
|
}
|
|
func OpenStore(storage stateio.Store, seed Seed, designs ...gamedata.PresetDesign) (*Store, error) {
|
|
s, e := NewStore(seed, designs...)
|
|
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")
|
|
}
|
|
if e = validWaypointState(loaded.Waypoints); e != nil {
|
|
return nil, e
|
|
}
|
|
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] = append([]uint64(nil), 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":
|
|
return s.handleWaypoint(path, req)
|
|
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", "/WaypointUse":
|
|
return s.handleWaypoint(path, req)
|
|
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 "/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
|
|
}
|