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.
592 lines
19 KiB
Go
592 lines
19 KiB
Go
package player
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"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()
|
|
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 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
|
|
}
|