603 lines
20 KiB
Go
603 lines
20 KiB
Go
package player
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
type characterSnapshot struct {
|
|
Version string `json:"version"`
|
|
CharacterOrder []uint64 `json:"character_order"`
|
|
}
|
|
|
|
// CharacterStore owns mutable character growth separately from immutable
|
|
// character design data. The seed supplies the initially owned instances.
|
|
type CharacterStore struct {
|
|
mu sync.Mutex
|
|
store stateio.AtomicEntryStore
|
|
characters []Character
|
|
persisted map[uint64]bool
|
|
persistedOrder []uint64
|
|
persistedCore bool
|
|
inventory *Inventory
|
|
gameDataRoot string
|
|
gameDataVersion string
|
|
grow func(Character, []gamedata.GrowthMaterial) (uint64, uint64, []gamedata.GrowthMaterial, error)
|
|
collection *CollectionStore
|
|
wallet *Wallet
|
|
maxHealth func(Character) (uint64, error)
|
|
promoteGrowth func(Character, []gamedata.PromotionCost) (gamedata.PromotionGrowthResult, error)
|
|
talentGrowth *gamedata.TalentGrowthDesign
|
|
immortal *gamedata.ImmortalDesign
|
|
immortalReplies map[string]talentUpgradeReply
|
|
sessionID string
|
|
talentReplies map[string]map[string]talentUpgradeReply
|
|
talentApplied map[string]talentUpgradeReply
|
|
}
|
|
|
|
type talentUpgradeReply struct {
|
|
Digest string `json:"digest"`
|
|
Code int `json:"code"`
|
|
Body []byte `json:"body,omitempty"`
|
|
}
|
|
|
|
func (s *CharacterStore) AttachWallet(wallet *Wallet) error {
|
|
if wallet == nil {
|
|
return errors.New("player: nil growth wallet")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.wallet = wallet
|
|
return nil
|
|
}
|
|
|
|
func (s *CharacterStore) AttachImmortalDesign(design *gamedata.ImmortalDesign) error {
|
|
if design == nil {
|
|
return errors.New("player: nil immortal design")
|
|
}
|
|
s.immortal = design
|
|
return nil
|
|
}
|
|
|
|
func (s *CharacterStore) AttachTalentGrowth(design *gamedata.TalentGrowthDesign) error {
|
|
if design == nil {
|
|
return errors.New("player: nil talent growth design")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.talentGrowth = design
|
|
return nil
|
|
}
|
|
|
|
// BeginSession scopes protobuf sequence replay. Network retries reuse the
|
|
// exact TalentSkillUpgrade request and must receive success without a second
|
|
// level increase or charge.
|
|
func (s *CharacterStore) BeginSession(id string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if id == "" || id == s.sessionID {
|
|
return
|
|
}
|
|
s.sessionID = id
|
|
if s.talentReplies == nil {
|
|
s.talentReplies = make(map[string]map[string]talentUpgradeReply)
|
|
}
|
|
if s.talentReplies[id] == nil {
|
|
if len(s.talentReplies) >= 1024 {
|
|
for oldID := range s.talentReplies {
|
|
if oldID != id {
|
|
delete(s.talentReplies, oldID)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
s.talentReplies[id] = make(map[string]talentUpgradeReply)
|
|
}
|
|
}
|
|
|
|
// AttachMaxHealth makes growth and post-battle revival consume the same
|
|
// GameData/collection calculator as AllCharRefresh and BattleEnter.
|
|
func (s *CharacterStore) AttachMaxHealth(maxHealth func(Character) (uint64, error)) error {
|
|
if maxHealth == nil {
|
|
return errors.New("player: nil maximum health calculator")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.maxHealth = maxHealth
|
|
return nil
|
|
}
|
|
|
|
func (s *CharacterStore) AttachCollection(collection *CollectionStore) error {
|
|
if collection == nil {
|
|
return errors.New("player: nil collection store")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.collection = collection
|
|
return nil
|
|
}
|
|
|
|
func OpenCharacterStore(store stateio.Store, seed []Character, inventory *Inventory, gameDataRoot, gameDataVersion string) (*CharacterStore, error) {
|
|
if store == nil || inventory == nil {
|
|
return nil, errors.New("player: invalid character store configuration")
|
|
}
|
|
entries, ok := store.(stateio.AtomicEntryStore)
|
|
if !ok {
|
|
return nil, errors.New("player: character store requires atomic entries")
|
|
}
|
|
s := &CharacterStore{store: entries, inventory: inventory, characters: append([]Character(nil), seed...), persisted: make(map[uint64]bool), gameDataRoot: gameDataRoot, gameDataVersion: gameDataVersion, talentReplies: make(map[string]map[string]talentUpgradeReply), talentApplied: make(map[string]talentUpgradeReply)}
|
|
s.grow = func(character Character, materials []gamedata.GrowthMaterial) (uint64, uint64, []gamedata.GrowthMaterial, error) {
|
|
return gamedata.CharacterGrowth(s.gameDataRoot, s.gameDataVersion, int(character.ID), character.Level, character.Exp, materials)
|
|
}
|
|
s.promoteGrowth = func(character Character, submitted []gamedata.PromotionCost) (gamedata.PromotionGrowthResult, error) {
|
|
return gamedata.CharacterGrowthPromotions(s.gameDataRoot, s.gameDataVersion, int(character.ID), character.Level, character.Exp, submitted)
|
|
}
|
|
data, err := entries.Load("characters")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if data == nil {
|
|
orphaned, err := entries.ListEntries("characters", "characters")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("player: list character entries: %w", err)
|
|
}
|
|
if len(orphaned) != 0 {
|
|
return nil, errors.New("player: character entries exist without core")
|
|
}
|
|
applied, err := entries.ListEntries("characters", "talent_upgrades")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(applied) != 0 {
|
|
return nil, errors.New("player: talent upgrade entries exist without character core")
|
|
}
|
|
return s, validateCharacters(s.characters)
|
|
}
|
|
saved, loaded, err := loadCharacterEntries(entries, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.characters = loaded
|
|
s.persistedOrder = append([]uint64{}, saved.CharacterOrder...)
|
|
s.persistedCore = true
|
|
seen := make(map[uint64]bool, len(s.characters))
|
|
for _, character := range s.characters {
|
|
seen[character.InvenIndex] = true
|
|
s.persisted[character.InvenIndex] = true
|
|
}
|
|
for _, character := range seed {
|
|
if !seen[character.InvenIndex] {
|
|
return nil, fmt.Errorf("player: current character state omits seeded inventory index %d", character.InvenIndex)
|
|
}
|
|
}
|
|
if err := s.loadTalentUpgradeLedger(entries); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateCharacters(s.characters); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (s *CharacterStore) loadTalentUpgradeLedger(entries stateio.AtomicEntryStore) error {
|
|
rows, err := entries.ListEntries("characters", "talent_upgrades")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for key, payload := range rows {
|
|
parts := strings.Split(key, ":")
|
|
if len(parts) != 2 {
|
|
return fmt.Errorf("player: invalid talent upgrade ledger key %q", key)
|
|
}
|
|
index, indexErr := strconv.ParseUint(parts[0], 10, 64)
|
|
level, levelErr := strconv.ParseUint(parts[1], 10, 64)
|
|
var reply talentUpgradeReply
|
|
if indexErr != nil || levelErr != nil || index == 0 || level < 2 || json.Unmarshal(payload, &reply) != nil ||
|
|
reply.Code != talentSkillUpgradePacketCode || len(reply.Digest) != 64 {
|
|
return fmt.Errorf("player: invalid talent upgrade ledger entry %q", key)
|
|
}
|
|
if _, err := hex.DecodeString(reply.Digest); err != nil {
|
|
return fmt.Errorf("player: invalid talent upgrade digest %q", key)
|
|
}
|
|
s.talentApplied[key] = reply
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *CharacterStore) EnsurePersisted() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.persist(append([]Character(nil), s.characters...))
|
|
}
|
|
|
|
func validateCharacters(characters []Character) error {
|
|
seen := make(map[uint64]bool, len(characters))
|
|
for _, character := range characters {
|
|
if character.InvenIndex == 0 || character.ID == 0 || character.Level == 0 || seen[character.InvenIndex] {
|
|
return errors.New("player: invalid saved character")
|
|
}
|
|
seen[character.InvenIndex] = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *CharacterStore) All() []Character {
|
|
characters := s.RawAll()
|
|
active := characters[:0]
|
|
for _, c := range characters {
|
|
if !CharacterExpired(c, time.Now()) {
|
|
active = append(active, c)
|
|
}
|
|
}
|
|
characters = active
|
|
for i := range characters {
|
|
if hp, err := s.savedCurrentHealth(characters[i]); err == nil {
|
|
characters[i].HP = hp
|
|
}
|
|
}
|
|
return characters
|
|
}
|
|
|
|
// RawAll is for account-ownership queries used by the stat calculator itself.
|
|
// Calling All() from that calculator would recursively calculate its inputs.
|
|
func (s *CharacterStore) RawAll() []Character {
|
|
s.mu.Lock()
|
|
characters := append([]Character(nil), s.characters...)
|
|
collection := s.collection
|
|
s.mu.Unlock()
|
|
if collection != nil {
|
|
characters = append(characters, collection.Characters()...)
|
|
}
|
|
return characters
|
|
}
|
|
|
|
func (s *CharacterStore) Find(inventoryIndex uint64) (Character, bool) {
|
|
for _, character := range s.RawAll() {
|
|
if character.InvenIndex == inventoryIndex {
|
|
if CharacterExpired(character, time.Now()) {
|
|
return Character{}, false
|
|
}
|
|
if hp, err := s.savedCurrentHealth(character); err == nil {
|
|
character.HP = hp
|
|
}
|
|
return character, true
|
|
}
|
|
}
|
|
return Character{}, false
|
|
}
|
|
|
|
func (s *CharacterStore) Handle(path string, request []byte) (int, []byte, bool, error) {
|
|
if path == "/CharImmortal" {
|
|
return s.charImmortal(request)
|
|
}
|
|
if path == "/TalentSkillUpgrade" {
|
|
return s.talentSkillUpgrade(request)
|
|
}
|
|
if path != "/CharGrowth" {
|
|
return 0, nil, false, nil
|
|
}
|
|
index, found, err := wire.Varint(request, 2)
|
|
if err != nil || !found || index == 0 {
|
|
return 0, nil, true, errors.New("player: CharGrowth missing character")
|
|
}
|
|
var materials []Item
|
|
if err := wire.Walk(request, func(field wire.Field) error {
|
|
if field.Number != 3 {
|
|
return nil
|
|
}
|
|
if field.Type != 2 {
|
|
return errors.New("player: invalid growth material")
|
|
}
|
|
var item Item
|
|
if err := decodeVarints(field.Value, map[int]*uint64{1: &item.InvenIndex, 2: &item.ID, 3: &item.Type, 4: &item.Count, 5: &item.KeepFlag, 6: &item.TimeValue, 9: &item.SortID, 10: &item.UseCount}); err != nil {
|
|
return err
|
|
}
|
|
if item.Type == 0 || item.Count == 0 || (item.Type != 4 && (item.InvenIndex == 0 || item.ID == 0)) {
|
|
return errors.New("player: incomplete growth material")
|
|
}
|
|
materials = append(materials, item)
|
|
return nil
|
|
}); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if len(materials) == 0 {
|
|
return 0, nil, true, errors.New("player: CharGrowth has no materials")
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
position := -1
|
|
for i, current := range s.characters {
|
|
if current.InvenIndex == index {
|
|
position = i
|
|
break
|
|
}
|
|
}
|
|
var current Character
|
|
fromCollection := false
|
|
if position >= 0 {
|
|
current = s.characters[position]
|
|
} else if s.collection != nil {
|
|
current, fromCollection = s.collection.FindCharacter(index)
|
|
}
|
|
if position < 0 && !fromCollection {
|
|
return 0, nil, true, fmt.Errorf("player: unknown character inventory index %d", index)
|
|
}
|
|
isPromotion := false
|
|
for _, material := range materials {
|
|
if material.Type == 4 {
|
|
isPromotion = true
|
|
break
|
|
}
|
|
}
|
|
if isPromotion {
|
|
return s.promoteCharacter(current, position, fromCollection, materials)
|
|
}
|
|
growthMaterials := make([]gamedata.GrowthMaterial, len(materials))
|
|
for i, material := range materials {
|
|
if material.Type != 8 {
|
|
return 0, nil, true, fmt.Errorf("player: unsupported growth material type %d", material.Type)
|
|
}
|
|
growthMaterials[i] = gamedata.GrowthMaterial{ID: material.ID, Count: material.Count}
|
|
}
|
|
newLevel, newExp, refunds, err := s.grow(current, growthMaterials)
|
|
if err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: calculate character growth: %w", err)
|
|
}
|
|
current.Level = newLevel
|
|
current.Exp = newExp
|
|
if s.maxHealth != nil {
|
|
maxHealth := s.maxHealth
|
|
s.mu.Unlock()
|
|
hp, healthErr := maxHealth(current)
|
|
s.mu.Lock()
|
|
current.HP, err = hp, healthErr
|
|
if err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: calculate grown character maximum health: %w", err)
|
|
}
|
|
}
|
|
returned, err := s.inventory.ConsumeAndRefund(materials, refunds)
|
|
if err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: consume growth material: %w", err)
|
|
}
|
|
if fromCollection {
|
|
if err := s.collection.UpdateCharacter(current.ID, current); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: persist collection character growth: %w", err)
|
|
}
|
|
} else {
|
|
next := append([]Character(nil), s.characters...)
|
|
next[position] = current
|
|
if err := s.persist(next); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: persist character growth: %w", err)
|
|
}
|
|
s.characters = next
|
|
}
|
|
if err := s.resetCurrentHealth(current.InvenIndex); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
response := wire.AppendBytes(nil, 1, CharacterWire(current))
|
|
var bundle []byte
|
|
for _, item := range returned {
|
|
bundle = wire.AppendBytes(bundle, 1, ItemWire(item))
|
|
}
|
|
response = wire.AppendBytes(response, 2, bundle)
|
|
return 433, response, true, nil
|
|
}
|
|
|
|
func (s *CharacterStore) promoteCharacter(current Character, position int, fromCollection bool, materials []Item) (int, []byte, bool, error) {
|
|
var items []Item
|
|
requested := make(map[[2]uint64]uint64)
|
|
var gold uint64
|
|
for _, material := range materials {
|
|
if material.Type == 4 {
|
|
if material.InvenIndex != 0 || material.ID != 0 || gold != 0 {
|
|
return 0, nil, true, errors.New("player: invalid promotion currency")
|
|
}
|
|
gold = material.Count
|
|
} else if material.Type == 8 {
|
|
items = append(items, material)
|
|
key := [2]uint64{8, material.ID}
|
|
if material.Count > ^uint64(0)-requested[key] {
|
|
return 0, nil, true, errors.New("player: promotion material overflow")
|
|
}
|
|
requested[key] += material.Count
|
|
} else {
|
|
return 0, nil, true, fmt.Errorf("player: unsupported promotion item type %d", material.Type)
|
|
}
|
|
}
|
|
if gold == 0 {
|
|
return 0, nil, true, errors.New("player: promotion has no gold cost")
|
|
}
|
|
submitted := make([]gamedata.PromotionCost, 0, len(requested)+1)
|
|
for key, count := range requested {
|
|
submitted = append(submitted, gamedata.PromotionCost{Type: key[0], ID: key[1], Count: count})
|
|
}
|
|
submitted = append(submitted, gamedata.PromotionCost{Type: 4, Count: gold})
|
|
result, err := s.promoteGrowth(current, submitted)
|
|
if err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: calculate combined character promotion: %w; request=%+v", err, materials)
|
|
}
|
|
if gold != 0 && (s.wallet == nil || !s.wallet.CanSpendGold(gold)) {
|
|
return 0, nil, true, errors.New("player: insufficient gold for promotion")
|
|
}
|
|
if len(items) == 0 {
|
|
return 0, nil, true, errors.New("player: promotion has no item material")
|
|
}
|
|
previousID := current.ID
|
|
current.ID = result.CharacterID
|
|
current.Level = result.Level
|
|
current.Exp = result.Exp
|
|
if fromCollection {
|
|
if err := s.collection.CanUpdateCharacter(previousID, current); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: validate promoted collection character: %w", err)
|
|
}
|
|
}
|
|
if err := s.inventory.CanConsume(items); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: validate promotion items: %w", err)
|
|
}
|
|
if s.maxHealth != nil {
|
|
maxHealth := s.maxHealth
|
|
s.mu.Unlock()
|
|
hp, healthErr := maxHealth(current)
|
|
s.mu.Lock()
|
|
if healthErr != nil {
|
|
return 0, nil, true, fmt.Errorf("player: calculate promoted character health: %w", healthErr)
|
|
}
|
|
current.HP = hp
|
|
}
|
|
if gold != 0 {
|
|
identity := "char-promote:" + strconv.FormatUint(current.InvenIndex, 10) + ":" + strconv.FormatUint(current.ID, 10)
|
|
if _, err := s.wallet.SpendGoldOnce(identity, gold); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: consume promotion gold: %w", err)
|
|
}
|
|
}
|
|
returned, err := s.inventory.ConsumeAndRefund(items, result.Refunds)
|
|
if err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: consume promotion items: %w", err)
|
|
}
|
|
if fromCollection {
|
|
if err := s.collection.UpdateCharacter(previousID, current); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: persist promoted collection character: %w", err)
|
|
}
|
|
} else {
|
|
next := append([]Character(nil), s.characters...)
|
|
next[position] = current
|
|
if err := s.persist(next); err != nil {
|
|
return 0, nil, true, fmt.Errorf("player: persist promoted character: %w", err)
|
|
}
|
|
s.characters = next
|
|
}
|
|
if err := s.resetCurrentHealth(current.InvenIndex); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
response := wire.AppendBytes(nil, 1, CharacterWire(current))
|
|
var bundle []byte
|
|
for _, item := range returned {
|
|
bundle = wire.AppendBytes(bundle, 1, ItemWire(item))
|
|
}
|
|
if len(bundle) != 0 {
|
|
response = wire.AppendBytes(response, 2, bundle)
|
|
}
|
|
return 433, response, true, nil
|
|
}
|
|
|
|
// charImmortal completes the automatic post-battle revival for characters
|
|
// whose TalentSkillTable.ClassType is 14. The story character 6010 has
|
|
// ValueList[0]=10000 at every talent level (100%). The authoritative maximum
|
|
// Maximum HP is calculated separately from the persisted current HP and the
|
|
// restored value is saved explicitly for subsequent character snapshots.
|
|
func (s *CharacterStore) charImmortal(request []byte) (int, []byte, bool, error) {
|
|
seq, present, err := wire.Varint(request, 1)
|
|
if err != nil || !present || seq == 0 {
|
|
return 0, nil, true, errors.New("player: CharImmortal missing sequence")
|
|
}
|
|
digest := fmt.Sprintf("%x", sha256.Sum256(request))
|
|
s.mu.Lock()
|
|
key := fmt.Sprintf("%s:%d", s.sessionID, seq)
|
|
prior, already := s.immortalReplies[key]
|
|
s.mu.Unlock()
|
|
if already {
|
|
if prior.Digest != digest {
|
|
return 0, nil, true, errors.New("player: immortal replay payload changed")
|
|
}
|
|
return prior.Code, append([]byte(nil), prior.Body...), true, nil
|
|
}
|
|
var indices []uint64
|
|
err = wire.Walk(request, func(field wire.Field) error {
|
|
if field.Number != 2 {
|
|
return nil
|
|
}
|
|
if field.Type != 0 && field.Type != 2 {
|
|
return errors.New("player: CharImmortal invalid inventory index field")
|
|
}
|
|
for data := field.Value; len(data) != 0; {
|
|
index, count := binary.Uvarint(data)
|
|
if count <= 0 || index == 0 {
|
|
return errors.New("player: CharImmortal invalid inventory index")
|
|
}
|
|
indices = append(indices, index)
|
|
data = data[count:]
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if len(indices) == 0 || len(indices) > 5 {
|
|
return 0, nil, true, fmt.Errorf("player: CharImmortal invalid character count %d", len(indices))
|
|
}
|
|
seen := make(map[uint64]bool, len(indices))
|
|
var response []byte
|
|
for _, index := range indices {
|
|
if seen[index] {
|
|
return 0, nil, true, fmt.Errorf("player: CharImmortal duplicate character %d", index)
|
|
}
|
|
seen[index] = true
|
|
character, found := s.Find(index)
|
|
if !found {
|
|
return 0, nil, true, fmt.Errorf("player: CharImmortal unknown character %d", index)
|
|
}
|
|
if character.HP != 0 {
|
|
return 0, nil, true, errors.New("player: immortal requires a defeated character")
|
|
}
|
|
if !s.immortal.CanRestore(character.ID, character.TalentLevel) {
|
|
return 0, nil, true, errors.New("player: character has no supported immortal talent at current level")
|
|
}
|
|
maximum, err := s.MaxHealth(index)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
character.HP = maximum
|
|
response = wire.AppendBytes(response, 1, CharacterWire(character))
|
|
}
|
|
for _, index := range indices {
|
|
maximum, err := s.MaxHealth(index)
|
|
if err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
if err := s.SetCurrentHealth(index, maximum); err != nil {
|
|
return 0, nil, true, err
|
|
}
|
|
}
|
|
s.mu.Lock()
|
|
if s.immortalReplies == nil {
|
|
s.immortalReplies = map[string]talentUpgradeReply{}
|
|
}
|
|
if len(s.immortalReplies) >= 1024 {
|
|
s.immortalReplies = map[string]talentUpgradeReply{}
|
|
}
|
|
s.immortalReplies[key] = talentUpgradeReply{Digest: digest, Code: 96, Body: append([]byte(nil), response...)}
|
|
s.mu.Unlock()
|
|
return 96, response, true, nil
|
|
}
|
|
|
|
func CharacterWire(c Character) []byte {
|
|
fields := []struct {
|
|
n int
|
|
v uint64
|
|
}{{1, c.InvenIndex}, {2, c.ID}, {3, c.HP}, {4, c.Level}, {5, c.CostumeID}, {6, c.Exp}, {7, c.UseCostume}, {8, c.TalentLevel}, {9, c.TalentExp}, {10, c.SolidarityReward}, {11, c.ExpiryTime}, {13, c.ConnectPotentialCostume}}
|
|
var out []byte
|
|
for _, field := range fields {
|
|
if field.v != 0 {
|
|
out = wire.AppendVarint(out, field.n, field.v)
|
|
}
|
|
}
|
|
for _, p := range c.Pictorialbook {
|
|
book := wire.AppendVarint(wire.AppendVarint(nil, 1, p.ID), 2, p.GroupID)
|
|
out = wire.AppendBytes(out, 12, book)
|
|
}
|
|
return out
|
|
}
|