Files
bd2/go/internal/server/player/talent_upgrade.go
T

194 lines
7.2 KiB
Go

package player
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"strconv"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
)
const talentSkillUpgradePacketCode = 44
func (s *CharacterStore) talentSkillUpgrade(request []byte) (int, []byte, bool, error) {
seq, found, err := wire.Varint(request, 1)
if err != nil || !found || seq == 0 || seq > math.MaxInt32 {
return 0, nil, true, errors.New("player: TalentSkillUpgrade missing sequence")
}
index, found, err := wire.Varint(request, 2)
if err != nil || !found || index == 0 || index > math.MaxInt64 {
return 0, nil, true, errors.New("player: TalentSkillUpgrade missing character")
}
materials, err := equipmentRequestItems(request, 3, "TalentSkillUpgrade")
if err != nil {
return 0, nil, true, err
}
for _, material := range materials {
if material.InvenIndex > math.MaxInt64 || material.ID > math.MaxInt32 || material.Type > math.MaxInt32 || material.Count > math.MaxInt32 ||
material.KeepFlag > math.MaxInt32 || material.TimeValue > math.MaxInt64 || material.ExpiryTime > math.MaxInt64 ||
material.SortID > math.MaxInt32 || material.UseCount > math.MaxInt32 {
return 0, nil, true, errors.New("player: TalentSkillUpgrade material exceeds protocol range")
}
}
s.mu.Lock()
defer s.mu.Unlock()
digestBytes := sha256.Sum256(request)
digest := hex.EncodeToString(digestBytes[:])
sessionID := s.sessionID
if sessionID == "" {
sessionID = "__direct__"
}
if s.talentReplies[sessionID] == nil {
s.talentReplies[sessionID] = make(map[string]talentUpgradeReply)
}
replies := s.talentReplies[sessionID]
cacheKey := "seq:" + strconv.FormatUint(seq, 10)
if reply, ok := replies[cacheKey]; ok {
if reply.Digest != digest {
return 0, nil, true, errors.New("player: TalentSkillUpgrade sequence reused with different request")
}
return reply.Code, append([]byte(nil), reply.Body...), true, nil
}
if s.talentGrowth == nil || s.wallet == nil || s.inventory == nil {
return 0, nil, true, errors.New("player: talent upgrade unavailable")
}
position := -1
var current Character
fromCollection := false
for i, character := range s.characters {
if character.InvenIndex == index {
position, current = i, character
break
}
}
if position < 0 && s.collection != nil {
current, fromCollection = s.collection.FindCharacter(index)
}
if position < 0 && !fromCollection {
return 0, nil, true, fmt.Errorf("player: unknown talent character inventory index %d", index)
}
previousLedgerKey := strconv.FormatUint(index, 10) + ":" + strconv.FormatUint(current.TalentLevel, 10)
if reply, ok := s.talentApplied[previousLedgerKey]; ok && reply.Digest == digest {
replies[cacheKey] = reply
return reply.Code, append([]byte(nil), reply.Body...), true, nil
}
rule, err := s.talentGrowth.UpgradeRule(current.ID, current.TalentLevel)
if err != nil {
return 0, nil, true, fmt.Errorf("player: resolve talent upgrade: %w", err)
}
if current.TalentExp < rule.RequiredTotalExp {
return 0, nil, true, fmt.Errorf("player: character %d talent experience %d is below required %d", index, current.TalentExp, rule.RequiredTotalExp)
}
items, gold, err := validateTalentUpgradeMaterials(rule.Costs, materials)
if err != nil {
return 0, nil, true, err
}
if gold != 0 && !s.wallet.CanSpendGold(gold) {
return 0, nil, true, errors.New("player: insufficient gold for talent upgrade")
}
if len(items) != 0 {
if err := s.inventory.CanConsume(items); err != nil {
return 0, nil, true, fmt.Errorf("player: validate talent upgrade items: %w", err)
}
}
previousID := current.ID
current.TalentLevel++
ledgerKey := strconv.FormatUint(index, 10) + ":" + strconv.FormatUint(current.TalentLevel, 10)
if _, exists := s.talentApplied[ledgerKey]; exists {
return 0, nil, true, fmt.Errorf("player: talent upgrade ledger already contains target %s", ledgerKey)
}
if fromCollection {
if err := s.collection.CanUpdateCharacter(previousID, current); err != nil {
return 0, nil, true, fmt.Errorf("player: validate collection talent upgrade: %w", err)
}
}
identity := "talent-upgrade:" + sessionID + ":" + cacheKey + ":character:" + strconv.FormatUint(index, 10) + ":level:" + strconv.FormatUint(rule.CurrentLevel, 10)
if gold != 0 {
if _, err := s.wallet.SpendGoldOnce(identity, gold); err != nil {
return 0, nil, true, fmt.Errorf("player: consume talent upgrade gold: %w", err)
}
}
if len(items) != 0 {
if err := s.inventory.Consume(items); err != nil {
return 0, nil, true, fmt.Errorf("player: consume talent upgrade items: %w", err)
}
}
reply := talentUpgradeReply{Digest: digest, Code: talentSkillUpgradePacketCode}
ledgerPayload, err := json.Marshal(reply)
if err != nil {
return 0, nil, true, err
}
ledgerChange := stateio.EntryMutation{Bucket: "talent_upgrades", Key: ledgerKey, Payload: ledgerPayload}
if fromCollection {
if err := s.collection.UpdateCharacter(previousID, current); err != nil {
return 0, nil, true, fmt.Errorf("player: persist collection talent upgrade: %w", err)
}
if err := s.store.SaveWithEntries("characters", nil, []stateio.EntryMutation{ledgerChange}); err != nil {
return 0, nil, true, fmt.Errorf("player: persist talent upgrade replay ledger: %w", err)
}
} else {
next := append([]Character(nil), s.characters...)
next[position] = current
if err := s.persistWithChanges(next, []stateio.EntryMutation{ledgerChange}); err != nil {
return 0, nil, true, fmt.Errorf("player: persist talent upgrade: %w", err)
}
s.characters = next
}
// TalentSkillUpgradeResponse.item_info is an optional grant list. Current
// GameData defines no refund, so the correct protobuf response is empty.
s.talentApplied[ledgerKey] = reply
replies[cacheKey] = reply
return talentSkillUpgradePacketCode, nil, true, nil
}
func validateTalentUpgradeMaterials(costs []gamedata.PromotionCost, materials []Item) ([]Item, uint64, error) {
want := make(map[[2]uint64]uint64, len(costs))
for _, cost := range costs {
if cost.Type == 0 || cost.Count == 0 || (cost.Type == 4 && cost.ID != 0) || (cost.Type != 4 && cost.ID == 0) {
return nil, 0, errors.New("player: invalid GameData talent upgrade cost")
}
key := [2]uint64{cost.Type, cost.ID}
if want[key] > math.MaxUint64-cost.Count {
return nil, 0, errors.New("player: talent upgrade GameData cost overflow")
}
want[key] += cost.Count
}
got := make(map[[2]uint64]uint64, len(materials))
var items []Item
var gold uint64
for _, material := range materials {
key := [2]uint64{material.Type, material.ID}
if got[key] > math.MaxUint64-material.Count {
return nil, 0, errors.New("player: talent upgrade material overflow")
}
got[key] += material.Count
if material.Type == 4 {
if gold != 0 || material.InvenIndex != 0 || material.ID != 0 {
return nil, 0, errors.New("player: invalid talent upgrade currency")
}
gold = material.Count
} else {
items = append(items, material)
}
}
if len(got) != len(want) {
return nil, 0, errors.New("player: talent upgrade material kinds mismatch")
}
for key, count := range want {
if got[key] != count {
return nil, 0, fmt.Errorf("player: talent upgrade material %d/%d=%d want=%d", key[0], key[1], got[key], count)
}
}
return items, gold, nil
}