feat(server): complete field talents, crafting, party recovery and pack monster lifecycle

This commit is contained in:
2026-10-05 23:14:47 +08:00
parent e30023053e
commit 76a2d72f05
64 changed files with 6208 additions and 246 deletions
+209
View File
@@ -0,0 +1,209 @@
package player
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"sync"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
)
type itemCraftReceipt struct {
Digest string
Body []byte
}
type ItemCraftService struct {
mu sync.Mutex
design *gamedata.ItemCraftDesign
talents *gamedata.TalentUseDesign
store stateio.Store
items *Inventory
characters *CharacterStore
wallet *Wallet
known func(uint64) bool
context func() (int, bool, error)
session string
}
func NewItemCraftService(d *gamedata.ItemCraftDesign, talents *gamedata.TalentUseDesign, store stateio.Store, items *Inventory, characters *CharacterStore, wallet *Wallet, known func(uint64) bool) (*ItemCraftService, error) {
if d == nil || talents == nil || store == nil || items == nil || characters == nil || wallet == nil || known == nil {
return nil, fmt.Errorf("craft: missing dependencies")
}
return &ItemCraftService{design: d, talents: talents, store: store, items: items, characters: characters, wallet: wallet, known: known}, nil
}
func (s *ItemCraftService) AttachContext(context func() (int, bool, error)) { s.context = context }
func (s *ItemCraftService) BeginSession(id string) { s.mu.Lock(); defer s.mu.Unlock(); s.session = id }
func (s *ItemCraftService) Handle(path string, request []byte) (int, []byte, bool, error) {
var recipes map[uint64]gamedata.ItemCraftRecipe
var code int
switch path {
case "/Cooking":
recipes = s.design.Cooking
code = 48
case "/Alchemy":
recipes = s.design.Alchemy
code = 49
case "/AlchemyBatch":
recipes = s.design.Alchemy
code = 262
default:
return 0, nil, false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
fail := func(err error) (int, []byte, bool, error) { return code, nil, true, err }
if s.session == "" || s.context == nil {
return fail(fmt.Errorf("craft: session/context unavailable"))
}
values := map[int]uint64{}
err := wire.Walk(request, func(f wire.Field) error {
if f.Number >= 1 && f.Number <= 4 {
if f.Type != 0 {
return fmt.Errorf("craft: invalid scalar")
}
if _, exists := values[f.Number]; exists {
return fmt.Errorf("craft: duplicate scalar")
}
v, _, err := wire.Varint(request, f.Number)
if err != nil {
return err
}
values[f.Number] = v
}
return nil
})
if err != nil {
return fail(err)
}
seq, index, recipeID, count := values[1], values[2], values[3], values[4]
if seq == 0 || seq > math.MaxInt32 || index == 0 || index > math.MaxInt64 || recipeID == 0 || recipeID > math.MaxInt32 || count == 0 || count > math.MaxInt32 {
return fail(fmt.Errorf("craft: invalid request"))
}
keyHash := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%d", s.session, path, seq)))
key := hex.EncodeToString(keyHash[:])
digestHash := sha256.Sum256(request)
digest := hex.EncodeToString(digestHash[:])
var receipts map[string]itemCraftReceipt
raw, err := s.store.Load("itemcraft")
if err != nil {
return fail(err)
}
if raw != nil && json.Unmarshal(raw, &receipts) != nil {
return fail(fmt.Errorf("craft: invalid receipt state"))
}
if receipts == nil {
receipts = map[string]itemCraftReceipt{}
}
if r, exists := receipts[key]; exists {
if r.Digest != digest {
return fail(fmt.Errorf("craft: changed request replay"))
}
return code, r.Body, true, nil
}
pack, battle, err := s.context()
if err != nil {
return fail(err)
}
if battle || pack <= 0 {
return fail(fmt.Errorf("craft: unavailable in current scene"))
}
recipe, exists := recipes[recipeID]
if !exists {
return fail(fmt.Errorf("craft: unknown recipe"))
}
if recipe.Class == 7 && !s.known(recipeID) {
return fail(fmt.Errorf("craft: recipe not learned"))
}
character, owned := s.characters.Find(index)
if !owned || IsStoryCharacter(character) {
return fail(fmt.Errorf("craft: unavailable producer"))
}
talent, exists := s.talents.Characters[character.ID]
if !exists || talent.BannedPacks[pack] {
return fail(fmt.Errorf("craft: producer talent blocked in pack"))
}
current := s.talents.Rules[[2]uint64{talent.Group, character.TalentLevel}]
limitIndex := 0
if recipe.Class == 7 {
limitIndex = 1
}
if len(current.Values) <= limitIndex || count > uint64(current.Values[limitIndex]) {
return fail(fmt.Errorf("craft: count exceeds talent limit"))
}
gain, catalyst, maximum, err := s.talents.CraftTalent(character.ID, character.TalentLevel, recipe.Class, recipe.TalentLevel, count, character.TalentExp)
if err != nil {
return fail(err)
}
materials, err := equipmentRequestItems(request, 5, path)
if err != nil {
return fail(err)
}
if path == "/AlchemyBatch" {
gain, catalyst, maximum, err = s.prepareAlchemyBatch(character, recipe, count, materials)
} else {
err = validateMakingMaterials(recipe.Costs, count, materials)
// Conversion recipes charge per produced resource, as AlchemyUI.GetNeededCurrency does.
if err == nil && recipe.Class == 8 && recipe.Category == 2 {
if recipe.Result.Count == 0 || catalyst > math.MaxInt32/recipe.Result.Count {
err = fmt.Errorf("craft: catalyst overflow")
} else {
catalyst *= recipe.Result.Count
}
}
}
if err != nil {
return fail(err)
}
if err = s.items.CanConsume(materials); err != nil {
return fail(err)
}
if catalyst > 0 && !s.wallet.CanSpendCatalyst(catalyst) {
return fail(fmt.Errorf("craft: insufficient catalyst"))
}
if recipe.Result.Count > math.MaxInt32/count {
return fail(fmt.Errorf("craft: result quantity overflow"))
}
result := recipe.Result
result.Count *= count
if path == "/AlchemyBatch" {
result.Count = count
}
if err = s.items.Consume(materials); err != nil {
return fail(err)
}
if catalyst > 0 {
if _, err = s.wallet.SpendCatalystOnce("itemcraft:"+key, catalyst); err != nil {
return fail(err)
}
}
granted, err := s.items.GrantOnce("itemcraft:"+key, []gamedata.BattleReward{result})
if err != nil {
return fail(err)
}
if gain > 0 {
if _, err = s.characters.AddTalentExperience(index, gain, maximum); err != nil {
return fail(err)
}
}
var response []byte
for _, item := range granted {
response = wire.AppendBytes(response, 1, ItemWire(item))
}
if gain > 0 {
response = wire.AppendVarint(response, 2, gain)
}
receipts[key] = itemCraftReceipt{Digest: digest, Body: response}
raw, err = json.Marshal(receipts)
if err != nil {
return fail(err)
}
if err = s.store.Save("itemcraft", raw); err != nil {
return fail(err)
}
return code, response, true, nil
}