mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
249 lines
7.7 KiB
Go
249 lines
7.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
pb "lunar-tear/server/gen/proto"
|
|
"lunar-tear/server/internal/gametime"
|
|
"lunar-tear/server/internal/masterdata"
|
|
"lunar-tear/server/internal/model"
|
|
"lunar-tear/server/internal/store"
|
|
"lunar-tear/server/internal/userdata"
|
|
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
var shopDiffTables = []string{
|
|
"IUserShopItem",
|
|
"IUserShopReplaceable",
|
|
"IUserShopReplaceableLineup",
|
|
"IUserGem",
|
|
"IUserConsumableItem",
|
|
"IUserMaterial",
|
|
"IUserImportantItem",
|
|
"IUserPremiumItem",
|
|
"IUserStatus",
|
|
"IUserCostume",
|
|
"IUserCostumeActiveSkill",
|
|
"IUserCharacter",
|
|
"IUserWeapon",
|
|
"IUserWeaponSkill",
|
|
"IUserWeaponAbility",
|
|
"IUserWeaponNote",
|
|
"IUserWeaponStory",
|
|
}
|
|
|
|
type ShopServiceServer struct {
|
|
pb.UnimplementedShopServiceServer
|
|
users store.UserRepository
|
|
sessions store.SessionRepository
|
|
catalog *masterdata.ShopCatalog
|
|
granter *store.PossessionGranter
|
|
}
|
|
|
|
func NewShopServiceServer(users store.UserRepository, sessions store.SessionRepository, catalog *masterdata.ShopCatalog, granter *store.PossessionGranter) *ShopServiceServer {
|
|
return &ShopServiceServer{users: users, sessions: sessions, catalog: catalog, granter: granter}
|
|
}
|
|
|
|
func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.BuyResponse, error) {
|
|
log.Printf("[ShopService] Buy: shopId=%d items=%v", req.ShopId, req.ShopItems)
|
|
|
|
userId := currentUserId(ctx, s.users, s.sessions)
|
|
nowMillis := gametime.NowMillis()
|
|
|
|
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
|
for shopItemId, qty := range req.ShopItems {
|
|
item, ok := s.catalog.Items[shopItemId]
|
|
if !ok {
|
|
log.Printf("[ShopService] Buy: unknown shopItemId=%d, skipping", shopItemId)
|
|
continue
|
|
}
|
|
|
|
totalPrice := item.Price * qty
|
|
if err := store.DeductPrice(user, item.PriceType, item.PriceId, totalPrice); err != nil {
|
|
log.Printf("[ShopService] Buy: deduct failed shopItemId=%d: %v", shopItemId, err)
|
|
continue
|
|
}
|
|
|
|
for _, content := range s.catalog.Contents[shopItemId] {
|
|
s.granter.GrantFull(user,
|
|
model.PossessionType(content.PossessionType),
|
|
content.PossessionId,
|
|
content.Count*qty,
|
|
nowMillis,
|
|
)
|
|
}
|
|
|
|
s.applyContentEffects(user, shopItemId, qty, nowMillis)
|
|
|
|
si := user.ShopItems[shopItemId]
|
|
si.ShopItemId = shopItemId
|
|
si.BoughtCount += qty
|
|
si.LatestBoughtCountChangedDatetime = nowMillis
|
|
si.LatestVersion = nowMillis
|
|
user.ShopItems[shopItemId] = si
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("shop buy: %w", err)
|
|
}
|
|
|
|
tables := userdata.FullClientTableMap(snapshot)
|
|
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
|
|
|
return &pb.BuyResponse{
|
|
OverflowPossession: []*pb.Possession{},
|
|
DiffUserData: diff,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.RefreshRequest) (*pb.RefreshResponse, error) {
|
|
log.Printf("[ShopService] RefreshUserData: isGemUsed=%v", req.IsGemUsed)
|
|
|
|
userId := currentUserId(ctx, s.users, s.sessions)
|
|
nowMillis := gametime.NowMillis()
|
|
|
|
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
|
if len(user.ShopReplaceableLineup) == 0 && len(s.catalog.ItemShopPool) > 0 {
|
|
for i, itemId := range s.catalog.ItemShopPool {
|
|
slot := int32(i + 1)
|
|
user.ShopReplaceableLineup[slot] = store.UserShopReplaceableLineupState{
|
|
SlotNumber: slot,
|
|
ShopItemId: itemId,
|
|
LatestVersion: nowMillis,
|
|
}
|
|
}
|
|
}
|
|
if req.IsGemUsed {
|
|
user.ShopReplaceable.LineupUpdateCount++
|
|
user.ShopReplaceable.LatestLineupUpdateDatetime = nowMillis
|
|
for _, itemId := range s.catalog.ItemShopPool {
|
|
if si, ok := user.ShopItems[itemId]; ok {
|
|
si.BoughtCount = 0
|
|
si.LatestVersion = nowMillis
|
|
user.ShopItems[itemId] = si
|
|
}
|
|
}
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("shop refresh: %w", err)
|
|
}
|
|
|
|
tables := userdata.FullClientTableMap(snapshot)
|
|
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
|
|
|
return &pb.RefreshResponse{
|
|
DiffUserData: diff,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ShopServiceServer) GetCesaLimit(_ context.Context, _ *emptypb.Empty) (*pb.GetCesaLimitResponse, error) {
|
|
log.Printf("[ShopService] GetCesaLimit")
|
|
return &pb.GetCesaLimitResponse{
|
|
CesaLimit: []*pb.CesaLimit{},
|
|
DiffUserData: userdata.EmptyDiff(),
|
|
}, nil
|
|
}
|
|
|
|
func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *pb.CreatePurchaseTransactionRequest) (*pb.CreatePurchaseTransactionResponse, error) {
|
|
log.Printf("[ShopService] CreatePurchaseTransaction: shopId=%d shopItemId=%d productId=%s",
|
|
req.ShopId, req.ShopItemId, req.ProductId)
|
|
|
|
userId := currentUserId(ctx, s.users, s.sessions)
|
|
nowMillis := gametime.NowMillis()
|
|
|
|
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
|
|
item, ok := s.catalog.Items[req.ShopItemId]
|
|
if !ok {
|
|
log.Printf("[ShopService] CreatePurchaseTransaction: unknown shopItemId=%d", req.ShopItemId)
|
|
return
|
|
}
|
|
|
|
if err := store.DeductPrice(user, item.PriceType, item.PriceId, item.Price); err != nil {
|
|
log.Printf("[ShopService] CreatePurchaseTransaction: deduct failed: %v", err)
|
|
}
|
|
|
|
for _, content := range s.catalog.Contents[req.ShopItemId] {
|
|
s.granter.GrantFull(user,
|
|
model.PossessionType(content.PossessionType),
|
|
content.PossessionId,
|
|
content.Count,
|
|
nowMillis,
|
|
)
|
|
}
|
|
|
|
s.applyContentEffects(user, req.ShopItemId, 1, nowMillis)
|
|
|
|
si := user.ShopItems[req.ShopItemId]
|
|
si.ShopItemId = req.ShopItemId
|
|
si.BoughtCount++
|
|
if item.ShopItemLimitedStockId > 0 {
|
|
if maxCount, ok := s.catalog.LimitedStock[item.ShopItemLimitedStockId]; ok && si.BoughtCount >= maxCount {
|
|
si.BoughtCount = 0
|
|
}
|
|
}
|
|
si.LatestBoughtCountChangedDatetime = nowMillis
|
|
si.LatestVersion = nowMillis
|
|
user.ShopItems[req.ShopItemId] = si
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create purchase transaction: %w", err)
|
|
}
|
|
|
|
txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis)
|
|
|
|
tables := userdata.FullClientTableMap(snapshot)
|
|
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
|
|
|
return &pb.CreatePurchaseTransactionResponse{
|
|
PurchaseTransactionId: txId,
|
|
DiffUserData: diff,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context, req *pb.PurchaseGooglePlayStoreProductRequest) (*pb.PurchaseGooglePlayStoreProductResponse, error) {
|
|
log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId)
|
|
|
|
userId := currentUserId(ctx, s.users, s.sessions)
|
|
snapshot, err := s.users.SnapshotUser(userId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("purchase google play: %w", err)
|
|
}
|
|
|
|
tables := userdata.FullClientTableMap(snapshot)
|
|
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
|
|
|
|
return &pb.PurchaseGooglePlayStoreProductResponse{
|
|
OverflowPossession: []*pb.Possession{},
|
|
DiffUserData: diff,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ShopServiceServer) applyContentEffects(user *store.UserState, shopItemId, qty int32, nowMillis int64) {
|
|
for _, effect := range s.catalog.Effects[shopItemId] {
|
|
switch effect.EffectTargetType {
|
|
case model.EffectTargetStaminaRecovery:
|
|
maxMillis := s.catalog.MaxStaminaMillis[user.Status.Level]
|
|
millis := s.resolveEffectMillis(effect.EffectValueType, effect.EffectValue, user.Status.Level)
|
|
store.RecoverStamina(user, millis*qty, maxMillis, nowMillis)
|
|
default:
|
|
log.Printf("[ShopService] unhandled effect: shopItemId=%d targetType=%d", shopItemId, effect.EffectTargetType)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *ShopServiceServer) resolveEffectMillis(effectValueType, effectValue, userLevel int32) int32 {
|
|
switch effectValueType {
|
|
case model.EffectValueFixed:
|
|
return effectValue
|
|
case model.EffectValuePermil:
|
|
maxMillis := s.catalog.MaxStaminaMillis[userLevel]
|
|
return effectValue * maxMillis / 1000
|
|
default:
|
|
return 0
|
|
}
|
|
}
|