Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling. Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata. Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks. Improve automatic login and recovery diagnostics; document configuration and release flows.
225 lines
5.2 KiB
Go
225 lines
5.2 KiB
Go
package hunting
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/versionconfig"
|
|
"bd2server/internal/server/wire"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
type battleReceipt struct {
|
|
Fingerprint string
|
|
Bundle []byte
|
|
Monsters [][]byte
|
|
}
|
|
type battleLedger struct {
|
|
Version string `json:"version"`
|
|
Receipts map[string]battleReceipt `json:"receipts"`
|
|
}
|
|
|
|
func (s *Service) loadBattleReceipts() (map[string]battleReceipt, error) {
|
|
b, e := s.storage.Load("huntingbattle")
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if b == nil {
|
|
return map[string]battleReceipt{}, nil
|
|
}
|
|
if e = stateio.RequireExactJSONObject(b, "version", "receipts"); e != nil {
|
|
return nil, e
|
|
}
|
|
var l battleLedger
|
|
d := json.NewDecoder(bytes.NewReader(b))
|
|
d.DisallowUnknownFields()
|
|
if e = d.Decode(&l); e != nil {
|
|
return nil, e
|
|
}
|
|
if l.Version != versionconfig.State() || l.Receipts == nil {
|
|
return nil, fmt.Errorf("hunting: invalid battle ledger")
|
|
}
|
|
return l.Receipts, nil
|
|
}
|
|
func (s *Service) saveBattleReceipts(rs map[string]battleReceipt) error {
|
|
b, e := json.Marshal(battleLedger{versionconfig.State(), rs})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
return s.storage.Save("huntingbattle", b)
|
|
}
|
|
func (s *Service) AttachEligibility(check func(int, uint64) error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.eligibility = check
|
|
}
|
|
func (s *Service) AttachRewards(grant func(string, []gamedata.Reward) ([]byte, error)) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.grant = grant
|
|
}
|
|
|
|
// EnsureForPack initializes the ordinary stage when the world loads a pack.
|
|
func (s *Service) EnsureForPack(pack int) ([]byte, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
d, e := s.load(pack)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if len(d.Grounds) == 0 {
|
|
return nil, nil
|
|
}
|
|
st := s.state.Packs[strconv.Itoa(pack)]
|
|
if st.Current == 0 {
|
|
st.Current = d.Grounds[0].ID
|
|
next := s.clone()
|
|
next.Packs[strconv.Itoa(pack)] = st
|
|
if e = s.persist(next); e != nil {
|
|
return nil, e
|
|
}
|
|
}
|
|
return s.info(pack, d)
|
|
}
|
|
func (s *Service) SnapshotForPack(pack int) ([]byte, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
d, e := s.load(pack)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if len(d.Grounds) == 0 {
|
|
return nil, nil
|
|
}
|
|
return s.info(pack, d)
|
|
}
|
|
|
|
// Ground mutations have their own persisted session+sequence replay ledger.
|
|
func (s *Service) handleGroundSession(path string, req []byte, session string) (int, []byte, bool, error) {
|
|
if path != "/HuntingGroundEnter" {
|
|
return s.Handle(path, req)
|
|
}
|
|
seq, found, e := wire.Varint(req, 1)
|
|
if e != nil || !found || seq == 0 {
|
|
return 110, nil, true, fmt.Errorf("hunting: missing sequence")
|
|
}
|
|
key := "enter:" + session + ":" + strconv.FormatUint(seq, 10)
|
|
rs, e := s.loadBattleReceipts()
|
|
if e != nil {
|
|
return 110, nil, true, e
|
|
}
|
|
fp := fmt.Sprintf("%x", req)
|
|
if r, ok := rs[key]; ok {
|
|
if r.Fingerprint != fp {
|
|
return 110, nil, true, fmt.Errorf("hunting: enter sequence conflict")
|
|
}
|
|
return 110, r.Bundle, true, nil
|
|
}
|
|
code, out, ok, e := s.Handle(path, req)
|
|
if e != nil || !ok {
|
|
return code, out, ok, e
|
|
}
|
|
rs[key] = battleReceipt{Fingerprint: fp, Bundle: out}
|
|
if e = s.saveBattleReceipts(rs); e != nil {
|
|
return code, nil, true, e
|
|
}
|
|
return code, out, ok, nil
|
|
}
|
|
func (s *Service) reload() error {
|
|
b, e := s.storage.Load("hunting")
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if b == nil {
|
|
return nil
|
|
}
|
|
var st snapshot
|
|
if e = json.Unmarshal(b, &st); e != nil {
|
|
return e
|
|
}
|
|
s.state = st
|
|
return nil
|
|
}
|
|
func (s *Service) CanExchangeAP(costs, rewards []gamedata.Reward) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if e := s.reload(); e != nil {
|
|
return e
|
|
}
|
|
if e := s.refreshAP(); e != nil {
|
|
return e
|
|
}
|
|
_, e := s.exchangeAP(costs, rewards)
|
|
return e
|
|
}
|
|
func (s *Service) exchangeAP(costs, rewards []gamedata.Reward) (snapshot, error) {
|
|
st := s.clone()
|
|
apply := func(rs []gamedata.Reward, cost bool) error {
|
|
for _, r := range rs {
|
|
if (r.Type != 21 && r.Type != 23) || r.ID != 0 || r.Count == 0 {
|
|
return fmt.Errorf("hunting: invalid AP exchange")
|
|
}
|
|
p := &st.Free
|
|
if r.Type == 23 {
|
|
p = &st.Bonus
|
|
}
|
|
if cost {
|
|
if *p < r.Count {
|
|
return fmt.Errorf("hunting: insufficient AP")
|
|
}
|
|
*p -= r.Count
|
|
} else {
|
|
if r.Count > 2147483647-*p {
|
|
return fmt.Errorf("hunting: AP overflow")
|
|
}
|
|
*p += r.Count
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if e := apply(costs, true); e != nil {
|
|
return st, e
|
|
}
|
|
return st, apply(rewards, false)
|
|
}
|
|
func (s *Service) ExchangeAPOnce(identity string, costs, rewards []gamedata.Reward) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if identity == "" {
|
|
return fmt.Errorf("hunting: missing AP identity")
|
|
}
|
|
if e := s.reload(); e != nil {
|
|
return e
|
|
}
|
|
key := "apexchange:" + identity
|
|
fpb, e := json.Marshal([][]gamedata.Reward{costs, rewards})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
rs, e := s.loadBattleReceipts()
|
|
if e != nil {
|
|
return e
|
|
}
|
|
fp := string(fpb)
|
|
if r, ok := rs[key]; ok {
|
|
if r.Fingerprint != fp {
|
|
return fmt.Errorf("hunting: AP identity conflict")
|
|
}
|
|
return nil
|
|
}
|
|
if e := s.refreshAP(); e != nil {
|
|
return e
|
|
}
|
|
st, e := s.exchangeAP(costs, rewards)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if e = s.persist(st); e != nil {
|
|
return e
|
|
}
|
|
rs[key] = battleReceipt{Fingerprint: fp}
|
|
return s.saveBattleReceipts(rs)
|
|
}
|