218 lines
6.0 KiB
Go
218 lines
6.0 KiB
Go
// Package npcinn implements motel opening and paid/free recovery. Each request
|
|
// runs within the account transaction, including currency, health and receipts.
|
|
package npcinn
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
)
|
|
|
|
type Context func(pack, npc uint64) (gamedata.InnRule, uint64, error)
|
|
type reply struct {
|
|
Digest string
|
|
Body []byte
|
|
}
|
|
type Service struct {
|
|
mu sync.Mutex
|
|
store stateio.Store
|
|
characters *player.CharacterStore
|
|
wallet *player.Wallet
|
|
context Context
|
|
level func() (uint64, error)
|
|
battle func() bool
|
|
session string
|
|
}
|
|
|
|
func New(store stateio.Store, characters *player.CharacterStore, wallet *player.Wallet, context Context, level func() (uint64, error), battle func() bool) (*Service, error) {
|
|
if store == nil || characters == nil || wallet == nil || context == nil || level == nil || battle == nil {
|
|
return nil, errors.New("npcinn: incomplete dependencies")
|
|
}
|
|
return &Service{store: store, characters: characters, wallet: wallet, context: context, level: level, battle: battle}, nil
|
|
}
|
|
func (s *Service) BeginSession(id string) { s.mu.Lock(); defer s.mu.Unlock(); s.session = id }
|
|
func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error) {
|
|
code := 109
|
|
if path == "/CharAllRevival" {
|
|
code = 11
|
|
} else if path != "/InnOpen" {
|
|
return 0, nil, false, nil
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
fail := func(e error) (int, []byte, bool, error) { return code, nil, true, e }
|
|
seq, ok, err := wire.Varint(request, 1)
|
|
if err != nil || !ok || seq == 0 || seq > math.MaxInt32 || s.session == "" {
|
|
return fail(errors.New("npcinn: invalid sequence or session"))
|
|
}
|
|
if err = wire.Walk(request, func(wire.Field) error { return nil }); err != nil {
|
|
return fail(err)
|
|
}
|
|
if s.battle() {
|
|
return fail(errors.New("npcinn: unavailable during battle"))
|
|
}
|
|
if path == "/InnOpen" {
|
|
_, state, err := s.context(0, 0)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
return code, wire.AppendVarint(nil, 1, state), true, nil
|
|
}
|
|
pack, _, err := wire.Varint(request, 2)
|
|
if err != nil || pack == 0 || pack > math.MaxInt32 {
|
|
return fail(errors.New("npcinn: invalid pack"))
|
|
}
|
|
npc, _, err := wire.Varint(request, 3)
|
|
if err != nil || npc == 0 || npc > math.MaxInt32 {
|
|
return fail(errors.New("npcinn: invalid NPC"))
|
|
}
|
|
keyBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%d", s.session, path, seq)))
|
|
key := hex.EncodeToString(keyBytes[:])
|
|
hash := sha256.Sum256(request)
|
|
digest := hex.EncodeToString(hash[:])
|
|
var replies map[string]reply
|
|
data, err := s.store.Load("npcinn")
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
if data != nil && json.Unmarshal(data, &replies) != nil {
|
|
return fail(errors.New("npcinn: invalid receipt store"))
|
|
}
|
|
if replies == nil {
|
|
replies = map[string]reply{}
|
|
}
|
|
if prior, exists := replies[key]; exists {
|
|
if prior.Digest != digest {
|
|
return fail(errors.New("npcinn: changed request replay"))
|
|
}
|
|
return code, prior.Body, true, nil
|
|
}
|
|
rule, state, err := s.context(pack, npc)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
level, err := s.level()
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
var indices []uint64
|
|
seen := map[uint64]bool{}
|
|
err = wire.Walk(request, func(f wire.Field) error {
|
|
if f.Number != 4 {
|
|
return nil
|
|
}
|
|
if f.Type != 0 && f.Type != 2 {
|
|
return errors.New("npcinn: invalid character list")
|
|
}
|
|
for raw := f.Value; len(raw) > 0; {
|
|
idx, n := binary.Uvarint(raw)
|
|
if n <= 0 || idx == 0 || idx > math.MaxInt64 || seen[idx] || len(indices) >= 4096 {
|
|
return errors.New("npcinn: invalid character index")
|
|
}
|
|
indices = append(indices, idx)
|
|
seen[idx] = true
|
|
raw = raw[n:]
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
if len(indices) == 0 {
|
|
return fail(errors.New("npcinn: empty recovery targets"))
|
|
}
|
|
discount := uint64(0)
|
|
if state == 2 {
|
|
discount = rule.GoodDiscount
|
|
}
|
|
if discount > 100 || rule.Currency != 4 || rule.ItemCount == 0 || rule.ItemCount > math.MaxInt32 {
|
|
return fail(errors.New("npcinn: invalid recovery design"))
|
|
}
|
|
rate := rule.ItemCount * (100 - discount)
|
|
const divisor uint64 = 4000
|
|
available := s.wallet.Snapshot().Gold
|
|
var total uint64
|
|
var targets []player.Character
|
|
for _, idx := range indices {
|
|
c, found := s.characters.Find(idx)
|
|
if !found {
|
|
return fail(errors.New("npcinn: recovery character not owned"))
|
|
}
|
|
maximum, err := s.characters.MaxHealth(idx)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
current, err := s.characters.CurrentHealth(idx)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
if current >= maximum {
|
|
c.HP = current
|
|
targets = append(targets, c)
|
|
continue
|
|
}
|
|
if current == 0 {
|
|
c.HP = 1
|
|
targets = append(targets, c)
|
|
continue
|
|
}
|
|
if level <= rule.FreeSquadLevel || rate == 0 {
|
|
c.HP = maximum
|
|
targets = append(targets, c)
|
|
continue
|
|
}
|
|
missing := maximum - current
|
|
if missing > (math.MaxUint64-divisor+1)/rate {
|
|
return fail(errors.New("npcinn: recovery price overflow"))
|
|
}
|
|
cost := (missing*rate + divisor - 1) / divisor
|
|
if cost <= available {
|
|
c.HP = maximum
|
|
} else {
|
|
if available > math.MaxUint64/divisor {
|
|
return fail(errors.New("npcinn: recovery amount overflow"))
|
|
}
|
|
heal := available * divisor / rate
|
|
if heal > missing {
|
|
heal = missing
|
|
}
|
|
cost = heal * rate / divisor
|
|
c.HP = current + heal
|
|
}
|
|
available -= cost
|
|
total += cost
|
|
targets = append(targets, c)
|
|
}
|
|
if total > 0 {
|
|
if _, err = s.wallet.SpendGoldOnce("npcinn:"+key, total); err != nil {
|
|
return fail(err)
|
|
}
|
|
}
|
|
var out []byte
|
|
for _, c := range targets {
|
|
if err = s.characters.SetCurrentHealth(c.InvenIndex, c.HP); err != nil {
|
|
return fail(err)
|
|
}
|
|
out = wire.AppendBytes(out, 1, player.CharacterWire(c))
|
|
}
|
|
replies[key] = reply{Digest: digest, Body: out}
|
|
data, err = json.Marshal(replies)
|
|
if err != nil {
|
|
return fail(err)
|
|
}
|
|
if err = s.store.Save("npcinn", data); err != nil {
|
|
return fail(err)
|
|
}
|
|
return code, out, true, nil
|
|
}
|