229 lines
7.0 KiB
Go
229 lines
7.0 KiB
Go
package player
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func applyStepUpProgress(next *collectionSnapshot, groupID, step uint64) error {
|
|
if groupID == 0 && step == 0 {
|
|
return nil
|
|
}
|
|
if groupID == 0 || step == 0 {
|
|
return errors.New("player: incomplete step-up purchase")
|
|
}
|
|
key := strconv.FormatUint(groupID, 10)
|
|
completed := next.StepUpProgress[key]
|
|
if completed >= step {
|
|
return nil
|
|
}
|
|
if completed+1 != step {
|
|
return fmt.Errorf("player: step-up group %d expects step %d, got %d", groupID, completed+1, step)
|
|
}
|
|
next.StepUpProgress[key] = step
|
|
return nil
|
|
}
|
|
|
|
func gachaFixedKey(fixedID, fixedType uint64) string {
|
|
return fmt.Sprintf("%d:%d", fixedID, fixedType)
|
|
}
|
|
|
|
func dailyGachaPrefix(day string, groupID, buyType uint64) string {
|
|
return fmt.Sprintf("daily-gacha:%s:%d:%d:", day, groupID, buyType)
|
|
}
|
|
|
|
func applyGachaPurchase(next *collectionSnapshot, identity string, costumeIDs []uint64, purchase GachaPurchase, grant *CollectionGrant) error {
|
|
if next.GachaApplied[identity] {
|
|
return nil
|
|
}
|
|
groupKey := strconv.FormatUint(purchase.Group.ID, 10)
|
|
user := next.GachaUsers[groupKey]
|
|
user.GroupID = purchase.Group.ID
|
|
if purchase.DailyLimit != 0 {
|
|
if purchase.DailyKey == "" || (purchase.BuyType != 0 && purchase.BuyType != 2) {
|
|
return errors.New("player: invalid daily gacha purchase")
|
|
}
|
|
prefix := dailyGachaPrefix(purchase.DailyKey, purchase.Group.ID, purchase.BuyType)
|
|
var used uint64
|
|
for key, applied := range next.GachaApplied {
|
|
if applied && strings.HasPrefix(key, prefix) {
|
|
used++
|
|
}
|
|
}
|
|
if used >= purchase.DailyLimit {
|
|
return fmt.Errorf("player: daily gacha group %d buy type %d exhausted", purchase.Group.ID, purchase.BuyType)
|
|
}
|
|
next.GachaApplied[prefix+identity] = true
|
|
}
|
|
rewardCount := uint64(len(costumeIDs))
|
|
if purchase.RewardCount != 0 {
|
|
rewardCount = purchase.RewardCount
|
|
}
|
|
point := purchase.Group.PointCount * rewardCount
|
|
user.Point += point
|
|
switch purchase.BuyType {
|
|
case 1: // GB_NORMAL
|
|
user.TotalBuyCount += rewardCount
|
|
case 2: // GB_CASH outside the date-scoped daily discount
|
|
if purchase.DailyLimit == 0 {
|
|
if rewardCount == 1 {
|
|
user.OneCashPickCount++
|
|
} else {
|
|
user.TenCashPickCount++
|
|
}
|
|
}
|
|
case 3: // GB_CASH_CONTENT_TICKET
|
|
user.TotalBuyCount += rewardCount
|
|
}
|
|
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
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// GachaUsersForDay returns cumulative gacha state with daily free/paid counts
|
|
// rebuilt from durable, date-scoped purchase markers. Historical counts never
|
|
// leak into a new reset day and no storage migration is required.
|
|
func (s *CollectionStore) GachaUsersForDay(day string) []GachaUserState {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
byGroup := make(map[uint64]GachaUserState, len(s.data.GachaUsers))
|
|
for _, value := range s.data.GachaUsers {
|
|
value.OneFreePickCount = 0
|
|
value.OneCashPickCount = 0
|
|
value.TenFreePickCount = 0
|
|
value.TenCashPickCount = 0
|
|
byGroup[value.GroupID] = value
|
|
}
|
|
prefix := "daily-gacha:" + day + ":"
|
|
for key, applied := range s.data.GachaApplied {
|
|
if !applied || !strings.HasPrefix(key, prefix) {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(strings.TrimPrefix(key, prefix), ":", 3)
|
|
if len(parts) != 3 {
|
|
continue
|
|
}
|
|
groupID, groupErr := strconv.ParseUint(parts[0], 10, 64)
|
|
buyType, typeErr := strconv.ParseUint(parts[1], 10, 64)
|
|
if groupErr != nil || typeErr != nil || groupID == 0 {
|
|
continue
|
|
}
|
|
user := byGroup[groupID]
|
|
user.GroupID = groupID
|
|
switch buyType {
|
|
case 0:
|
|
user.OneFreePickCount++
|
|
case 2:
|
|
user.OneCashPickCount++
|
|
}
|
|
byGroup[groupID] = user
|
|
}
|
|
result := make([]GachaUserState, 0, len(byGroup))
|
|
for _, value := range byGroup {
|
|
result = append(result, value)
|
|
}
|
|
sort.Slice(result, func(i, j int) bool { return result[i].GroupID < result[j].GroupID })
|
|
return result
|
|
}
|
|
|
|
func (s *CollectionStore) GachaDailyCount(day string, groupID, buyType uint64) uint64 {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
prefix := dailyGachaPrefix(day, groupID, buyType)
|
|
var count uint64
|
|
for key, applied := range s.data.GachaApplied {
|
|
if applied && strings.HasPrefix(key, prefix) {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
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
|
|
}
|