Files
bd2/go/internal/player/gacha_state.go
T
2026-09-21 09:53:46 +08:00

249 lines
7.4 KiB
Go

package player
import (
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"bd2server/internal/gamedata"
)
func gachaFixedKey(fixedID, fixedType uint64) string {
return fmt.Sprintf("%d:%d", fixedID, fixedType)
}
func applyGachaPurchase(next *collectionSnapshot, identity string, costumeIDs []uint64, purchase GachaPurchase, grant *CollectionGrant) {
if next.GachaApplied[identity] {
return
}
groupKey := strconv.FormatUint(purchase.Group.ID, 10)
user := next.GachaUsers[groupKey]
user.GroupID = purchase.Group.ID
point := purchase.Group.PointCount * uint64(len(costumeIDs))
user.Point += point
switch purchase.BuyType {
case 1: // GB_NORMAL
user.TotalBuyCount += uint64(len(costumeIDs))
case 2: // GB_CASH
if len(costumeIDs) == 1 {
user.OneCashPickCount++
} else {
user.TenCashPickCount++
}
}
next.GachaUsers[groupKey] = user
grant.GachaGroupID = purchase.Group.ID
grant.GachaPoint = point
grant.GachaFixed = append([]GachaFixedState(nil), purchase.Fixed...)
grant.SelectionApplySortIDs = append([]uint64(nil), purchase.SelectionApplySortIDs...)
for _, fixed := range purchase.Fixed {
stored := fixed
stored.ApplySort = -1
next.GachaFixed[gachaFixedKey(fixed.FixedID, fixed.Type)] = stored
}
next.GachaApplied[identity] = true
}
func (s *CollectionStore) GachaUsers() []GachaUserState {
s.mu.Lock()
defer s.mu.Unlock()
result := make([]GachaUserState, 0, len(s.data.GachaUsers))
for _, value := range s.data.GachaUsers {
result = append(result, value)
}
sort.Slice(result, func(i, j int) bool { return result[i].GroupID < result[j].GroupID })
return result
}
func (s *CollectionStore) GachaUser(groupID uint64) GachaUserState {
s.mu.Lock()
defer s.mu.Unlock()
return s.data.GachaUsers[strconv.FormatUint(groupID, 10)]
}
func (s *CollectionStore) GachaFixedStates() []GachaFixedState {
s.mu.Lock()
defer s.mu.Unlock()
result := make([]GachaFixedState, 0, len(s.data.GachaFixed))
for _, value := range s.data.GachaFixed {
value.ApplySort = -1
result = append(result, value)
}
sort.Slice(result, func(i, j int) bool {
if result[i].FixedID != result[j].FixedID {
return result[i].FixedID < result[j].FixedID
}
return result[i].Type < result[j].Type
})
return result
}
func (s *CollectionStore) GachaFixedCount(fixedID, fixedType uint64) uint64 {
s.mu.Lock()
defer s.mu.Unlock()
return s.data.GachaFixed[gachaFixedKey(fixedID, fixedType)].Count
}
// ExchangeGachaPoint performs the authoritative point debit. The exchange is
// saved before the wallet credit; a retry can therefore safely recover a
// wallet write using the same identity without spending the points twice.
func (s *CollectionStore) ExchangeGachaPoint(identity string, groupID, count uint64) (GachaPointExchange, error) {
if identity == "" || groupID == 0 || count == 0 {
return GachaPointExchange{}, errors.New("player: invalid gacha point exchange")
}
s.mu.Lock()
defer s.mu.Unlock()
if exchange, ok := s.data.GachaPointExchange[identity]; ok {
if exchange.GroupID != groupID || exchange.Count != count {
return GachaPointExchange{}, errors.New("player: gacha point exchange identity conflict")
}
return exchange, nil
}
key := strconv.FormatUint(groupID, 10)
user, ok := s.data.GachaUsers[key]
if !ok || user.Point < count {
return GachaPointExchange{}, errors.New("player: insufficient gacha point")
}
next := cloneCollection(s.data)
user.Point -= count
if math.MaxUint64-user.ExchangeMileageCount < count {
return GachaPointExchange{}, errors.New("player: gacha point exchange count overflow")
}
user.ExchangeMileageCount += count
next.GachaUsers[key] = user
exchange := GachaPointExchange{GroupID: groupID, Count: count}
next.GachaPointExchange[identity] = exchange
if err := s.commit(next); err != nil {
return GachaPointExchange{}, err
}
return exchange, nil
}
type legacyGachaDraw struct {
identity string
session string
seq uint64
gachaID uint64
grant CollectionGrant
}
// RepairGachaProgress is a one-time migration for grants written before the
// server persisted GachaUserDBInfo and GachaFixedDBInfo. Existing results are
// replayed in request order to recover points and resettable rarity counters.
func (s *CollectionStore) RepairGachaProgress(catalog *gamedata.RegularGachaCatalog) error {
if catalog == nil {
return errors.New("player: nil gacha catalog")
}
s.mu.Lock()
defer s.mu.Unlock()
if len(s.data.GachaApplied) != 0 || len(s.data.GachaUsers) != 0 || len(s.data.GachaFixed) != 0 {
if s.data.GachaCountCorrected {
return nil
}
next := cloneCollection(s.data)
counts := map[uint64]uint64{}
for identity, grant := range next.Grants {
parts := strings.Split(identity, ":")
if len(parts) < 4 || parts[0] != "regular-gacha" {
continue
}
gachaID, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
continue
}
group, ok := catalog.GroupForGacha(gachaID)
if ok && next.GachaApplied[identity] {
counts[group.ID] += uint64(len(grant.ViewCostumeIDs))
}
}
for groupID, count := range counts {
key := strconv.FormatUint(groupID, 10)
user := next.GachaUsers[key]
user.TotalBuyCount = count
next.GachaUsers[key] = user
}
next.GachaCountCorrected = true
return s.commit(next)
}
var draws []legacyGachaDraw
for identity, grant := range s.data.Grants {
parts := strings.Split(identity, ":")
if len(parts) < 4 || parts[0] != "regular-gacha" {
continue
}
gachaID, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
continue
}
if _, ok := catalog.GroupForGacha(gachaID); !ok {
continue
}
seq, err := strconv.ParseUint(parts[len(parts)-1], 10, 64)
if err != nil {
continue
}
session := ""
if len(parts) >= 6 && parts[2] == "session" && parts[4] == "seq" {
session = parts[3]
}
draws = append(draws, legacyGachaDraw{identity: identity, session: session, seq: seq, gachaID: gachaID, grant: grant})
}
if len(draws) == 0 {
return nil
}
sort.SliceStable(draws, func(i, j int) bool {
if draws[i].session != draws[j].session {
if draws[i].session == "" {
return true
}
if draws[j].session == "" {
return false
}
return draws[i].session < draws[j].session
}
return draws[i].seq < draws[j].seq
})
next := cloneCollection(s.data)
for _, draw := range draws {
group, _ := catalog.GroupForGacha(draw.gachaID)
key := strconv.FormatUint(group.ID, 10)
user := next.GachaUsers[key]
user.GroupID = group.ID
user.TotalBuyCount += uint64(len(draw.grant.ViewCostumeIDs))
user.Point += group.PointCount * uint64(len(draw.grant.ViewCostumeIDs))
next.GachaUsers[key] = user
if group.FixedID != 0 {
fourKey := gachaFixedKey(group.FixedID, 0)
fiveKey := gachaFixedKey(group.FixedID, 1)
four := next.GachaFixed[fourKey]
five := next.GachaFixed[fiveKey]
four.FixedID, four.Type, four.ApplySort = group.FixedID, 0, -1
five.FixedID, five.Type, five.ApplySort = group.FixedID, 1, -1
for _, costumeID := range draw.grant.ViewCostumeIDs {
grade, ok := catalog.CostumeGrade(costumeID)
if !ok {
return fmt.Errorf("player: gacha costume %d has no grade", costumeID)
}
switch grade {
case 5:
four.Count, five.Count = 0, 0
case 4:
four.Count = 0
five.Count++
default:
four.Count++
five.Count++
}
}
next.GachaFixed[fourKey] = four
next.GachaFixed[fiveKey] = five
}
next.GachaApplied[draw.identity] = true
}
next.GachaCountCorrected = true
return s.commit(next)
}