Files
bd2/go/internal/server/deck/auto_recovery.go
T

243 lines
6.4 KiB
Go

package deck
import (
"bd2server/internal/server/player"
"bd2server/internal/server/stateio"
"bd2server/internal/server/wire"
"crypto/sha256"
"encoding/json"
"fmt"
"sort"
)
func (s *Store) AttachAutoRecovery(f func(uint64, uint64, []uint64) (player.AutoRecoveryResult, error)) {
s.mu.Lock()
defer s.mu.Unlock()
s.autoRecovery = f
}
func (s *Store) AttachAutoRecoveryAllowed(f func() (bool, error)) {
s.mu.Lock()
defer s.mu.Unlock()
s.autoRecoveryAllowed = f
}
type autoRecoveryReceipt struct {
Digest string
Body []byte
}
func (s *Store) handleAutoRecovery(req []byte) (int, []byte, bool, error) {
fail := func(e error) (int, []byte, bool, error) { return 373, nil, true, e }
if e := checkSeq(req); e != nil {
return fail(e)
}
caster, _, e := wire.Varint(req, 2)
if e != nil {
return fail(e)
}
seq, _, _ := wire.Varint(req, 1)
s.mu.Lock()
defer s.mu.Unlock()
key := s.sessionID + ":" + fmt.Sprint(seq)
digest := fmt.Sprintf("%x", sha256.Sum256(req))
if s.storage != nil {
raw, ok, err := s.storage.LoadEntry("deck", "auto_recovery", key)
if err != nil {
return fail(err)
}
if ok {
var r autoRecoveryReceipt
if err = json.Unmarshal(raw, &r); err != nil {
return fail(err)
}
if r.Digest != digest {
return fail(fmt.Errorf("deck: changed automatic recovery replay"))
}
return 373, r.Body, true, nil
}
}
n := clone(s.state)
if len(n.Deck) == 0 {
return fail(fmt.Errorf("deck: automatic recovery has no battle deck"))
}
result := player.AutoRecoveryResult{Caster: caster, Catalyst: n.AutoReviveCatalyst}
if s.wallet != nil {
result.Catalyst = s.wallet.CatalystBalance()
}
mode := uint64(2)
var settings fieldSettings
if s.characters != nil {
settings, e = s.loadFieldSettings()
if e != nil {
return fail(e)
}
if caster != settings.Caster {
return fail(fmt.Errorf("deck: automatic recovery caster differs from saved setting"))
}
if n.FieldCharControlDeckType == 2 {
allExhausted := true
for _, v := range n.Deck {
hp, err := s.characters.CurrentHealth(v.CharacterInvenIndex)
if err != nil {
return fail(err)
}
if hp > 0 {
allExhausted = false
}
}
if allExhausted {
mode = 3
}
}
field := s.visibleFieldDeckLocked()
var targets []uint64
seen := map[uint64]bool{}
add := func(id uint64) error {
if seen[id] {
return nil
}
seen[id] = true
c, ok := s.characters.Find(id)
if !ok || player.IsStoryCharacter(c) || player.IsCharmCharacter(c) {
return nil
}
hp, err := s.characters.CurrentHealth(id)
if err != nil {
return err
}
if hp == 0 {
targets = append(targets, id)
}
return nil
}
if n.FieldCharControlDeckType == 0 { //nolint:staticcheck // QF1003
for _, v := range n.Deck {
if e = add(v.CharacterInvenIndex); e != nil {
return fail(e)
}
}
} else if n.FieldCharControlDeckType == 1 {
for _, v := range field {
if e = add(v.CharacterInvenIndex); e != nil {
return fail(e)
}
}
}
allowed := true
if s.autoRecoveryAllowed != nil {
allowed, e = s.autoRecoveryAllowed()
if e != nil {
return fail(e)
}
}
if settings.AutoRevive && allowed && len(targets) > 0 {
if s.autoRecovery == nil {
return fail(fmt.Errorf("deck: automatic recovery executor unavailable"))
}
result, e = s.autoRecovery(seq, caster, targets)
if e != nil {
return fail(e)
}
settings.Caster = result.Caster
if len(result.Characters) > 0 {
mode = 1
}
}
// On recovery failure, replace fatigued party members with living,
// permanent owned characters in inventory order. Keep the fatigued
// member when no replacement exists so the client can show exhaustion.
if mode != 1 && len(targets) > 0 {
all := s.characters.RawAll()
sort.Slice(all, func(i, j int) bool { return all[i].InvenIndex < all[j].InvenIndex })
used := map[uint64]bool{}
for _, v := range n.Deck {
used[v.CharacterInvenIndex] = true
}
for _, v := range field {
used[v.CharacterInvenIndex] = true
}
replacements := map[uint64]player.Character{}
for _, id := range targets {
for _, c := range all {
if used[c.InvenIndex] || player.IsStoryCharacter(c) || player.IsCharmCharacter(c) {
continue
}
hp, err := s.characters.CurrentHealth(c.InvenIndex)
if err != nil {
return fail(err)
}
if hp == 0 {
continue
}
replacements[id] = c
used[c.InvenIndex] = true
break
}
if _, ok := replacements[id]; !ok {
mode = 3
}
}
for i, v := range n.Deck {
if c, ok := replacements[v.CharacterInvenIndex]; ok {
n.Deck[i].CharacterInvenIndex = c.InvenIndex
}
}
for i, v := range n.FieldDeck {
if c, ok := replacements[v.CharacterInvenIndex]; ok {
n.FieldDeck[i].CharacterInvenIndex = c.InvenIndex
n.FieldDeck[i].CostumeInvenIndex = c.UseCostume
}
}
}
} else if caster != 0 {
return fail(fmt.Errorf("deck: automatic recovery character provider unavailable"))
}
var out []byte
for _, v := range n.Deck {
b := wire.AppendVarint(nil, 1, v.CharacterInvenIndex)
b = wire.AppendVarint(b, 2, v.CostumeInvenIndex)
b = wire.AppendVarint(b, 3, v.Slot)
out = wire.AppendBytes(out, 1, b)
}
for _, v := range n.FieldDeck {
b := wire.AppendVarint(nil, 1, v.Slot)
b = wire.AppendVarint(b, 2, v.CharacterInvenIndex)
b = wire.AppendVarint(b, 3, v.CostumeInvenIndex)
out = wire.AppendBytes(out, 2, b)
}
for _, c := range result.Characters {
out = wire.AppendVarint(out, 3, c.InvenIndex)
out = wire.AppendBytes(out, 5, player.CharacterWire(c))
}
out = wire.AppendVarint(out, 4, mode)
if result.Caster != 0 {
out = wire.AppendVarint(out, 9, result.Caster)
}
out = wire.AppendVarint(out, 6, result.Experience)
out = wire.AppendVarint(out, 7, result.Catalyst)
out = wire.AppendVarint(out, 8, result.Disabled)
if s.storage != nil {
core, err := json.Marshal(n)
if err != nil {
return fail(err)
}
raw, err := json.Marshal(autoRecoveryReceipt{digest, out})
if err != nil {
return fail(err)
}
changes := []stateio.EntryMutation{{Bucket: "auto_recovery", Key: key, Payload: raw}}
if s.characters != nil {
raw, err = json.Marshal(settings)
if err != nil {
return fail(err)
}
changes = append(changes, stateio.EntryMutation{Bucket: "field_settings", Key: "state", Payload: raw})
}
if err = s.storage.SaveWithEntries("deck", core, changes); err != nil {
return fail(err)
}
}
s.state = n
return 373, out, true, nil
}