feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
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.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"bd2server/internal/server/gamedata"
|
||||
"bd2server/internal/server/player"
|
||||
"bd2server/internal/server/stateio"
|
||||
"bd2server/internal/server/wire"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type economyAPClock struct {
|
||||
Next int64 `json:"next"`
|
||||
}
|
||||
|
||||
func (e *Economy) AttachAPRefresh(caps map[uint64]uint64, design gamedata.HuntingAPDesign) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for typ, max := range caps {
|
||||
if (typ != 30 && typ != 32) || max == 0 || max > math.MaxInt32 {
|
||||
return fmt.Errorf("events: invalid free AP refresh")
|
||||
}
|
||||
}
|
||||
e.apCaps = caps
|
||||
e.resetSeconds = design.ResetSeconds
|
||||
e.now = time.Now
|
||||
return nil
|
||||
}
|
||||
func (e *Economy) refreshAP(s *economySnapshot) error {
|
||||
if len(e.apCaps) == 0 {
|
||||
return nil
|
||||
}
|
||||
now := e.now().UnixMilli()
|
||||
offset := (e.resetSeconds - 9*3600) * 1000
|
||||
next := ((now-offset)/86400000+1)*86400000 + offset
|
||||
b, err := e.store.Load("eventapclock")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clock := economyAPClock{Next: next}
|
||||
reset := b == nil
|
||||
if b != nil {
|
||||
if err = stateio.RequireExactJSONObject(b, "next"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = json.Unmarshal(b, &clock); err != nil {
|
||||
return err
|
||||
}
|
||||
reset = now >= clock.Next
|
||||
}
|
||||
if !reset {
|
||||
return nil
|
||||
}
|
||||
for typ, max := range e.apCaps {
|
||||
s.Balances[typ] = max
|
||||
}
|
||||
clock.Next = next
|
||||
raw, err := json.Marshal(clock)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = e.store.Save("eventapclock", raw); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err = json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.store.Save("event_economy", raw)
|
||||
}
|
||||
func (e *Economy) CanApply(costs []gamedata.Reward) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
s, err := e.load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = e.refreshAP(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
var wallet, ap []gamedata.Reward
|
||||
var items []player.Item
|
||||
totals := map[[2]uint64]uint64{}
|
||||
for _, r := range costs {
|
||||
k := [2]uint64{r.Type, r.ID}
|
||||
if r.Count == 0 || r.Count > math.MaxInt32 || totals[k] > math.MaxInt32-r.Count {
|
||||
return fmt.Errorf("events: invalid cost")
|
||||
}
|
||||
totals[k] += r.Count
|
||||
}
|
||||
for k, n := range totals {
|
||||
r := gamedata.Reward{Type: k[0], ID: k[1], Count: n}
|
||||
switch {
|
||||
case walletCurrency(r.Type):
|
||||
wallet = append(wallet, r)
|
||||
case apCurrency(r.Type):
|
||||
ap = append(ap, r)
|
||||
case extraCurrency(r.Type):
|
||||
if r.ID != 0 || s.Balances[r.Type] < n {
|
||||
return fmt.Errorf("events: insufficient currency")
|
||||
}
|
||||
case inventoryType(r.Type):
|
||||
selected, err := e.items.SelectMutable(r.Type, r.ID, n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items = append(items, selected...)
|
||||
default:
|
||||
return fmt.Errorf("events: unsupported cost type %d", r.Type)
|
||||
}
|
||||
}
|
||||
if err = e.wallet.CanExchange(wallet, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(ap) > 0 {
|
||||
if e.hunting == nil {
|
||||
return fmt.Errorf("events: AP unavailable")
|
||||
}
|
||||
if err = e.hunting.CanExchangeAP(ap, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
return e.items.CanConsume(items)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Economy) ChargeInfo() ([]byte, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
s, err := e.load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = e.refreshAP(&s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := e.store.Load("eventapclock")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var clock economyAPClock
|
||||
if err = json.Unmarshal(raw, &clock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []byte
|
||||
for _, typ := range []uint64{30, 32} {
|
||||
if _, ok := e.apCaps[typ]; !ok {
|
||||
continue
|
||||
}
|
||||
info := wire.AppendVarint(nil, 1, uint64(clock.Next-86400000))
|
||||
item := wire.AppendVarint(nil, 3, typ)
|
||||
item = wire.AppendVarint(item, 4, s.Balances[typ])
|
||||
info = wire.AppendBytes(info, 2, item)
|
||||
out = wire.AppendBytes(out, 1, info)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user