Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling. Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata. Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks. Improve automatic login and recovery diagnostics; document configuration and release flows.
627 lines
19 KiB
Go
627 lines
19 KiB
Go
// Package account owns the local-account representation used by LoginUser.
|
|
// It deliberately stores decoded protobuf data, never a captured HTTP reply
|
|
// or a captured session key.
|
|
package account
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"bd2server/internal/server/cryptox"
|
|
"bd2server/internal/server/versionconfig"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
func StateVersion() string { return versionconfig.State() }
|
|
|
|
var (
|
|
ErrInvalidSeed = errors.New("account: invalid LoginUser seed")
|
|
)
|
|
|
|
// LoginSeed is the versioned, decoded representation of a LoginUser response.
|
|
// UserInfo is UserDBInfo protobuf field 1 with its field 3 (user_key)
|
|
// deliberately absent. ResponseFields contains the remaining top-level
|
|
// protobuf fields (such as client-notification state), never an HTTP envelope.
|
|
type LoginSeed struct {
|
|
Version string
|
|
PacketCode int
|
|
UserInfo []byte
|
|
ResponseFields []byte
|
|
currencies CurrencyProvider
|
|
purchaseCounts PurchaseCountProvider
|
|
presetSlots PresetSlotProvider
|
|
inventorySlots InventorySlotProvider
|
|
firstGacha FirstGachaProvider
|
|
friendshipAP FriendshipAPProvider
|
|
lastPlayedPack LastPlayedPackProvider
|
|
achievementExp AchievementExperienceProvider
|
|
levelReward LevelRewardProvider
|
|
huntingAP HuntingAPProvider
|
|
monsterHuntSlots PresetSlotProvider
|
|
additionalCurrencies interface {
|
|
AdditionalCurrencies() (map[int]uint64, error)
|
|
}
|
|
newbieStep interface{ NewbieStep() uint64 }
|
|
}
|
|
|
|
func (s *LoginSeed) AttachNewbieStep(provider interface{ NewbieStep() uint64 }) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing newbie step provider")
|
|
}
|
|
s.newbieStep = provider
|
|
return nil
|
|
}
|
|
|
|
// AttachAdditionalCurrencies supplies balances owned by optional gameplay
|
|
// domains without altering the stable wallet representation.
|
|
func (s *LoginSeed) AttachAdditionalCurrencies(provider interface {
|
|
AdditionalCurrencies() (map[int]uint64, error)
|
|
}) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing additional currency provider")
|
|
}
|
|
s.additionalCurrencies = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) AttachMonsterHuntSlots(provider PresetSlotProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing monster hunt preset provider")
|
|
}
|
|
s.monsterHuntSlots = provider
|
|
return nil
|
|
}
|
|
|
|
type HuntingAPProvider interface {
|
|
HuntingAP() (free, bonus uint64, err error)
|
|
}
|
|
|
|
func (s *LoginSeed) AttachHuntingAP(provider HuntingAPProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing hunting AP provider")
|
|
}
|
|
s.huntingAP = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) SeedHuntingAP() (free, bonus uint64, err error) {
|
|
if err = s.Validate(); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if free, _, err = wire.Varint(s.UserInfo, 20); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
bonus, _, err = wire.Varint(s.UserInfo, 21)
|
|
return
|
|
}
|
|
|
|
type LevelRewardProvider interface {
|
|
LevelRewardCount() (uint64, error)
|
|
}
|
|
|
|
func (s *LoginSeed) AttachLevelReward(provider LevelRewardProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing level reward provider")
|
|
}
|
|
s.levelReward = provider
|
|
return nil
|
|
}
|
|
|
|
type AchievementExperienceProvider interface {
|
|
AchievementExperience() (uint64, error)
|
|
}
|
|
|
|
func (s *LoginSeed) AttachAchievementExperience(provider AchievementExperienceProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing achievement experience provider")
|
|
}
|
|
s.achievementExp = provider
|
|
return nil
|
|
}
|
|
|
|
// LastPlayedPackProvider reads the persisted return destination. A zero value
|
|
// means there is no saved destination yet, so a new account uses its seed.
|
|
type LastPlayedPackProvider interface {
|
|
LastPlayedPackID() (uint64, error)
|
|
}
|
|
|
|
func (s *LoginSeed) AttachLastPlayedPack(provider LastPlayedPackProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing last played pack provider")
|
|
}
|
|
s.lastPlayedPack = provider
|
|
return nil
|
|
}
|
|
|
|
// FirstGachaProvider reads the mutable account flag for every login, including
|
|
// a login after confirming the starter draw without restarting the server.
|
|
type FirstGachaProvider interface {
|
|
FirstGachaCompleted() bool
|
|
}
|
|
|
|
func (s *LoginSeed) AttachFirstGacha(provider FirstGachaProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: missing first gacha provider")
|
|
}
|
|
s.firstGacha = provider
|
|
return nil
|
|
}
|
|
|
|
// CurrencyProvider supplies the authoritative mutable UserDBInfo wallet.
|
|
type CurrencyProvider interface {
|
|
Currencies() (gold, freeJewelry, jewelry, mileage uint64)
|
|
}
|
|
|
|
type HopePowderProvider interface {
|
|
HopePowderBalance() uint64
|
|
}
|
|
|
|
type CatalystProvider interface {
|
|
CatalystBalance() uint64
|
|
}
|
|
|
|
type EquipmentMileageProvider interface {
|
|
EquipmentMileageBalances() (mileage, exchangeGage uint64)
|
|
}
|
|
|
|
// PurchaseCountProvider supplies the current PurchaseCountDBInfo messages for
|
|
// UserDBInfo field 26. Implementations must derive them from authoritative
|
|
// account state rather than the immutable login seed.
|
|
type PurchaseCountProvider interface {
|
|
PurchaseCountDBInfos() [][]byte
|
|
}
|
|
|
|
// PresetSlotProvider supplies the authoritative number of ordinary party
|
|
// preset slots for UserDBInfo field 28. The deck domain owns both purchased
|
|
// slot state and the preset records stored in those slots.
|
|
type PresetSlotProvider interface {
|
|
PresetSlotCount() uint64
|
|
}
|
|
|
|
// InventorySlotProvider owns the four mutable UserDBInfo capacity fields.
|
|
// Development overrides are applied by the provider as a login-time view;
|
|
// the immutable account seed is never rewritten.
|
|
type InventorySlotProvider interface {
|
|
UserInventorySlots() (items, storage, equipment, equipmentStorage uint64, err error)
|
|
}
|
|
|
|
// FriendshipAPProvider supplies the account's remaining daily counseling
|
|
// points; LoginUser must not restore points from its immutable seed.
|
|
type FriendshipAPProvider interface {
|
|
FriendshipAP() (uint64, error)
|
|
}
|
|
|
|
func (s *LoginSeed) AttachFriendshipAP(provider FriendshipAPProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: nil friendship AP provider")
|
|
}
|
|
s.friendshipAP = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) AttachCurrencies(provider CurrencyProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: nil currency provider")
|
|
}
|
|
s.currencies = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) AttachPurchaseCounts(provider PurchaseCountProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: nil purchase count provider")
|
|
}
|
|
s.purchaseCounts = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) AttachPresetSlots(provider PresetSlotProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: nil preset slot provider")
|
|
}
|
|
s.presetSlots = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) AttachInventorySlots(provider InventorySlotProvider) error {
|
|
if provider == nil {
|
|
return errors.New("account: nil inventory slot provider")
|
|
}
|
|
s.inventorySlots = provider
|
|
return nil
|
|
}
|
|
|
|
func (s *LoginSeed) SeedInventorySlots() (items, storage, equipment, equipmentStorage uint64, err error) {
|
|
if err = s.Validate(); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
values := []*uint64{&items, &storage, &equipment, &equipmentStorage}
|
|
for i, field := range []int{5, 6, 10, 15} {
|
|
value, _, readErr := wire.Varint(s.UserInfo, field)
|
|
if readErr != nil {
|
|
return 0, 0, 0, 0, readErr
|
|
}
|
|
*values[i] = value
|
|
}
|
|
return items, storage, equipment, equipmentStorage, nil
|
|
}
|
|
|
|
// SeedCurrencies returns the immutable starting balances embedded in the
|
|
// versioned UserDBInfo template. Mutable balances live in player.Wallet.
|
|
func (s *LoginSeed) SeedCurrencies() (gold, freeJewelry, jewelry, mileage uint64, err error) {
|
|
if err = s.Validate(); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
read := func(field int) (uint64, error) {
|
|
value, found, readErr := wire.Varint(s.UserInfo, field)
|
|
if readErr != nil {
|
|
return 0, readErr
|
|
}
|
|
if !found {
|
|
return 0, nil
|
|
}
|
|
return value, nil
|
|
}
|
|
if gold, err = read(7); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
if freeJewelry, err = read(8); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
if jewelry, err = read(9); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
if mileage, err = read(23); err != nil {
|
|
return 0, 0, 0, 0, err
|
|
}
|
|
return gold, freeJewelry, jewelry, mileage, nil
|
|
}
|
|
|
|
func (s *LoginSeed) SeedHopePowder() (uint64, error) {
|
|
if err := s.Validate(); err != nil {
|
|
return 0, err
|
|
}
|
|
value, found, err := wire.Varint(s.UserInfo, 24)
|
|
if err != nil || !found {
|
|
return value, err
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (s *LoginSeed) SeedCatalyst() (uint64, error) {
|
|
if err := s.Validate(); err != nil {
|
|
return 0, err
|
|
}
|
|
value, _, err := wire.Varint(s.UserInfo, 11)
|
|
return value, err
|
|
}
|
|
|
|
func (s *LoginSeed) SeedEquipmentMileage() (mileage, exchangeGage uint64, err error) {
|
|
if err = s.Validate(); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if mileage, _, err = wire.Varint(s.UserInfo, 67); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if exchangeGage, _, err = wire.Varint(s.UserInfo, 68); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return mileage, exchangeGage, nil
|
|
}
|
|
|
|
type diskSeed struct {
|
|
Version string `json:"version"`
|
|
PacketCode int `json:"packet_code"`
|
|
UserInfoBase64 string `json:"user_info_base64"`
|
|
ResponseFieldsBase64 string `json:"response_fields_base64,omitempty"`
|
|
}
|
|
|
|
// Load reads one versioned seed file. Relative paths are intentionally left to
|
|
// the caller; production code can therefore choose an explicit local asset.
|
|
func Load(path string) (*LoginSeed, error) {
|
|
data, err := os.ReadFile(filepath.Clean(path))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: read seed: %w", err)
|
|
}
|
|
var disk diskSeed
|
|
if err := json.Unmarshal(data, &disk); err != nil {
|
|
return nil, fmt.Errorf("account: decode seed JSON: %w", err)
|
|
}
|
|
user, err := base64.StdEncoding.DecodeString(disk.UserInfoBase64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: decode user_info_base64: %w", err)
|
|
}
|
|
other, err := base64.StdEncoding.DecodeString(disk.ResponseFieldsBase64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: decode response_fields_base64: %w", err)
|
|
}
|
|
seed := &LoginSeed{Version: disk.Version, PacketCode: disk.PacketCode, UserInfo: user, ResponseFields: other}
|
|
if err := seed.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return seed, nil
|
|
}
|
|
|
|
// Write stores a seed in the portable JSON form used by the one-shot importer.
|
|
func (s *LoginSeed) Write(path string) error {
|
|
if err := s.Validate(); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(diskSeed{
|
|
Version: s.Version, PacketCode: s.PacketCode,
|
|
UserInfoBase64: base64.StdEncoding.EncodeToString(s.UserInfo),
|
|
ResponseFieldsBase64: base64.StdEncoding.EncodeToString(s.ResponseFields),
|
|
}, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = append(data, '\n')
|
|
if err := os.WriteFile(filepath.Clean(path), data, 0o644); err != nil {
|
|
return fmt.Errorf("account: write seed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Validate verifies the minimum protocol contract and makes sure a captured
|
|
// user_key cannot accidentally be committed to a local seed.
|
|
func (s *LoginSeed) Validate() error {
|
|
if s == nil || s.Version == "" || s.PacketCode <= 0 || len(s.UserInfo) == 0 {
|
|
return ErrInvalidSeed
|
|
}
|
|
if _, found, err := wire.Bytes(s.UserInfo, 3); err != nil {
|
|
return fmt.Errorf("%w: malformed UserInfo: %v", ErrInvalidSeed, err)
|
|
} else if found {
|
|
return fmt.Errorf("%w: UserInfo contains user_key", ErrInvalidSeed)
|
|
}
|
|
if err := wire.Walk(s.ResponseFields, func(field wire.Field) error {
|
|
if field.Number == 1 {
|
|
return fmt.Errorf("%w: response fields contain UserInfo", ErrInvalidSeed)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Encode makes a fresh encrypted LoginUser HTTP envelope. sessionKey belongs
|
|
// to the new local session; no value from a capture is used at runtime.
|
|
func (s *LoginSeed) Encode(sessionKey string, now time.Time) ([]byte, error) {
|
|
if err := s.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
proto, err := s.Login(nil, []byte(sessionKey))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := cryptox.EncryptBase64Payload(proto, cryptox.Key())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
envelope := struct {
|
|
ErrorType int `json:"errorType"`
|
|
PacketCode int `json:"packetCode"`
|
|
Length int `json:"length"`
|
|
Data string `json:"data"`
|
|
ServerNowTime int64 `json:"serverNowTime"`
|
|
}{
|
|
ErrorType: 0, PacketCode: s.PacketCode,
|
|
Length: base64.StdEncoding.EncodedLen(len(proto)), Data: data,
|
|
ServerNowTime: now.UnixMilli(),
|
|
}
|
|
return json.Marshal(envelope)
|
|
}
|
|
|
|
// Login constructs the decoded LoginUser protobuf for one fresh local
|
|
// session. request is the already-decrypted LoginUser protobuf. The reply
|
|
// is deliberately protobuf-only so the session/transport layer owns its
|
|
// packet-code and HTTP envelope policy.
|
|
func (s *LoginSeed) Login(request, sessionKey []byte) ([]byte, error) {
|
|
if err := s.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(request) != 0 {
|
|
if _, found, err := wire.Varint(request, 1); err != nil || !found {
|
|
return nil, fmt.Errorf("account: LoginUser request has no sequence: %w", err)
|
|
}
|
|
}
|
|
if len(sessionKey) != cryptox.AESKeySize {
|
|
return nil, fmt.Errorf("account: session key: %w", cryptox.ErrInvalidKey)
|
|
}
|
|
if _, err := cryptox.SessionKey(string(sessionKey)); err != nil {
|
|
return nil, fmt.Errorf("account: session key: %w", err)
|
|
}
|
|
user := append([]byte(nil), s.UserInfo...)
|
|
if s.newbieStep != nil {
|
|
step := s.newbieStep.NewbieStep()
|
|
if step > math.MaxInt32 {
|
|
return nil, errors.New("account: newbie step exceeds protocol range")
|
|
}
|
|
var err error
|
|
user, _, err = wire.ReplaceVarint(user, 39, step)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.additionalCurrencies != nil {
|
|
values, err := s.additionalCurrencies.AdditionalCurrencies()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for field, value := range values {
|
|
if value > math.MaxInt32 {
|
|
return nil, errors.New("account: additional currency exceeds protocol range")
|
|
}
|
|
user, _, err = wire.ReplaceVarint(user, field, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
if s.monsterHuntSlots != nil {
|
|
count := s.monsterHuntSlots.PresetSlotCount()
|
|
if count > math.MaxInt32 {
|
|
return nil, errors.New("account: monster hunt preset slots exceed protocol range")
|
|
}
|
|
var err error
|
|
if user, _, err = wire.ReplaceVarint(user, 52, count); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.huntingAP != nil {
|
|
free, bonus, err := s.huntingAP.HuntingAP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: hunting AP: %w", err)
|
|
}
|
|
if free > math.MaxInt32 || bonus > math.MaxInt32 {
|
|
return nil, errors.New("account: hunting AP exceeds protocol range")
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 20, free); err != nil {
|
|
return nil, err
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 21, bonus); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.levelReward != nil {
|
|
claimed, err := s.levelReward.LevelRewardCount()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: level reward: %w", err)
|
|
}
|
|
if claimed > math.MaxInt32 {
|
|
return nil, errors.New("account: level reward exceeds protocol range")
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 13, claimed); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.achievementExp != nil {
|
|
experience, err := s.achievementExp.AchievementExperience()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: achievement experience: %w", err)
|
|
}
|
|
if experience > math.MaxInt32 {
|
|
return nil, errors.New("account: achievement experience exceeds protocol range")
|
|
}
|
|
user, _, err = wire.ReplaceVarint(user, 12, experience)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if s.lastPlayedPack != nil {
|
|
packID, err := s.lastPlayedPack.LastPlayedPackID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: last played pack: %w", err)
|
|
}
|
|
if packID > 0 {
|
|
if user, _, err = wire.ReplaceVarint(user, 4, packID); err != nil {
|
|
return nil, fmt.Errorf("account: replace last played pack: %w", err)
|
|
}
|
|
}
|
|
}
|
|
if s.currencies != nil {
|
|
gold, freeJewelry, jewelry, mileage := s.currencies.Currencies()
|
|
var err error
|
|
if user, _, err = wire.ReplaceVarint(user, 7, gold); err != nil {
|
|
return nil, fmt.Errorf("account: replace gold: %w", err)
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 8, freeJewelry); err != nil {
|
|
return nil, fmt.Errorf("account: replace free jewelry: %w", err)
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 9, jewelry); err != nil {
|
|
return nil, fmt.Errorf("account: replace jewelry: %w", err)
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 23, mileage); err != nil {
|
|
return nil, fmt.Errorf("account: replace mileage: %w", err)
|
|
}
|
|
if provider, ok := s.currencies.(CatalystProvider); ok {
|
|
if user, _, err = wire.ReplaceVarint(user, 11, provider.CatalystBalance()); err != nil {
|
|
return nil, fmt.Errorf("account: replace catalyst: %w", err)
|
|
}
|
|
}
|
|
if provider, ok := s.currencies.(HopePowderProvider); ok {
|
|
if user, _, err = wire.ReplaceVarint(user, 24, provider.HopePowderBalance()); err != nil {
|
|
return nil, fmt.Errorf("account: replace hope powder: %w", err)
|
|
}
|
|
}
|
|
if provider, ok := s.currencies.(EquipmentMileageProvider); ok {
|
|
equipMileage, exchangeGage := provider.EquipmentMileageBalances()
|
|
if user, _, err = wire.ReplaceVarint(user, 67, equipMileage); err != nil {
|
|
return nil, fmt.Errorf("account: replace equipment mileage: %w", err)
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 68, exchangeGage); err != nil {
|
|
return nil, fmt.Errorf("account: replace equipment mileage exchange gauge: %w", err)
|
|
}
|
|
}
|
|
}
|
|
if s.firstGacha != nil {
|
|
value := uint64(0)
|
|
if s.firstGacha.FirstGachaCompleted() {
|
|
value = 1
|
|
}
|
|
var err error
|
|
if user, _, err = wire.ReplaceVarint(user, 27, value); err != nil {
|
|
return nil, fmt.Errorf("account: replace first gacha: %w", err)
|
|
}
|
|
}
|
|
if s.purchaseCounts != nil {
|
|
var err error
|
|
if user, err = replaceRepeatedBytes(user, 26, s.purchaseCounts.PurchaseCountDBInfos()); err != nil {
|
|
return nil, fmt.Errorf("account: replace purchase counts: %w", err)
|
|
}
|
|
}
|
|
if s.presetSlots != nil {
|
|
var err error
|
|
if user, _, err = wire.ReplaceVarint(user, 28, s.presetSlots.PresetSlotCount()); err != nil {
|
|
return nil, fmt.Errorf("account: replace preset slots: %w", err)
|
|
}
|
|
}
|
|
if s.inventorySlots != nil {
|
|
items, storage, equipment, equipmentStorage, err := s.inventorySlots.UserInventorySlots()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: inventory slots: %w", err)
|
|
}
|
|
for field, value := range map[int]uint64{5: items, 6: storage, 10: equipment, 15: equipmentStorage} {
|
|
if user, _, err = wire.ReplaceVarint(user, field, value); err != nil {
|
|
return nil, fmt.Errorf("account: replace inventory slot field %d: %w", field, err)
|
|
}
|
|
}
|
|
}
|
|
if s.friendshipAP != nil {
|
|
remaining, err := s.friendshipAP.FriendshipAP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("account: friendship AP: %w", err)
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 69, remaining); err != nil {
|
|
return nil, err
|
|
}
|
|
if user, _, err = wire.ReplaceVarint(user, 70, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
user = wire.AppendBytes(user, 3, sessionKey)
|
|
return append(wire.AppendBytes(nil, 1, user), s.ResponseFields...), nil
|
|
}
|
|
|
|
func replaceRepeatedBytes(data []byte, number int, values [][]byte) ([]byte, error) {
|
|
result := make([]byte, 0, len(data))
|
|
if err := wire.Walk(data, func(field wire.Field) error {
|
|
if field.Number != number {
|
|
result = append(result, data[field.Start:field.End]...)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, value := range values {
|
|
result = wire.AppendBytes(result, number, value)
|
|
}
|
|
return result, nil
|
|
}
|