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

583 lines
20 KiB
Go

package player
import (
"errors"
"fmt"
"math"
"sort"
"strconv"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
)
const equipmentBatchLimit = 100
type equipmentAutoBreakResult struct {
Equipment []Equipment
Result uint64
Attempts uint64
Consumed []Item
Lack []Item
Gold uint64
Granted []Item
}
func (s *EquipmentInventory) makeEquipment(request []byte) (int, []byte, bool, error) {
seq, _, _ := wire.Varint(request, 1)
characterIndex, found, err := wire.Varint(request, 2)
if err != nil || !found || characterIndex == 0 {
return 0, nil, true, errors.New("player: EquipMaking missing character")
}
recipeID, found, err := wire.Varint(request, 3)
if err != nil || !found || recipeID == 0 {
return 0, nil, true, errors.New("player: EquipMaking missing recipe")
}
count, found, err := wire.Varint(request, 4)
if err != nil || !found || count == 0 || count > 1000 {
return 0, nil, true, errors.New("player: EquipMaking invalid count")
}
materials, err := equipmentRequestItems(request, 5, "EquipMaking")
if err != nil {
return 0, nil, true, err
}
cacheKey := s.smeltingCacheKey("making", seq)
if response, ok := s.cachedEquipmentReply(cacheKey); ok {
return response.code, response.body, true, nil
}
generated, gain, catalyst, maximum, err := s.prepareEquipmentMaking(characterIndex, recipeID, count, materials)
if err != nil {
return 0, nil, true, err
}
if err := s.inventory.Consume(materials); err != nil {
return 0, nil, true, fmt.Errorf("player: consume equipment making materials: %w", err)
}
if catalyst != 0 {
identity := "equip-making-catalyst:" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
if _, err := s.wallet.SpendCatalystOnce(identity, catalyst); err != nil {
return 0, nil, true, fmt.Errorf("player: spend equipment making catalyst: %w", err)
}
}
created, err := s.grantGeneratedBatch("equip-making:"+s.sessionID+":"+strconv.FormatUint(seq, 10), generated, true)
if err != nil {
return 0, nil, true, fmt.Errorf("player: persist made equipment: %w", err)
}
if _, err := s.characters.AddTalentExperience(characterIndex, gain, maximum); err != nil {
return 0, nil, true, err
}
var response []byte
for _, entry := range created {
response = wire.AppendBytes(response, 1, EquipmentWire(entry))
}
if gain != 0 {
response = wire.AppendVarint(response, 2, gain)
}
s.cacheEquipmentReply(cacheKey, 50, response)
return 50, response, true, nil
}
func (s *EquipmentInventory) prepareEquipmentMaking(characterIndex, recipeID, count uint64, materials []Item) ([]Equipment, uint64, uint64, uint64, error) {
if s.craft == nil || s.inventory == nil || s.characters == nil || s.wallet == nil {
return nil, 0, 0, 0, errors.New("player: equipment making unavailable")
}
recipe, ok := s.craft.Recipe(recipeID)
if !ok {
return nil, 0, 0, 0, fmt.Errorf("player: unknown equipment making recipe %d", recipeID)
}
character, ok := s.characters.Find(characterIndex)
if !ok {
return nil, 0, 0, 0, fmt.Errorf("player: unknown equipment making character %d", characterIndex)
}
gain, catalyst, maximum, err := s.craft.Talent(character.ID, character.TalentLevel, recipe.TalentLevel, count, character.TalentExp)
if err != nil {
return nil, 0, 0, 0, err
}
if catalyst != 0 && !s.wallet.CanSpendCatalyst(catalyst) {
return nil, 0, 0, 0, errors.New("player: insufficient catalyst for equipment making")
}
if err := validateMakingMaterials(recipe.Costs, count, materials); err != nil {
return nil, 0, 0, 0, err
}
if err := s.inventory.CanConsume(materials); err != nil {
return nil, 0, 0, 0, err
}
if recipe.ResultCount != 0 && count > math.MaxUint64/recipe.ResultCount {
return nil, 0, 0, 0, errors.New("player: equipment making result count overflow")
}
resultCount := count * recipe.ResultCount
generated := make([]Equipment, 0, resultCount)
for i := uint64(0); i < resultCount; i++ {
rolled, err := s.craft.Generate(recipeID)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("player: roll equipment making result: %w", err)
}
entry := Equipment{ID: rolled.Design.ID, Rank: []uint64{0, 0, 0}}
for _, option := range rolled.Main {
entry.MainOption = append(entry.MainOption, EquipmentOption{GroupID: option.GroupID, ID: option.ID})
}
for _, option := range rolled.Sub {
entry.SubOption = append(entry.SubOption, EquipmentOption{GroupID: option.GroupID, ID: option.ID})
}
if rolled.Private != nil {
entry.PrivateOption = &EquipmentOption{GroupID: rolled.Private.GroupID, ID: rolled.Private.ID}
}
generated = append(generated, entry)
}
return generated, gain, catalyst, maximum, nil
}
func validateMakingMaterials(costs []gamedata.PromotionCost, count uint64, materials []Item) error {
want := make(map[[2]uint64]uint64, len(costs))
for _, cost := range costs {
if cost.Count == 0 || count > math.MaxUint64/cost.Count {
return errors.New("player: equipment making material cost overflow")
}
key := [2]uint64{cost.Type, cost.ID}
value := cost.Count * count
if math.MaxUint64-want[key] < value {
return errors.New("player: equipment making material cost overflow")
}
want[key] += value
}
got := make(map[[2]uint64]uint64, len(materials))
for _, item := range materials {
key := [2]uint64{item.Type, item.ID}
if math.MaxUint64-got[key] < item.Count {
return errors.New("player: equipment making material count overflow")
}
got[key] += item.Count
}
if len(got) != len(want) {
return errors.New("player: equipment making material kinds mismatch")
}
for key, count := range want {
if got[key] != count {
return fmt.Errorf("player: equipment making material %d/%d=%d want=%d", key[0], key[1], got[key], count)
}
}
return nil
}
func (s *EquipmentInventory) breakEquipment(request []byte) (int, []byte, bool, error) {
seq, _, _ := wire.Varint(request, 1)
indices, err := repeatedEquipmentIndices(request, 2, equipmentBatchLimit)
if err != nil {
return 0, nil, true, err
}
cacheKey := s.smeltingCacheKey("break", seq)
if response, ok := s.cachedEquipmentReply(cacheKey); ok {
return response.code, response.body, true, nil
}
s.mu.Lock()
if s.upgrade == nil || s.inventory == nil {
s.mu.Unlock()
return 0, nil, true, errors.New("player: equipment break unavailable")
}
rewards := make(map[[2]uint64]uint64)
positions := make([]int, 0, len(indices))
for _, index := range indices {
position := s.equipmentPositionLocked(index)
if position < 0 {
s.mu.Unlock()
return 0, nil, true, fmt.Errorf("player: EquipBreak unknown equipment %d", index)
}
entry := s.owned.Equipment[position]
if entry.UseChar != 0 || entry.LockFlag != 0 || !s.upgrade.CanBreak(entry.ID) {
s.mu.Unlock()
return 0, nil, true, fmt.Errorf("player: equipment %d cannot be broken", index)
}
result, err := s.upgrade.BreakRewards(entry.ID, entry.Level)
if err != nil {
s.mu.Unlock()
return 0, nil, true, err
}
if err := addBattleRewards(rewards, result); err != nil {
s.mu.Unlock()
return 0, nil, true, err
}
positions = append(positions, position)
}
sort.Sort(sort.Reverse(sort.IntSlice(positions)))
next := cloneEquipmentSnapshot(s.owned)
for _, position := range positions {
next.Equipment = append(next.Equipment[:position], next.Equipment[position+1:]...)
}
if err := s.commitLocked(next, "break"); err != nil {
s.mu.Unlock()
return 0, nil, true, err
}
s.mu.Unlock()
identity := "equip-break:" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
granted, err := s.inventory.GrantOnce(identity, aggregateBattleRewards(rewards))
if err != nil {
return 0, nil, true, fmt.Errorf("player: grant equipment break rewards: %w", err)
}
if granted == nil {
granted = s.inventory.GrantedItems(identity)
}
response := wire.AppendBytes(nil, 1, rewardItemBundle(granted))
s.cacheEquipmentReply(cacheKey, 56, response)
return 56, response, true, nil
}
func (s *EquipmentInventory) upgradeToBreakAuto(request []byte) (int, []byte, bool, error) {
seq, _, _ := wire.Varint(request, 1)
indices, err := repeatedEquipmentIndices(request, 2, equipmentBatchLimit)
if err != nil {
return 0, nil, true, err
}
target, _, err := wire.Varint(request, 3)
if err != nil {
return 0, nil, true, errors.New("player: EquipUpgradeToBreakAuto invalid target")
}
cacheKey := s.smeltingCacheKey("upgrade-break-auto", seq)
if response, ok := s.cachedEquipmentReply(cacheKey); ok {
return response.code, response.body, true, nil
}
result, err := s.runUpgradeToBreak(indices, target, "equip-upgrade-break:"+s.sessionID+":"+strconv.FormatUint(seq, 10))
if err != nil {
return 0, nil, true, err
}
response := encodeUpgradeBreakResponse(result, 1, 2, 3, 4, 5, 6, 7)
s.cacheEquipmentReply(cacheKey, 516, response)
return 516, response, true, nil
}
func (s *EquipmentInventory) makeToBreakAuto(request []byte) (int, []byte, bool, error) {
seq, _, _ := wire.Varint(request, 1)
characterIndex, found, err := wire.Varint(request, 2)
if err != nil || !found || characterIndex == 0 {
return 0, nil, true, errors.New("player: EquipMakingToBreakAuto missing character")
}
recipeID, found, err := wire.Varint(request, 3)
if err != nil || !found || recipeID == 0 {
return 0, nil, true, errors.New("player: EquipMakingToBreakAuto missing recipe")
}
count, found, err := wire.Varint(request, 4)
if err != nil || !found || count == 0 || count > equipmentBatchLimit {
return 0, nil, true, errors.New("player: EquipMakingToBreakAuto invalid count")
}
materials, err := equipmentRequestItems(request, 5, "EquipMakingToBreakAuto")
if err != nil {
return 0, nil, true, err
}
target, _, err := wire.Varint(request, 6)
if err != nil {
return 0, nil, true, errors.New("player: EquipMakingToBreakAuto invalid target")
}
cacheKey := s.smeltingCacheKey("making-break-auto", seq)
if response, ok := s.cachedEquipmentReply(cacheKey); ok {
return response.code, response.body, true, nil
}
generated, gain, catalyst, maximum, err := s.prepareEquipmentMaking(characterIndex, recipeID, count, materials)
if err != nil {
return 0, nil, true, err
}
if err := s.inventory.Consume(materials); err != nil {
return 0, nil, true, fmt.Errorf("player: consume equipment making materials: %w", err)
}
if catalyst != 0 {
identity := "equip-making-break-catalyst:" + s.sessionID + ":" + strconv.FormatUint(seq, 10)
if _, err := s.wallet.SpendCatalystOnce(identity, catalyst); err != nil {
return 0, nil, true, fmt.Errorf("player: spend automatic equipment making catalyst: %w", err)
}
}
created, err := s.grantGeneratedBatch("", generated, false)
if err != nil {
return 0, nil, true, fmt.Errorf("player: persist auto-break made equipment: %w", err)
}
indices := make([]uint64, 0, len(created))
for _, entry := range created {
indices = append(indices, entry.InvenIndex)
}
if _, err := s.characters.AddTalentExperience(characterIndex, gain, maximum); err != nil {
return 0, nil, true, err
}
result, err := s.runUpgradeToBreak(indices, target, "equip-making-break-reward:"+s.sessionID+":"+strconv.FormatUint(seq, 10))
if err != nil {
return 0, nil, true, err
}
var response []byte
if gain != 0 {
response = wire.AppendVarint(response, 1, gain)
}
response = append(response, encodeUpgradeBreakResponse(result, 2, 3, 4, 5, 6, 7, 8)...)
s.cacheEquipmentReply(cacheKey, 515, response)
return 515, response, true, nil
}
func (s *EquipmentInventory) grantGeneratedBatch(identityPrefix string, entries []Equipment, trackReplay bool) ([]Equipment, error) {
if len(entries) == 0 || (trackReplay && identityPrefix == "") {
return nil, errors.New("player: invalid generated equipment batch")
}
s.mu.Lock()
defer s.mu.Unlock()
if trackReplay {
existing := make([]Equipment, 0, len(entries))
found := 0
for i := range entries {
index := s.owned.Granted[identityPrefix+":"+strconv.Itoa(i)]
if index == 0 {
continue
}
found++
position := s.equipmentPositionLocked(index)
if position < 0 {
return nil, errors.New("player: generated equipment replay index is missing")
}
existing = append(existing, cloneEquipment(s.owned.Equipment[position]))
}
if found != 0 {
if found != len(entries) {
return nil, errors.New("player: partial generated equipment replay batch")
}
return existing, nil
}
}
next := cloneEquipmentSnapshot(s.owned)
created := make([]Equipment, 0, len(entries))
for i, source := range entries {
entry := cloneEquipment(source)
if entry.ID == 0 || len(entry.Rank) != 3 {
return nil, errors.New("player: invalid generated equipment in batch")
}
entry.InvenIndex = next.NextIndex
next.NextIndex++
next.Equipment = append(next.Equipment, entry)
created = append(created, cloneEquipment(entry))
if trackReplay {
next.Granted[identityPrefix+":"+strconv.Itoa(i)] = entry.InvenIndex
}
}
if err := s.commitLocked(next, "generated equipment batch"); err != nil {
return nil, err
}
return created, nil
}
func (s *EquipmentInventory) runUpgradeToBreak(indices []uint64, target uint64, rewardIdentity string) (equipmentAutoBreakResult, error) {
s.mu.Lock()
if s.upgrade == nil || s.wallet == nil || s.inventory == nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, errors.New("player: automatic equipment upgrade and break unavailable")
}
for _, index := range indices {
position := s.equipmentPositionLocked(index)
if position < 0 {
s.mu.Unlock()
return equipmentAutoBreakResult{}, fmt.Errorf("player: unknown equipment %d", index)
}
entry := s.owned.Equipment[position]
if entry.UseChar != 0 || entry.LockFlag != 0 || !s.upgrade.CanBreak(entry.ID) {
s.mu.Unlock()
return equipmentAutoBreakResult{}, fmt.Errorf("player: equipment %d cannot be automatically broken", index)
}
}
result := equipmentAutoBreakResult{Result: equipUpgradeStopTargetLevel}
stopUpgrades := false
for _, index := range indices {
position := s.equipmentPositionLocked(index)
entry := s.owned.Equipment[position]
maximum := s.upgrade.MaxLevel[entry.ID]
goal := target
if goal > maximum {
goal = maximum
}
for !stopUpgrades && entry.Level < goal {
level, _, err := s.upgrade.Level(entry.ID, entry.Level)
if err != nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, err
}
materials, gold, err := s.selectUpgradeCosts(level.Costs)
if err != nil || (gold != 0 && !s.wallet.CanSpendGold(gold)) {
result.Result = equipUpgradeStopNotEnough
result.Lack = upgradeLackItems(level.Costs)
stopUpgrades = true
break
}
updated, _, spent, consumed, err := s.attemptUpgradeLocked(index, materials)
if err != nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, err
}
result.Attempts++
result.Gold += spent
for _, item := range consumed {
if item.Type != 4 {
result.Consumed = append(result.Consumed, item)
}
}
entry = updated
if result.Attempts > 300000 {
s.mu.Unlock()
return equipmentAutoBreakResult{}, errors.New("player: automatic equipment upgrade exceeded safety limit")
}
}
position = s.equipmentPositionLocked(index)
result.Equipment = append(result.Equipment, cloneEquipment(s.owned.Equipment[position]))
}
rewards := make(map[[2]uint64]uint64)
positions := make([]int, 0, len(indices))
for _, entry := range result.Equipment {
breakRewards, err := s.upgrade.BreakRewards(entry.ID, entry.Level)
if err != nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, err
}
if err := addBattleRewards(rewards, breakRewards); err != nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, err
}
positions = append(positions, s.equipmentPositionLocked(entry.InvenIndex))
}
sort.Sort(sort.Reverse(sort.IntSlice(positions)))
next := cloneEquipmentSnapshot(s.owned)
for _, position := range positions {
next.Equipment = append(next.Equipment[:position], next.Equipment[position+1:]...)
}
if err := s.commitLocked(next, "automatic upgrade and break"); err != nil {
s.mu.Unlock()
return equipmentAutoBreakResult{}, err
}
s.mu.Unlock()
granted, err := s.inventory.GrantOnce(rewardIdentity, aggregateBattleRewards(rewards))
if err != nil {
return equipmentAutoBreakResult{}, fmt.Errorf("player: grant automatic equipment break rewards: %w", err)
}
if granted == nil {
granted = s.inventory.GrantedItems(rewardIdentity)
}
result.Granted = granted
result.Consumed = aggregateItemCounts(result.Consumed)
result.Lack = aggregateItemCounts(result.Lack)
return result, nil
}
func encodeUpgradeBreakResponse(result equipmentAutoBreakResult, equipmentField, resultField, attemptsField, consumedField, lackField, goldField, rewardsField int) []byte {
var response []byte
for _, entry := range result.Equipment {
response = wire.AppendBytes(response, equipmentField, EquipmentWire(entry))
}
if result.Result != 0 {
response = wire.AppendVarint(response, resultField, result.Result)
}
if result.Attempts != 0 {
response = wire.AppendVarint(response, attemptsField, result.Attempts)
}
for _, item := range result.Consumed {
response = wire.AppendBytes(response, consumedField, ItemWire(item))
}
for _, item := range result.Lack {
response = wire.AppendBytes(response, lackField, ItemWire(item))
}
if result.Gold != 0 {
response = wire.AppendVarint(response, goldField, result.Gold)
}
response = wire.AppendBytes(response, rewardsField, rewardItemBundle(result.Granted))
return response
}
func repeatedEquipmentIndices(request []byte, number, maximum int) ([]uint64, error) {
var result []uint64
err := wire.Walk(request, func(field wire.Field) error {
if field.Number != number {
return nil
}
values, err := decodeRepeatedUint64(field)
if err != nil {
return err
}
result = append(result, values...)
return nil
})
if err != nil || len(result) == 0 || len(result) > maximum {
return nil, errors.New("player: invalid equipment index batch")
}
seen := make(map[uint64]bool, len(result))
for _, index := range result {
if index == 0 || seen[index] {
return nil, errors.New("player: invalid or duplicate equipment index")
}
seen[index] = true
}
return result, nil
}
func addBattleRewards(total map[[2]uint64]uint64, rewards []gamedata.BattleReward) error {
for _, reward := range rewards {
key := [2]uint64{reward.Type, reward.ID}
if reward.Count == 0 || math.MaxUint64-total[key] < reward.Count {
return errors.New("player: equipment break reward overflow")
}
total[key] += reward.Count
}
return nil
}
func aggregateBattleRewards(values map[[2]uint64]uint64) []gamedata.BattleReward {
keys := make([][2]uint64, 0, len(values))
for key, count := range values {
if count != 0 {
keys = append(keys, key)
}
}
sort.Slice(keys, func(i, j int) bool {
if keys[i][0] != keys[j][0] {
return keys[i][0] < keys[j][0]
}
return keys[i][1] < keys[j][1]
})
out := make([]gamedata.BattleReward, 0, len(keys))
for _, key := range keys {
out = append(out, gamedata.BattleReward{Type: key[0], ID: key[1], Count: values[key]})
}
return out
}
func aggregateItemCounts(items []Item) []Item {
values := make(map[[2]uint64]uint64)
for _, item := range items {
values[[2]uint64{item.Type, item.ID}] += item.Count
}
keys := make([][2]uint64, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i][0] != keys[j][0] {
return keys[i][0] < keys[j][0]
}
return keys[i][1] < keys[j][1]
})
out := make([]Item, 0, len(keys))
for _, key := range keys {
out = append(out, Item{Type: key[0], ID: key[1], Count: values[key]})
}
return out
}
func rewardItemBundle(items []Item) []byte {
var bundle []byte
for _, item := range items {
bundle = wire.AppendBytes(bundle, 1, ItemWire(item))
}
return bundle
}
func (s *EquipmentInventory) cachedEquipmentReply(key string) (smeltingReply, bool) {
s.mu.Lock()
defer s.mu.Unlock()
reply, ok := s.smeltCache[key]
if ok {
reply.body = append([]byte(nil), reply.body...)
}
return reply, ok
}
func (s *EquipmentInventory) cacheEquipmentReply(key string, code int, body []byte) {
s.mu.Lock()
defer s.mu.Unlock()
s.smeltCache[key] = smeltingReply{code: code, body: append([]byte(nil), body...)}
}