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.
187 lines
5.7 KiB
Go
187 lines
5.7 KiB
Go
package commerce
|
|
|
|
import (
|
|
"bd2server/internal/server/gamedata"
|
|
"bd2server/internal/server/player"
|
|
"bd2server/internal/server/stateio"
|
|
"bd2server/internal/server/wire"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type ClearPackageInventory interface{ All() []player.Item }
|
|
type clearClaim struct{ Kind, GroupID, TicketID, TargetID, Level uint64 }
|
|
type clearClaimReceipt struct {
|
|
Claim clearClaim `json:"claim"`
|
|
Response []byte `json:"response"`
|
|
}
|
|
type ClearPackages struct {
|
|
mu sync.Mutex
|
|
store stateio.Store
|
|
economy Economy
|
|
items ClearPackageInventory
|
|
design map[clearClaim]gamedata.ClearPackageRewardDesign
|
|
packCleared func(uint64, uint64) bool
|
|
towerCleared func(uint64, uint64) bool
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewClearPackages(store stateio.Store, design *gamedata.ClearPackageCatalog, economy Economy, items ClearPackageInventory) (*ClearPackages, error) {
|
|
if store == nil || design == nil || economy == nil || items == nil {
|
|
return nil, fmt.Errorf("commerce: invalid clear package dependencies")
|
|
}
|
|
s := &ClearPackages{store: store, economy: economy, items: items, design: map[clearClaim]gamedata.ClearPackageRewardDesign{}, now: time.Now}
|
|
for _, r := range design.Rewards {
|
|
k := clearClaim{r.Kind, r.GroupID, r.TicketID, r.TargetID, r.Level}
|
|
if _, ok := s.design[k]; ok {
|
|
return nil, fmt.Errorf("commerce: duplicate clear reward")
|
|
}
|
|
s.design[k] = r
|
|
}
|
|
_, err := s.load()
|
|
return s, err
|
|
}
|
|
func (s *ClearPackages) AttachProgress(pack, tower func(uint64, uint64) bool) {
|
|
s.packCleared = pack
|
|
s.towerCleared = tower
|
|
}
|
|
func (s *ClearPackages) load() (map[string]clearClaimReceipt, error) {
|
|
v := map[string]clearClaimReceipt{}
|
|
raw, err := s.store.Load("commerce_clear_claims")
|
|
if err != nil || raw == nil {
|
|
return v, err
|
|
}
|
|
err = json.Unmarshal(raw, &v)
|
|
if err == nil && v == nil {
|
|
err = fmt.Errorf("commerce: invalid clear claim state")
|
|
}
|
|
if err == nil {
|
|
for identity, receipt := range v {
|
|
if identity != clearClaimID(receipt.Claim) || len(receipt.Response) == 0 {
|
|
err = fmt.Errorf("commerce: invalid saved clear claim")
|
|
break
|
|
}
|
|
if _, ok := s.design[receipt.Claim]; !ok {
|
|
err = fmt.Errorf("commerce: unknown saved clear claim")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return v, err
|
|
}
|
|
func clearClaimID(c clearClaim) string {
|
|
return fmt.Sprintf("clear-package:%d:%d:%d:%d:%d", c.Kind, c.GroupID, c.TicketID, c.TargetID, c.Level)
|
|
}
|
|
func (s *ClearPackages) entitled(ticket uint64) bool {
|
|
for _, item := range s.items.All() {
|
|
if item.Type == 19 && item.ID == ticket && item.Count > 0 && (item.ExpiryTime == 0 || item.ExpiryTime > uint64(s.now().UnixMilli())) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
func (s *ClearPackages) Handle(path string, request []byte) (int, []byte, bool, error) {
|
|
return s.HandleSession(path, request, "")
|
|
}
|
|
func (s *ClearPackages) HandleSession(path string, request []byte, session string) (int, []byte, bool, error) {
|
|
if path != "/ClearPackageReward" {
|
|
return 0, nil, false, nil
|
|
}
|
|
seq, ok, err := wire.Varint(request, 1)
|
|
if err != nil || !ok || seq == 0 {
|
|
return 286, nil, true, fmt.Errorf("commerce: invalid clear claim sequence")
|
|
}
|
|
kind, _, err := wire.Varint(request, 2)
|
|
if err != nil || kind > 1 {
|
|
return 286, nil, true, fmt.Errorf("commerce: invalid clear claim type")
|
|
}
|
|
var nested []byte
|
|
err = wire.Walk(request, func(f wire.Field) error {
|
|
if f.Number == 3 || f.Number == 4 {
|
|
if int(f.Number) != int(kind)+3 || f.Type != 2 || nested != nil {
|
|
return fmt.Errorf("commerce: invalid clear claim row")
|
|
}
|
|
nested = append([]byte(nil), f.Value...)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil || nested == nil {
|
|
return 286, nil, true, fmt.Errorf("commerce: missing clear claim row")
|
|
}
|
|
var values [4]uint64
|
|
for i := range values {
|
|
values[i], _, err = wire.Varint(nested, i+1)
|
|
if err != nil {
|
|
return 286, nil, true, err
|
|
}
|
|
}
|
|
claim := clearClaim{kind, values[0], values[1], values[2], values[3]}
|
|
d, ok := s.design[claim]
|
|
if !ok {
|
|
return 286, nil, true, fmt.Errorf("commerce: unknown clear reward")
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
v, err := s.load()
|
|
if err != nil {
|
|
return 286, nil, true, err
|
|
}
|
|
identity := clearClaimID(claim)
|
|
if receipt, ok := v[identity]; ok {
|
|
return 286, append([]byte(nil), receipt.Response...), true, nil
|
|
}
|
|
if d.Type == 1 && !s.entitled(d.TicketID) {
|
|
return 286, nil, true, fmt.Errorf("commerce: clear reward premium ticket required")
|
|
}
|
|
proof := s.packCleared
|
|
if kind == 1 {
|
|
proof = s.towerCleared
|
|
}
|
|
if proof == nil || !proof(d.TargetID, d.Level) {
|
|
return 286, nil, true, fmt.Errorf("commerce: clear reward progression incomplete")
|
|
}
|
|
bundle, err := s.economy.Apply(identity, nil, []gamedata.Reward{{Type: 9, ID: d.RandomBoxID, Count: 1}})
|
|
if err != nil {
|
|
return 286, nil, true, err
|
|
}
|
|
response := wire.AppendBytes(nil, 1, bundle)
|
|
v[identity] = clearClaimReceipt{Claim: claim, Response: response}
|
|
raw, err := json.Marshal(v)
|
|
if err == nil {
|
|
err = s.store.Save("commerce_clear_claims", raw)
|
|
}
|
|
return 286, response, true, err
|
|
}
|
|
|
|
// RewardDBInfos is attached to PackInfoResponse fields 3 and 4 so reconnects
|
|
// restore claimed reward buttons from server state.
|
|
func (s *ClearPackages) RewardDBInfos() (pack, evil [][]byte, err error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
v, err := s.load()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
keys := make([]string, 0, len(v))
|
|
for k := range v {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
c := v[k].Claim
|
|
b := wire.AppendVarint(nil, 1, c.GroupID)
|
|
b = wire.AppendVarint(b, 2, c.TicketID)
|
|
b = wire.AppendVarint(b, 3, c.TargetID)
|
|
b = wire.AppendVarint(b, 4, c.Level)
|
|
if c.Kind == 0 {
|
|
pack = append(pack, b)
|
|
} else {
|
|
evil = append(evil, b)
|
|
}
|
|
}
|
|
return pack, evil, nil
|
|
}
|