Files
bd2/go/internal/server/commerce/clear_packages.go
T
Flechazo d8771caef2 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.
2026-10-05 11:50:31 +08:00

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
}