Add SQLite persistence, import-snapshot tool, and karma functionality

This commit is contained in:
Ilya Groshev
2026-04-20 09:57:47 +03:00
parent c9ad3fa4f4
commit c33e738fd5
70 changed files with 4151 additions and 833 deletions
+42
View File
@@ -0,0 +1,42 @@
package database
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
func Open(path string) (*sql.DB, error) {
if dir := filepath.Dir(path); dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create db directory %q: %w", dir, err)
}
}
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("open sqlite %q: %w", path, err)
}
pragmas := []string{
"PRAGMA journal_mode=WAL",
"PRAGMA foreign_keys=ON",
"PRAGMA busy_timeout=5000",
}
for _, p := range pragmas {
if _, err := db.Exec(p); err != nil {
db.Close()
return nil, fmt.Errorf("exec %q: %w", p, err)
}
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("ping sqlite %q: %w", path, err)
}
return db, nil
}
+6
View File
@@ -35,6 +35,9 @@ type GameConfig struct {
QuestSkipMaxCountAtOnce int32
WeaponLimitBreakAvailableCount int32
CostumeLotteryEffectUnlockSlotConsumeGold int32
CostumeLotteryEffectDrawSlotConsumeGold int32
}
func LoadGameConfig() (*GameConfig, error) {
@@ -73,6 +76,9 @@ func LoadGameConfig() (*GameConfig, error) {
cfg.WeaponLimitBreakAvailableCount = parseInt32(kv, "WEAPON_LIMIT_BREAK_AVAILABLE_COUNT")
cfg.CostumeLotteryEffectUnlockSlotConsumeGold = parseInt32(kv, "COSTUME_LOTTERY_EFFECT_UNLOCK_SLOT_CONSUME_GOLD")
cfg.CostumeLotteryEffectDrawSlotConsumeGold = parseInt32(kv, "COSTUME_LOTTERY_EFFECT_DRAW_SLOT_CONSUME_GOLD")
return cfg, nil
}
+59
View File
@@ -78,6 +78,31 @@ type CostumeActiveSkillEnhanceMaterialRow struct {
SortOrder int32 `json:"SortOrder"`
}
type CostumeLotteryEffectRow struct {
CostumeId int32 `json:"CostumeId"`
SlotNumber int32 `json:"SlotNumber"`
CostumeLotteryEffectOddsGroupId int32 `json:"CostumeLotteryEffectOddsGroupId"`
CostumeLotteryEffectUnlockMaterialGroupId int32 `json:"CostumeLotteryEffectUnlockMaterialGroupId"`
CostumeLotteryEffectDrawMaterialGroupId int32 `json:"CostumeLotteryEffectDrawMaterialGroupId"`
CostumeLotteryEffectReleaseScheduleId int32 `json:"CostumeLotteryEffectReleaseScheduleId"`
}
type CostumeLotteryEffectMaterialGroupRow struct {
CostumeLotteryEffectMaterialGroupId int32 `json:"CostumeLotteryEffectMaterialGroupId"`
MaterialId int32 `json:"MaterialId"`
Count int32 `json:"Count"`
SortOrder int32 `json:"SortOrder"`
}
type CostumeLotteryEffectOddsRow struct {
CostumeLotteryEffectOddsGroupId int32 `json:"CostumeLotteryEffectOddsGroupId"`
OddsNumber int32 `json:"OddsNumber"`
Weight int32 `json:"Weight"`
CostumeLotteryEffectType int32 `json:"CostumeLotteryEffectType"`
CostumeLotteryEffectTargetId int32 `json:"CostumeLotteryEffectTargetId"`
RarityType int32 `json:"RarityType"`
}
type CostumeCatalog struct {
Costumes map[int32]CostumeMasterRow
Materials map[int32]MaterialRow
@@ -96,6 +121,10 @@ type CostumeCatalog struct {
ActiveSkillEnhanceMats map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow // key: [enhancementMaterialId, skillLevel]
ActiveSkillMaxLevelByRarity map[int32]NumericalFunc
ActiveSkillCostByRarity map[int32]NumericalFunc
LotteryEffects map[[2]int32]CostumeLotteryEffectRow // key: [costumeId, slotNumber]
LotteryEffectMats map[int32][]CostumeLotteryEffectMaterialGroupRow // key: materialGroupId (both unlock and draw)
LotteryEffectOdds map[int32][]CostumeLotteryEffectOddsRow // key: oddsGroupId
}
func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
@@ -149,6 +178,19 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
return nil, fmt.Errorf("load costume active skill enhancement material table: %w", err)
}
lotteryEffectRows, err := utils.ReadJSON[CostumeLotteryEffectRow]("EntityMCostumeLotteryEffectTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect table: %w", err)
}
lotteryEffectMatRows, err := utils.ReadJSON[CostumeLotteryEffectMaterialGroupRow]("EntityMCostumeLotteryEffectMaterialGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect material group table: %w", err)
}
lotteryEffectOddsRows, err := utils.ReadJSON[CostumeLotteryEffectOddsRow]("EntityMCostumeLotteryEffectOddsGroupTable.json")
if err != nil {
return nil, fmt.Errorf("load costume lottery effect odds group table: %w", err)
}
catalog := &CostumeCatalog{
Costumes: make(map[int32]CostumeMasterRow, len(costumes)),
Materials: matCatalog.ByType[model.MaterialTypeCostumeEnhancement],
@@ -167,6 +209,10 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
ActiveSkillEnhanceMats: make(map[[2]int32][]CostumeActiveSkillEnhanceMaterialRow),
ActiveSkillMaxLevelByRarity: make(map[int32]NumericalFunc, len(rarities)),
ActiveSkillCostByRarity: make(map[int32]NumericalFunc, len(rarities)),
LotteryEffects: make(map[[2]int32]CostumeLotteryEffectRow, len(lotteryEffectRows)),
LotteryEffectMats: make(map[int32][]CostumeLotteryEffectMaterialGroupRow),
LotteryEffectOdds: make(map[int32][]CostumeLotteryEffectOddsRow),
}
for _, row := range costumes {
@@ -242,5 +288,18 @@ func LoadCostumeCatalog(matCatalog *MaterialCatalog) (*CostumeCatalog, error) {
catalog.ActiveSkillEnhanceMats[key] = append(catalog.ActiveSkillEnhanceMats[key], row)
}
for _, row := range lotteryEffectRows {
key := [2]int32{row.CostumeId, row.SlotNumber}
catalog.LotteryEffects[key] = row
}
for _, row := range lotteryEffectMatRows {
gid := row.CostumeLotteryEffectMaterialGroupId
catalog.LotteryEffectMats[gid] = append(catalog.LotteryEffectMats[gid], row)
}
for _, row := range lotteryEffectOddsRows {
gid := row.CostumeLotteryEffectOddsGroupId
catalog.LotteryEffectOdds[gid] = append(catalog.LotteryEffectOdds[gid], row)
}
return catalog, nil
}
+8
View File
@@ -22,6 +22,14 @@ const (
CostumeAwakenEffectTypeItemAcquire CostumeAwakenEffectType = 3
)
type CostumeLotteryEffectType int32
const (
CostumeLotteryEffectTypeUnknown CostumeLotteryEffectType = 0
CostumeLotteryEffectTypeAbility CostumeLotteryEffectType = 1
CostumeLotteryEffectTypeStatusUp CostumeLotteryEffectType = 2
)
type WeaponAwakenEffectType int32
const (
+4 -2
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"log"
"github.com/google/uuid"
"lunar-tear/server/internal/gameutil"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/model"
@@ -276,7 +278,7 @@ func (h *QuestHandler) grantCompanion(user *store.UserState, companionId int32,
return
}
}
key := fmt.Sprintf("reward-companion-%d", companionId)
key := uuid.New().String()
user.Companions[key] = store.CompanionState{
UserCompanionUuid: key,
CompanionId: companionId,
@@ -306,7 +308,7 @@ func (h *QuestHandler) grantParts(user *store.UserState, partsId int32, nowMilli
}
}
key := fmt.Sprintf("reward-parts-%d", partsId)
key := uuid.New().String()
user.Parts[key] = store.PartsState{
UserPartsUuid: key,
PartsId: partsId,
+4 -4
View File
@@ -11,15 +11,15 @@ import (
type BannerServiceServer struct {
pb.UnimplementedBannerServiceServer
gacha store.GachaRepository
catalog []store.GachaCatalogEntry
}
func NewBannerServiceServer(gacha store.GachaRepository) *BannerServiceServer {
return &BannerServiceServer{gacha: gacha}
func NewBannerServiceServer(catalog []store.GachaCatalogEntry) *BannerServiceServer {
return &BannerServiceServer{catalog: catalog}
}
func (s *BannerServiceServer) GetMamaBanner(ctx context.Context, req *pb.GetMamaBannerRequest) (*pb.GetMamaBannerResponse, error) {
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
var termLimited []*pb.GachaBanner
var latestChapter *pb.GachaBanner
for _, entry := range catalog {
+2 -4
View File
@@ -43,8 +43,7 @@ func (s *CageOrnamentServiceServer) ReceiveReward(ctx context.Context, req *pb.R
s.granter.GrantFull(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{
"IUserMaterial", "IUserConsumableItem", "IUserGem",
"IUserCostume", "IUserCostumeActiveSkill", "IUserCharacter",
@@ -82,8 +81,7 @@ func (s *CageOrnamentServiceServer) RecordAccess(ctx context.Context, req *pb.Re
}
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{"IUserCageOrnamentReward"},
))
+2 -2
View File
@@ -35,7 +35,7 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
return &pb.RebirthResponse{}, nil
}
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
@@ -78,7 +78,7 @@ func (s *CharacterServiceServer) Rebirth(ctx context.Context, req *pb.RebirthReq
}
rebirthTables := []string{"IUserCharacterRebirth", "IUserMaterial", "IUserConsumableItem"}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), rebirthTables)
tables := userdata.ProjectTables(snapshot, rebirthTables)
diff := tracker.Apply(snapshot, tables)
return &pb.RebirthResponse{DiffUserData: diff}, nil
+2 -2
View File
@@ -27,7 +27,7 @@ func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}).
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
@@ -54,7 +54,7 @@ func (s *CharacterBoardServiceServer) ReleasePanel(ctx context.Context, req *pb.
"IUserConsumableItem",
"IUserGem",
}
tables := userdata.SelectTables(userdata.FullClientTableMap(user), boardTables)
tables := userdata.ProjectTables(user, boardTables)
diff := tracker.Apply(user, tables)
return &pb.ReleasePanelResponse{DiffUserData: diff}, nil
+1 -1
View File
@@ -29,7 +29,7 @@ func (s *CharacterViewerServiceServer) CharacterViewerTop(ctx context.Context, _
log.Printf("[CharacterViewerService] CharacterViewerTop")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
panic(fmt.Sprintf("CharacterViewerTop: no user for userId=%d: %v", userId, err))
}
+1 -2
View File
@@ -77,8 +77,7 @@ func (s *CompanionServiceServer) Enhance(ctx context.Context, req *pb.CompanionE
return nil, fmt.Errorf("companion enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, companionDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, companionDiffTables))
return &pb.CompanionEnhanceResponse{
DiffUserData: diff,
+2 -2
View File
@@ -28,7 +28,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"})
@@ -66,7 +66,7 @@ func (s *ConsumableItemServiceServer) Sell(ctx context.Context, req *pb.Consumab
return nil, fmt.Errorf("consumable item sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), []string{"IUserConsumableItem"})
tables := userdata.ProjectTables(snapshot, []string{"IUserConsumableItem"})
diff := tracker.Apply(snapshot, tables)
return &pb.ConsumableItemSellResponse{
+1 -2
View File
@@ -34,8 +34,7 @@ func (s *ContentsStoryServiceServer) RegisterPlayed(ctx context.Context, req *pb
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserContentsStory"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserContentsStory"}))
return &pb.ContentsStoryRegisterPlayedResponse{
DiffUserData: diff,
+219 -11
View File
@@ -4,6 +4,9 @@ import (
"context"
"fmt"
"log"
"math/rand"
"github.com/google/uuid"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
@@ -95,8 +98,7 @@ func (s *CostumeServiceServer) Enhance(ctx context.Context, req *pb.EnhanceReque
return nil, fmt.Errorf("costume enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables))
return &pb.EnhanceResponse{
IsGreatSuccess: false,
@@ -177,8 +179,7 @@ func (s *CostumeServiceServer) Awaken(ctx context.Context, req *pb.AwakenRequest
return nil, fmt.Errorf("costume awaken: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, awakenDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, awakenDiffTables))
return &pb.AwakenResponse{
DiffUserData: diff,
@@ -229,10 +230,12 @@ func (s *CostumeServiceServer) applyAwakenItemAcquire(user *store.UserState, ite
return
}
key := fmt.Sprintf("awaken-thought-%d", acq.PossessionId)
if _, exists := user.Thoughts[key]; exists {
return
for _, t := range user.Thoughts {
if t.ThoughtId == acq.PossessionId {
return
}
}
key := uuid.New().String()
user.Thoughts[key] = store.ThoughtState{
UserThoughtUuid: key,
ThoughtId: acq.PossessionId,
@@ -329,8 +332,7 @@ func (s *CostumeServiceServer) EnhanceActiveSkill(ctx context.Context, req *pb.E
return nil, fmt.Errorf("costume enhance active skill: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, activeSkillDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, activeSkillDiffTables))
return &pb.EnhanceActiveSkillResponse{
DiffUserData: diff,
@@ -387,10 +389,216 @@ func (s *CostumeServiceServer) LimitBreak(ctx context.Context, req *pb.LimitBrea
return nil, fmt.Errorf("costume limit break: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, costumeDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, costumeDiffTables))
return &pb.LimitBreakResponse{
DiffUserData: diff,
}, nil
}
var lotteryEffectDiffTables = []string{
"IUserCostume",
"IUserCostumeLotteryEffect",
"IUserCostumeLotteryEffectAbility",
"IUserCostumeLotteryEffectStatusUp",
"IUserCostumeLotteryEffectPending",
"IUserConsumableItem",
"IUserMaterial",
}
func (s *CostumeServiceServer) UnlockLotteryEffectSlot(ctx context.Context, req *pb.UnlockLotteryEffectSlotRequest) (*pb.UnlockLotteryEffectSlotResponse, error) {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
costume, ok := user.Costumes[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: costume uuid=%s not found", req.UserCostumeUuid)
return
}
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return
}
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectUnlockSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectUnlockMaterialGroupId]
for _, mat := range mats {
cur := user.Materials[mat.MaterialId]
cost := mat.Count
if cur < cost {
log.Printf("[CostumeService] UnlockLotteryEffectSlot: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
cost = cur
}
user.Materials[mat.MaterialId] = cur - cost
}
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
}
user.CostumeLotteryEffects[key] = store.CostumeLotteryEffectState{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
OddsNumber: 0,
LatestVersion: nowMillis,
}
costume.CostumeLotteryEffectUnlockedSlotCount++
costume.LatestVersion = nowMillis
user.Costumes[req.UserCostumeUuid] = costume
log.Printf("[CostumeService] UnlockLotteryEffectSlot: costumeId=%d slot=%d unlocked slotCount=%d", costume.CostumeId, req.SlotNumber, costume.CostumeLotteryEffectUnlockedSlotCount)
})
if err != nil {
return nil, fmt.Errorf("costume unlock lottery effect slot: %w", err)
}
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.UnlockLotteryEffectSlotResponse{
DiffUserData: diff,
}, nil
}
func (s *CostumeServiceServer) DrawLotteryEffect(ctx context.Context, req *pb.DrawLotteryEffectRequest) (*pb.DrawLotteryEffectResponse, error) {
log.Printf("[CostumeService] DrawLotteryEffect: uuid=%s slot=%d", req.UserCostumeUuid, req.SlotNumber)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"}).
Track("IUserConsumableItem", oldUser, userdata.SortedConsumableItemRecords, []string{"userId", "consumableItemId"}).
Track("IUserCostumeLotteryEffectPending", oldUser, userdata.SortedCostumeLotteryEffectPendingRecords, []string{"userId", "userCostumeUuid"})
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
costume, ok := user.Costumes[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] DrawLotteryEffect: costume uuid=%s not found", req.UserCostumeUuid)
return
}
effectRow, ok := s.catalog.LotteryEffects[[2]int32{costume.CostumeId, req.SlotNumber}]
if !ok {
log.Printf("[CostumeService] DrawLotteryEffect: no lottery effect for costumeId=%d slot=%d", costume.CostumeId, req.SlotNumber)
return
}
oddsPool := s.catalog.LotteryEffectOdds[effectRow.CostumeLotteryEffectOddsGroupId]
if len(oddsPool) == 0 {
log.Printf("[CostumeService] DrawLotteryEffect: empty odds pool for groupId=%d", effectRow.CostumeLotteryEffectOddsGroupId)
return
}
user.ConsumableItems[s.config.ConsumableItemIdForGold] -= s.config.CostumeLotteryEffectDrawSlotConsumeGold
mats := s.catalog.LotteryEffectMats[effectRow.CostumeLotteryEffectDrawMaterialGroupId]
for _, mat := range mats {
cur := user.Materials[mat.MaterialId]
cost := mat.Count
if cur < cost {
log.Printf("[CostumeService] DrawLotteryEffect: insufficient material id=%d have=%d need=%d", mat.MaterialId, cur, cost)
cost = cur
}
user.Materials[mat.MaterialId] = cur - cost
}
totalWeight := int32(0)
for _, row := range oddsPool {
totalWeight += row.Weight
}
roll := rand.Int31n(totalWeight)
var picked masterdata.CostumeLotteryEffectOddsRow
for _, row := range oddsPool {
roll -= row.Weight
if roll < 0 {
picked = row
break
}
}
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
}
existing := user.CostumeLotteryEffects[key]
if existing.OddsNumber == 0 {
existing.UserCostumeUuid = req.UserCostumeUuid
existing.SlotNumber = req.SlotNumber
existing.OddsNumber = picked.OddsNumber
existing.LatestVersion = nowMillis
user.CostumeLotteryEffects[key] = existing
} else {
user.CostumeLotteryEffectPending[req.UserCostumeUuid] = store.CostumeLotteryEffectPendingState{
UserCostumeUuid: req.UserCostumeUuid,
SlotNumber: req.SlotNumber,
OddsNumber: picked.OddsNumber,
LatestVersion: nowMillis,
}
}
log.Printf("[CostumeService] DrawLotteryEffect: costumeId=%d slot=%d drew oddsNumber=%d type=%d targetId=%d firstDraw=%v",
costume.CostumeId, req.SlotNumber, picked.OddsNumber, picked.CostumeLotteryEffectType, picked.CostumeLotteryEffectTargetId, existing.OddsNumber == 0)
})
if err != nil {
return nil, fmt.Errorf("costume draw lottery effect: %w", err)
}
diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.DrawLotteryEffectResponse{
DiffUserData: diff,
}, nil
}
func (s *CostumeServiceServer) ConfirmLotteryEffect(ctx context.Context, req *pb.ConfirmLotteryEffectRequest) (*pb.ConfirmLotteryEffectResponse, error) {
log.Printf("[CostumeService] ConfirmLotteryEffect: uuid=%s accept=%v", req.UserCostumeUuid, req.IsAccept)
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserCostumeLotteryEffectPending", oldUser, userdata.SortedCostumeLotteryEffectPendingRecords, []string{"userId", "userCostumeUuid"})
snapshot, err := s.users.UpdateUser(userId, func(user *store.UserState) {
pending, ok := user.CostumeLotteryEffectPending[req.UserCostumeUuid]
if !ok {
log.Printf("[CostumeService] ConfirmLotteryEffect: no pending for uuid=%s", req.UserCostumeUuid)
return
}
if req.IsAccept {
key := store.CostumeLotteryEffectKey{
UserCostumeUuid: pending.UserCostumeUuid,
SlotNumber: pending.SlotNumber,
}
effect := user.CostumeLotteryEffects[key]
effect.UserCostumeUuid = pending.UserCostumeUuid
effect.SlotNumber = pending.SlotNumber
effect.OddsNumber = pending.OddsNumber
effect.LatestVersion = nowMillis
user.CostumeLotteryEffects[key] = effect
log.Printf("[CostumeService] ConfirmLotteryEffect: accepted oddsNumber=%d for slot=%d", pending.OddsNumber, pending.SlotNumber)
} else {
log.Printf("[CostumeService] ConfirmLotteryEffect: rejected oddsNumber=%d for slot=%d", pending.OddsNumber, pending.SlotNumber)
}
delete(user.CostumeLotteryEffectPending, req.UserCostumeUuid)
})
if err != nil {
return nil, fmt.Errorf("costume confirm lottery effect: %w", err)
}
diff := tracker.Apply(snapshot, userdata.ProjectTables(snapshot, lotteryEffectDiffTables))
return &pb.ConfirmLotteryEffectResponse{
DiffUserData: diff,
}, nil
}
+2 -2
View File
@@ -42,12 +42,12 @@ func (s *DataServiceServer) GetUserData(ctx context.Context, req *pb.UserDataGet
log.Printf("[DataService] GetUserData: tables=%v", req.TableName)
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
defaults := userdata.FirstEntranceClientTableMap(user)
defaults := userdata.FullClientTableMap(user)
result := userdata.SelectTables(defaults, req.TableName)
return &pb.UserDataGetResponse{
UserDataJson: result,
+7 -7
View File
@@ -32,7 +32,7 @@ func (s *DeckServiceServer) UpdateName(ctx context.Context, req *pb.UpdateNameRe
user.Decks[deckKey] = deck
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserDeck"})
result := userdata.ProjectTables(user, []string{"IUserDeck"})
return &pb.UpdateNameResponse{
DiffUserData: userdata.BuildDiffFromTables(result),
}, nil
@@ -81,7 +81,7 @@ func (s *DeckServiceServer) RefreshDeckPower(ctx context.Context, req *pb.Refres
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
})
return &pb.RefreshDeckPowerResponse{
@@ -133,7 +133,7 @@ func (s *DeckServiceServer) RefreshMultiDeckPower(ctx context.Context, req *pb.R
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckTypeNote",
})
return &pb.RefreshMultiDeckPowerResponse{
@@ -173,7 +173,7 @@ func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeck
}
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
@@ -189,7 +189,7 @@ func (s *DeckServiceServer) ReplaceDeck(ctx context.Context, req *pb.ReplaceDeck
store.ApplyDeckReplacement(user, model.DeckType(req.DeckType), req.UserDeckNumber, deckSlotsFromProto(req.Deck), gametime.NowMillis())
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
"IUserDeckCharacterDressupCostume",
})
@@ -202,7 +202,7 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla
log.Printf("[DeckService] ReplaceTripleDeck: deckType=%d deckNumber=%d", req.DeckType, req.UserDeckNumber)
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserDeckSubWeaponGroup", oldUser, userdata.DeckSubWeaponRecords,
[]string{"userId", "userDeckCharacterUuid", "userWeaponUuid"}).
@@ -231,7 +231,7 @@ func (s *DeckServiceServer) ReplaceTripleDeck(ctx context.Context, req *pb.Repla
}
})
result := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
result := userdata.ProjectTables(user, []string{
"IUserDeck", "IUserDeckCharacter", "IUserDeckSubWeaponGroup", "IUserDeckPartsGroup",
"IUserDeckCharacterDressupCostume",
})
+1 -2
View File
@@ -33,8 +33,7 @@ func (s *DokanServiceServer) RegisterDokanConfirmed(ctx context.Context, req *pb
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserDokan"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserDokan"}))
return &pb.RegisterDokanConfirmedResponse{
DiffUserData: diff,
+3 -6
View File
@@ -71,8 +71,7 @@ func (s *ExploreServiceServer) StartExplore(ctx context.Context, req *pb.StartEx
return nil, fmt.Errorf("start explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreDiffTables))
return &pb.StartExploreResponse{
DiffUserData: diff,
@@ -124,8 +123,7 @@ func (s *ExploreServiceServer) FinishExplore(ctx context.Context, req *pb.Finish
return nil, fmt.Errorf("finish explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, exploreFinishDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, exploreFinishDiffTables))
rewards := []*pb.ExploreReward{
{
@@ -161,8 +159,7 @@ func (s *ExploreServiceServer) RetireExplore(ctx context.Context, req *pb.Retire
return nil, fmt.Errorf("retire explore: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserExplore"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserExplore"}))
return &pb.RetireExploreResponse{
DiffUserData: diff,
+10 -13
View File
@@ -34,20 +34,20 @@ type GachaServiceServer struct {
pb.UnimplementedGachaServiceServer
users store.UserRepository
sessions store.SessionRepository
gacha store.GachaRepository
catalog []store.GachaCatalogEntry
handler *gacha.GachaHandler
}
func NewGachaServiceServer(
users store.UserRepository,
sessions store.SessionRepository,
gachaRepo store.GachaRepository,
catalog []store.GachaCatalogEntry,
handler *gacha.GachaHandler,
) *GachaServiceServer {
return &GachaServiceServer{
users: users,
sessions: sessions,
gacha: gachaRepo,
catalog: catalog,
handler: handler,
}
}
@@ -55,7 +55,7 @@ func NewGachaServiceServer(
func (s *GachaServiceServer) GetGachaList(ctx context.Context, req *pb.GetGachaListRequest) (*pb.GetGachaListResponse, error) {
log.Printf("[GachaService] GetGachaList: labels=%v", req.GachaLabelType)
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
@@ -132,10 +132,10 @@ func (s *GachaServiceServer) autoConvertExpiredMedals(user *store.UserState, cat
func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaRequest) (*pb.GetGachaResponse, error) {
log.Printf("[GachaService] GetGacha: ids=%v", req.GachaId)
catalog, _ := s.gacha.SnapshotCatalog()
catalog := s.catalog
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
@@ -160,8 +160,7 @@ func (s *GachaServiceServer) GetGacha(ctx context.Context, req *pb.GetGachaReque
func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb.DrawResponse, error) {
log.Printf("[GachaService] Draw: gachaId=%d phaseId=%d execCount=%d", req.GachaId, req.GachaPricePhaseId, req.ExecCount)
catalog, _ := s.gacha.SnapshotCatalog()
entry := findCatalogEntry(catalog, req.GachaId)
entry := findCatalogEntry(s.catalog, req.GachaId)
if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
}
@@ -293,8 +292,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
changedStoryIds := s.handler.Granter.DrainChangedStoryWeaponIds()
diffOrder := append(gachaDiffTables, "IUserWeaponStory")
allTables := userdata.FullClientTableMap(updatedUser)
diff := userdata.BuildDiffFromTablesOrdered(userdata.SelectTables(allTables, diffOrder), diffOrder)
diff := userdata.BuildDiffFromTablesOrdered(userdata.ProjectTables(updatedUser, diffOrder), diffOrder)
userdata.AddWeaponStoryDiff(diff, updatedUser, changedStoryIds)
return &pb.DrawResponse{
@@ -309,8 +307,7 @@ func (s *GachaServiceServer) Draw(ctx context.Context, req *pb.DrawRequest) (*pb
func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBoxGachaRequest) (*pb.ResetBoxGachaResponse, error) {
log.Printf("[GachaService] ResetBoxGacha: gachaId=%d", req.GachaId)
catalog, _ := s.gacha.SnapshotCatalog()
entry := findCatalogEntry(catalog, req.GachaId)
entry := findCatalogEntry(s.catalog, req.GachaId)
if entry == nil {
return nil, fmt.Errorf("gacha %d not found", req.GachaId)
}
@@ -336,7 +333,7 @@ func (s *GachaServiceServer) ResetBoxGacha(ctx context.Context, req *pb.ResetBox
func (s *GachaServiceServer) GetRewardGacha(ctx context.Context, req *emptypb.Empty) (*pb.GetRewardGachaResponse, error) {
log.Printf("[GachaService] GetRewardGacha")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
+2 -2
View File
@@ -71,7 +71,7 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList
req.RewardKindType, req.ExpirationType, req.IsAscendingSort, req.NextCursor, req.PreviousCursor, req.GetCount)
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
@@ -108,7 +108,7 @@ func (s *GiftServiceServer) GetGiftList(ctx context.Context, req *pb.GetGiftList
func (s *GiftServiceServer) GetGiftReceiveHistoryList(ctx context.Context, req *emptypb.Empty) (*pb.GetGiftReceiveHistoryListResponse, error) {
log.Printf("[GiftService] GetGiftReceiveHistoryList")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
+4 -4
View File
@@ -38,7 +38,7 @@ func (s *GimmickServiceServer) UpdateSequence(ctx context.Context, req *pb.Updat
user.Gimmick.Sequences[key] = sequence
})
return &pb.UpdateSequenceResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickSequence"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickSequence"})),
}, nil
}
@@ -74,7 +74,7 @@ func (s *GimmickServiceServer) UpdateGimmickProgress(ctx context.Context, req *p
GimmickOrnamentReward: []*pb.GimmickReward{},
IsSequenceCleared: false,
GimmickSequenceClearReward: []*pb.GimmickReward{},
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{
"IUserGimmick",
"IUserGimmickOrnamentProgress",
})),
@@ -98,7 +98,7 @@ func (s *GimmickServiceServer) InitSequenceSchedule(ctx context.Context, _ *empt
}
})
return &pb.InitSequenceScheduleResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), gimmickDiffTables)),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, gimmickDiffTables)),
}, nil
}
@@ -119,6 +119,6 @@ func (s *GimmickServiceServer) Unlock(ctx context.Context, req *pb.UnlockRequest
}
})
return &pb.UnlockResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserGimmickUnlock"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserGimmickUnlock"})),
}, nil
}
+1 -1
View File
@@ -495,4 +495,4 @@ func objectIdToFilePathCandidates(revision, assetType, objectId string) (candida
}
}
return candidates, entry.Size, true
}
}
+4 -4
View File
@@ -2,10 +2,11 @@ package service
import (
"context"
"fmt"
"log"
"time"
"github.com/google/uuid"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
@@ -55,7 +56,7 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb
GrantDatetime: now,
},
ExpirationDatetime: now + int64(30*24*time.Hour/time.Millisecond),
UserGiftUuid: fmt.Sprintf("login-bonus-%d-%d", userId, nextStamp),
UserGiftUuid: uuid.New().String(),
})
user.Notifications.GiftNotReceiveCount = int32(len(user.Gifts.NotReceived))
user.LoginBonus.CurrentStampNumber = nextStamp
@@ -63,8 +64,7 @@ func (s *LoginBonusServiceServer) ReceiveStamp(ctx context.Context, req *emptypb
user.LoginBonus.LatestVersion = now
})
diff := userdata.BuildDiffFromTables(userdata.SelectTables(
userdata.FullClientTableMap(user),
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user,
[]string{"IUserLoginBonus"},
))
setCommonResponseTrailers(ctx, diff, false)
+2 -2
View File
@@ -33,7 +33,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserMaterial", oldUser, userdata.SortedMaterialRecords, []string{"userId", "materialId"})
@@ -71,7 +71,7 @@ func (s *MaterialServiceServer) Sell(ctx context.Context, req *pb.MaterialSellRe
return nil, fmt.Errorf("material sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), materialDiffTables)
tables := userdata.ProjectTables(snapshot, materialDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.MaterialSellResponse{
+2 -3
View File
@@ -24,13 +24,12 @@ func (s *MissionServiceServer) UpdateMissionProgress(ctx context.Context, req *p
log.Printf("[MissionService] UpdateMissionProgress: cage=%v pictureBook=%v", req.CageMeasurableValues, req.PictureBookMeasurableValues)
userId := currentUserId(ctx, s.users, s.sessions)
snapshot, err := s.users.SnapshotUser(userId)
snapshot, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("snapshot user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMission"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMission"}))
return &pb.UpdateMissionProgressResponse{
DiffUserData: diff,
+1 -2
View File
@@ -36,8 +36,7 @@ func (s *MovieServiceServer) SaveViewedMovie(ctx context.Context, req *pb.SaveVi
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserMovie"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserMovie"}))
return &pb.SaveViewedMovieResponse{
DiffUserData: diff,
+1 -2
View File
@@ -31,8 +31,7 @@ func (s *NaviCutInServiceServer) RegisterPlayed(ctx context.Context, req *pb.Reg
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserNaviCutIn"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserNaviCutIn"}))
return &pb.RegisterPlayedResponse{
DiffUserData: diff,
+1 -1
View File
@@ -24,7 +24,7 @@ func NewNotificationServiceServer(users store.UserRepository, sessions store.Ses
func (s *NotificationServiceServer) GetHeaderNotification(ctx context.Context, req *emptypb.Empty) (*pb.GetHeaderNotificationResponse, error) {
log.Printf("[NotificationService] GetHeaderNotification")
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetHeaderNotificationResponse{
GiftNotReceiveCount: 0,
+1 -2
View File
@@ -36,8 +36,7 @@ func (s *OmikujiServiceServer) OmikujiDraw(ctx context.Context, req *pb.OmikujiD
return nil, fmt.Errorf("update user: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserOmikuji"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserOmikuji"}))
return &pb.OmikujiDrawResponse{
OmikujiResultAssetId: s.catalog.LookupAssetId(req.OmikujiId),
+4 -6
View File
@@ -37,7 +37,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserParts", oldUser, userdata.SortedPartsRecords, []string{"userId", "userPartsUuid"})
@@ -81,7 +81,7 @@ func (s *PartsServiceServer) Sell(ctx context.Context, req *pb.PartsSellRequest)
return nil, fmt.Errorf("parts sell: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), partsDiffTables)
tables := userdata.ProjectTables(snapshot, partsDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.PartsSellResponse{
@@ -158,8 +158,7 @@ func (s *PartsServiceServer) Enhance(ctx context.Context, req *pb.PartsEnhanceRe
return nil, fmt.Errorf("parts enhance: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, partsDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, partsDiffTables))
return &pb.PartsEnhanceResponse{
IsSuccess: isSuccess,
@@ -187,8 +186,7 @@ func (s *PartsServiceServer) ReplacePreset(ctx context.Context, req *pb.PartsRep
return nil, fmt.Errorf("parts replace preset: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserPartsPreset"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserPartsPreset"}))
return &pb.PartsReplacePresetResponse{
DiffUserData: diff,
+1 -2
View File
@@ -30,8 +30,7 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
user.PortalCageStatus.LatestVersion = now
})
tables := userdata.SelectTables(
userdata.FullClientTableMap(user),
tables := userdata.ProjectTables(user,
[]string{"IUserPortalCageStatus"},
)
return &pb.UpdatePortalCageSceneProgressResponse{
+2 -2
View File
@@ -42,7 +42,7 @@ var bigHuntDiffTables = []string{
}
func buildBigHuntDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
@@ -331,7 +331,7 @@ func (s *BigHuntServiceServer) GetBigHuntTopData(ctx context.Context, _ *emptypb
log.Printf("[BigHuntService] GetBigHuntTopData")
userId := currentUserId(ctx, s.users, s.sessions)
user, _ := s.users.SnapshotUser(userId)
user, _ := s.users.LoadUser(userId)
nowMillis := gametime.NowMillis()
weeklyVersion := gametime.WeeklyVersion(nowMillis)
+1 -1
View File
@@ -29,7 +29,7 @@ func NewQuestServiceServer(users store.UserRepository, sessions store.SessionRep
}
func buildSelectedQuestDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
+1 -1
View File
@@ -24,7 +24,7 @@ func NewSideStoryQuestServiceServer(users store.UserRepository, sessions store.S
}
func buildSideStoryDiff(user store.UserState, tableNames []string) map[string]*pb.DiffData {
tables := userdata.SelectTables(userdata.FullClientTableMap(user), tableNames)
tables := userdata.ProjectTables(user, tableNames)
return userdata.BuildDiffFromTablesOrdered(tables, tableNames)
}
+1 -1
View File
@@ -106,7 +106,7 @@ func (s *RewardServiceServer) ReceiveBigHuntReward(ctx context.Context, _ *empty
weeklyScoreResults = []*pb.WeeklyScoreResult{}
}
tables := userdata.SelectTables(userdata.FullClientTableMap(user), []string{
tables := userdata.ProjectTables(user, []string{
"IUserBigHuntWeeklyStatus",
"IUserBigHuntWeeklyMaxScore",
"IUserConsumableItem",
+5 -9
View File
@@ -89,8 +89,7 @@ func (s *ShopServiceServer) Buy(ctx context.Context, req *pb.BuyRequest) (*pb.Bu
return nil, fmt.Errorf("shop buy: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, s.granter.DrainChangedStoryWeaponIds())
return &pb.BuyResponse{
@@ -132,8 +131,7 @@ func (s *ShopServiceServer) RefreshUserData(ctx context.Context, req *pb.Refresh
return nil, fmt.Errorf("shop refresh: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.RefreshResponse{
DiffUserData: diff,
@@ -195,8 +193,7 @@ func (s *ShopServiceServer) CreatePurchaseTransaction(ctx context.Context, req *
txId := fmt.Sprintf("tx_%d_%d_%d", userId, req.ShopItemId, nowMillis)
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.CreatePurchaseTransactionResponse{
PurchaseTransactionId: txId,
@@ -208,13 +205,12 @@ func (s *ShopServiceServer) PurchaseGooglePlayStoreProduct(ctx context.Context,
log.Printf("[ShopService] PurchaseGooglePlayStoreProduct: txId=%s", req.PurchaseTransactionId)
userId := currentUserId(ctx, s.users, s.sessions)
snapshot, err := s.users.SnapshotUser(userId)
snapshot, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("purchase google play: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, shopDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, shopDiffTables))
return &pb.PurchaseGooglePlayStoreProductResponse{
OverflowPossession: []*pb.Possession{},
+1 -2
View File
@@ -15,7 +15,6 @@ var startedGameStartTables = []string{
"IUserWeapon",
"IUserWeaponSkill",
"IUserWeaponAbility",
"IUserWeaponStory",
"IUserCompanion",
"IUserDeckCharacter",
"IUserDeck",
@@ -47,7 +46,7 @@ var gimmickDiffTables = []string{
func currentUserId(ctx context.Context, users store.UserRepository, sessions store.SessionRepository) int64 {
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-session-key"); len(vals) > 0 {
if vals := md.Get("x-apb-session-key"); len(vals) > 0 {
if userId, err := sessions.ResolveUserId(vals[0]); err == nil {
return userId
}
+4 -5
View File
@@ -37,11 +37,10 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
ChoiceId: req.ChoiceId,
}
}
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
req.TutorialType == int32(model.TutorialTypeMenuSecond) {
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
if req.TutorialType == int32(model.TutorialTypeMenuFirst) && req.ProgressPhase == 20 {
store.EnsureDefaultDeck(user, nowMillis)
}
grants = s.engine.ApplyTutorialReward(user, model.TutorialType(req.TutorialType), req.ChoiceId, nowMillis)
})
tables := []string{"IUserTutorialProgress"}
if req.TutorialType == int32(model.TutorialTypeMenuFirst) ||
@@ -55,7 +54,7 @@ func (s *TutorialServiceServer) SetTutorialProgress(ctx context.Context, req *pb
if len(grants) > 0 {
tables = append(tables, "IUserCompanion")
}
result := userdata.SelectTables(userdata.FullClientTableMap(user), tables)
result := userdata.ProjectTables(user, tables)
for _, t := range tables {
log.Printf("[TutorialService] DiffTable %s -> %s", t, result[t])
}
@@ -89,7 +88,7 @@ func (s *TutorialServiceServer) SetTutorialProgressAndReplaceDeck(ctx context.Co
}
})
return &pb.SetTutorialProgressAndReplaceDeckResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{
"IUserTutorialProgress",
"IUserDeck",
"IUserDeckCharacter",
+24 -17
View File
@@ -50,9 +50,13 @@ func setCommonResponseTrailers(ctx context.Context, diff map[string]*pb.DiffData
}
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
user, err := s.users.EnsureUser(req.Uuid)
userId, err := s.users.CreateUser(req.Uuid)
if err != nil {
return nil, fmt.Errorf("ensure user: %w", err)
return nil, fmt.Errorf("create user: %w", err)
}
user, err := s.users.LoadUser(userId)
if err != nil {
return nil, fmt.Errorf("load user: %w", err)
}
log.Printf("[UserService] RegisterUser: uuid=%s terminalId=%s -> userId=%d", req.Uuid, req.TerminalId, user.UserId)
@@ -66,10 +70,14 @@ func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUs
func (s *UserServiceServer) Auth(ctx context.Context, req *pb.AuthUserRequest) (*pb.AuthUserResponse, error) {
log.Printf("[UserService] Auth: uuid=%s", req.Uuid)
user, session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour)
session, err := s.sessions.CreateSession(req.Uuid, 24*time.Hour)
if err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
user, err := s.users.LoadUser(session.UserId)
if err != nil {
return nil, fmt.Errorf("load user: %w", err)
}
return &pb.AuthUserResponse{
SessionKey: session.SessionKey,
@@ -84,7 +92,7 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
log.Printf("[UserService] GameStart")
if md, ok := metadata.FromIncomingContext(ctx); ok {
if vals := md.Get("x-session-key"); len(vals) > 0 {
if vals := md.Get("x-apb-session-key"); len(vals) > 0 {
log.Printf("[UserService] GameStart session: %s", vals[0])
}
}
@@ -93,8 +101,7 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
user, _ := s.users.UpdateUser(userId, func(user *store.UserState) {
user.GameStartDatetime = gametime.NowMillis()
})
fullTables := userdata.FullClientTableMap(user)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(fullTables, startedGameStartTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(user, startedGameStartTables))
setCommonResponseTrailers(ctx, diff, true)
return &pb.GameStartResponse{
@@ -106,12 +113,12 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
func (s *UserServiceServer) TransferUser(ctx context.Context, req *pb.TransferUserRequest) (*pb.TransferUserResponse, error) {
log.Printf("[UserService] TransferUser")
user, err := s.users.EnsureUser(req.Uuid)
userId, err := s.users.CreateUser(req.Uuid)
if err != nil {
return nil, fmt.Errorf("ensure user: %w", err)
return nil, fmt.Errorf("create user: %w", err)
}
return &pb.TransferUserResponse{
UserId: user.UserId,
UserId: userId,
Signature: "transferred-sig",
DiffUserData: userdata.EmptyDiff(),
}, nil
@@ -126,7 +133,7 @@ func (s *UserServiceServer) SetUserName(ctx context.Context, req *pb.SetUserName
user.Profile.NameUpdateDatetime = nowMillis
})
return &pb.SetUserNameResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -139,7 +146,7 @@ func (s *UserServiceServer) SetUserMessage(ctx context.Context, req *pb.SetUserM
user.Profile.MessageUpdateDatetime = nowMillis
})
return &pb.SetUserMessageResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -152,7 +159,7 @@ func (s *UserServiceServer) SetUserFavoriteCostumeId(ctx context.Context, req *p
user.Profile.FavoriteCostumeIdUpdateDatetime = nowMillis
})
return &pb.SetUserFavoriteCostumeIdResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserProfile"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserProfile"})),
}, nil
}
@@ -162,7 +169,7 @@ func (s *UserServiceServer) GetUserProfile(ctx context.Context, req *pb.GetUserP
if userId == 0 {
userId = currentUserId(ctx, s.users, s.sessions)
}
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetUserProfileResponse{DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -219,7 +226,7 @@ func (s *UserServiceServer) SetBirthYearMonth(ctx context.Context, req *pb.SetBi
func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Empty) (*pb.GetBirthYearMonthResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetBirthYearMonthResponse{BirthYear: 2000, BirthMonth: 1, DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -228,7 +235,7 @@ func (s *UserServiceServer) GetBirthYearMonth(ctx context.Context, _ *emptypb.Em
func (s *UserServiceServer) GetChargeMoney(ctx context.Context, _ *emptypb.Empty) (*pb.GetChargeMoneyResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetChargeMoneyResponse{ChargeMoneyThisMonth: 0, DiffUserData: userdata.EmptyDiff()}, nil
}
@@ -242,7 +249,7 @@ func (s *UserServiceServer) SetUserSetting(ctx context.Context, req *pb.SetUserS
user.Setting.IsNotifyPurchaseAlert = req.IsNotifyPurchaseAlert
})
return &pb.SetUserSettingResponse{
DiffUserData: userdata.BuildDiffFromTables(userdata.SelectTables(userdata.FullClientTableMap(user), []string{"IUserSetting"})),
DiffUserData: userdata.BuildDiffFromTables(userdata.ProjectTables(user, []string{"IUserSetting"})),
}, nil
}
@@ -252,7 +259,7 @@ func (s *UserServiceServer) GetAndroidArgs(ctx context.Context, req *pb.GetAndro
func (s *UserServiceServer) GetBackupToken(ctx context.Context, req *pb.GetBackupTokenRequest) (*pb.GetBackupTokenResponse, error) {
userId := currentUserId(ctx, s.users, s.sessions)
user, err := s.users.SnapshotUser(userId)
user, err := s.users.LoadUser(userId)
if err != nil {
return &pb.GetBackupTokenResponse{BackupToken: "mock-backup-token", DiffUserData: userdata.EmptyDiff()}, nil
}
+14 -22
View File
@@ -71,8 +71,7 @@ func (s *WeaponServiceServer) Protect(ctx context.Context, req *pb.ProtectReques
}
})
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"}))
return &pb.ProtectResponse{DiffUserData: diff}, nil
}
@@ -95,8 +94,7 @@ func (s *WeaponServiceServer) Unprotect(ctx context.Context, req *pb.UnprotectRe
}
})
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, []string{"IUserWeapon"}))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, []string{"IUserWeapon"}))
return &pb.UnprotectResponse{DiffUserData: diff}, nil
}
@@ -165,8 +163,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
return nil, fmt.Errorf("weapon enhance by material: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
return &pb.EnhanceByMaterialResponse{
@@ -181,7 +178,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
userId := currentUserId(ctx, s.users, s.sessions)
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -229,7 +226,7 @@ func (s *WeaponServiceServer) Sell(ctx context.Context, req *pb.SellRequest) (*p
}
sellDiffTables := []string{"IUserWeapon", "IUserWeaponSkill", "IUserWeaponAbility", "IUserWeaponAwaken", "IUserConsumableItem"}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), sellDiffTables)
tables := userdata.ProjectTables(snapshot, sellDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.SellResponse{DiffUserData: diff}, nil
@@ -307,8 +304,7 @@ func (s *WeaponServiceServer) Evolve(ctx context.Context, req *pb.EvolveRequest)
return nil, fmt.Errorf("weapon evolve: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
return &pb.EvolveResponse{DiffUserData: diff}, nil
@@ -407,8 +403,7 @@ func (s *WeaponServiceServer) EnhanceSkill(ctx context.Context, req *pb.EnhanceS
return nil, fmt.Errorf("weapon enhance skill: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
return &pb.EnhanceSkillResponse{DiffUserData: diff}, nil
}
@@ -506,8 +501,7 @@ func (s *WeaponServiceServer) EnhanceAbility(ctx context.Context, req *pb.Enhanc
return nil, fmt.Errorf("weapon enhance ability: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponDiffTables))
return &pb.EnhanceAbilityResponse{DiffUserData: diff}, nil
}
@@ -578,8 +572,7 @@ func (s *WeaponServiceServer) LimitBreakByMaterial(ctx context.Context, req *pb.
return nil, fmt.Errorf("weapon limit break by material: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, limitBreakDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, limitBreakDiffTables))
return &pb.LimitBreakByMaterialResponse{DiffUserData: diff}, nil
}
@@ -590,7 +583,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -665,7 +658,7 @@ func (s *WeaponServiceServer) LimitBreakByWeapon(ctx context.Context, req *pb.Li
return nil, fmt.Errorf("weapon limit break by weapon: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), limitBreakDiffTables)
tables := userdata.ProjectTables(snapshot, limitBreakDiffTables)
diff := tracker.Apply(snapshot, tables)
return &pb.LimitBreakByWeaponResponse{DiffUserData: diff}, nil
@@ -677,7 +670,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
userId := currentUserId(ctx, s.users, s.sessions)
nowMillis := gametime.NowMillis()
oldUser, _ := s.users.SnapshotUser(userId)
oldUser, _ := s.users.LoadUser(userId)
tracker := userdata.NewDeleteTracker().
Track("IUserWeapon", oldUser, userdata.SortedWeaponRecords, []string{"userId", "userWeaponUuid"}).
Track("IUserWeaponSkill", oldUser, userdata.SortedWeaponSkillRecords, []string{"userId", "userWeaponUuid", "slotNumber"}).
@@ -753,7 +746,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
return nil, fmt.Errorf("weapon enhance by weapon: %w", err)
}
tables := userdata.SelectTables(userdata.FullClientTableMap(snapshot), weaponDiffTables)
tables := userdata.ProjectTables(snapshot, weaponDiffTables)
diff := tracker.Apply(snapshot, tables)
userdata.AddWeaponStoryDiff(diff, snapshot, changedStoryIds)
@@ -864,8 +857,7 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
return nil, fmt.Errorf("weapon awaken: %w", err)
}
tables := userdata.FullClientTableMap(snapshot)
diff := userdata.BuildDiffFromTables(userdata.SelectTables(tables, weaponAwakenDiffTables))
diff := userdata.BuildDiffFromTables(userdata.ProjectTables(snapshot, weaponAwakenDiffTables))
return &pb.WeaponAwakenResponse{DiffUserData: diff}, nil
}
@@ -1,12 +1,8 @@
package memory
package store
import (
"maps"
import "maps"
"lunar-tear/server/internal/store"
)
func cloneUserState(u store.UserState) store.UserState {
func CloneUserState(u UserState) UserState {
out := u
out.Tutorials = maps.Clone(u.Tutorials)
out.Characters = maps.Clone(u.Characters)
@@ -15,14 +11,14 @@ func cloneUserState(u store.UserState) store.UserState {
out.Companions = maps.Clone(u.Companions)
out.Thoughts = maps.Clone(u.Thoughts)
out.DeckCharacters = maps.Clone(u.DeckCharacters)
out.DeckSubWeapons = maps.Clone(u.DeckSubWeapons)
out.DeckSubWeapons = cloneSliceMap(u.DeckSubWeapons)
out.DeckParts = cloneSliceMap(u.DeckParts)
out.Decks = maps.Clone(u.Decks)
out.Quests = maps.Clone(u.Quests)
out.QuestMissions = maps.Clone(u.QuestMissions)
out.WeaponStories = maps.Clone(u.WeaponStories)
out.Missions = maps.Clone(u.Missions)
out.Gimmick = store.GimmickState{
out.Gimmick = GimmickState{
Progress: maps.Clone(u.Gimmick.Progress),
OrnamentProgress: maps.Clone(u.Gimmick.OrnamentProgress),
Sequences: maps.Clone(u.Gimmick.Sequences),
@@ -38,6 +34,7 @@ func cloneUserState(u store.UserState) store.UserState {
out.CostumeActiveSkills = maps.Clone(u.CostumeActiveSkills)
out.WeaponSkills = cloneSliceMap(u.WeaponSkills)
out.WeaponAbilities = cloneSliceMap(u.WeaponAbilities)
out.WeaponAwakens = maps.Clone(u.WeaponAwakens)
out.DeckTypeNotes = maps.Clone(u.DeckTypeNotes)
out.WeaponNotes = maps.Clone(u.WeaponNotes)
out.NaviCutInPlayed = maps.Clone(u.NaviCutInPlayed)
@@ -50,40 +47,46 @@ func cloneUserState(u store.UserState) store.UserState {
out.ShopReplaceableLineup = maps.Clone(u.ShopReplaceableLineup)
out.Explore = u.Explore
out.ExploreScores = maps.Clone(u.ExploreScores)
out.Gacha = store.GachaState{
out.Gacha = GachaState{
RewardAvailable: u.Gacha.RewardAvailable,
TodaysCurrentDrawCount: u.Gacha.TodaysCurrentDrawCount,
DailyMaxCount: u.Gacha.DailyMaxCount,
LastRewardDrawDate: u.Gacha.LastRewardDrawDate,
ConvertedGachaMedal: store.ConvertedGachaMedalState{
ConvertedMedalPossession: append([]store.ConsumableItemState(nil), u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession...),
ConvertedGachaMedal: ConvertedGachaMedalState{
ConvertedMedalPossession: append([]ConsumableItemState(nil), u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession...),
ObtainPossession: cloneConsumableItemPtr(u.Gacha.ConvertedGachaMedal.ObtainPossession),
},
BannerStates: cloneBannerStates(u.Gacha.BannerStates),
}
out.Gifts = store.GiftState{
out.Gifts = GiftState{
NotReceived: cloneNotReceivedGifts(u.Gifts.NotReceived),
Received: cloneReceivedGifts(u.Gifts.Received),
}
out.Battle = u.Battle
out.SideStoryQuests = maps.Clone(u.SideStoryQuests)
out.QuestLimitContentStatus = maps.Clone(u.QuestLimitContentStatus)
out.BigHuntMaxScores = maps.Clone(u.BigHuntMaxScores)
out.BigHuntStatuses = maps.Clone(u.BigHuntStatuses)
out.BigHuntScheduleMaxScores = maps.Clone(u.BigHuntScheduleMaxScores)
out.BigHuntWeeklyMaxScores = maps.Clone(u.BigHuntWeeklyMaxScores)
out.BigHuntWeeklyStatuses = maps.Clone(u.BigHuntWeeklyStatuses)
out.BigHuntBattleBinary = append([]byte(nil), u.BigHuntBattleBinary...)
out.CharacterBoards = maps.Clone(u.CharacterBoards)
out.CharacterBoardAbilities = maps.Clone(u.CharacterBoardAbilities)
out.CharacterBoardStatusUps = maps.Clone(u.CharacterBoardStatusUps)
out.CostumeAwakenStatusUps = maps.Clone(u.CostumeAwakenStatusUps)
out.CostumeLotteryEffects = maps.Clone(u.CostumeLotteryEffects)
out.CostumeLotteryEffectPending = maps.Clone(u.CostumeLotteryEffectPending)
out.AutoSaleSettings = maps.Clone(u.AutoSaleSettings)
out.CharacterRebirths = maps.Clone(u.CharacterRebirths)
return out
}
func cloneGachaCatalogEntry(entry store.GachaCatalogEntry) store.GachaCatalogEntry {
out := entry
out.PricePhases = append([]store.GachaPricePhaseEntry(nil), entry.PricePhases...)
out.PromotionItems = append([]store.GachaPromotionItem(nil), entry.PromotionItems...)
return out
}
func cloneBannerStates(m map[int32]store.GachaBannerState) map[int32]store.GachaBannerState {
func cloneBannerStates(m map[int32]GachaBannerState) map[int32]GachaBannerState {
if m == nil {
return nil
}
out := make(map[int32]store.GachaBannerState, len(m))
out := make(map[int32]GachaBannerState, len(m))
for k, v := range m {
bs := v
bs.BoxDrewCounts = maps.Clone(v.BoxDrewCounts)
@@ -92,7 +95,7 @@ func cloneBannerStates(m map[int32]store.GachaBannerState) map[int32]store.Gacha
return out
}
func cloneConsumableItemPtr(item *store.ConsumableItemState) *store.ConsumableItemState {
func cloneConsumableItemPtr(item *ConsumableItemState) *ConsumableItemState {
if item == nil {
return nil
}
@@ -100,11 +103,11 @@ func cloneConsumableItemPtr(item *store.ConsumableItemState) *store.ConsumableIt
return &out
}
func cloneNotReceivedGifts(gifts []store.NotReceivedGiftState) []store.NotReceivedGiftState {
out := make([]store.NotReceivedGiftState, len(gifts))
func cloneNotReceivedGifts(gifts []NotReceivedGiftState) []NotReceivedGiftState {
out := make([]NotReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = store.NotReceivedGiftState{
GiftCommon: store.GiftCommonState{
out[i] = NotReceivedGiftState{
GiftCommon: GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
@@ -119,6 +122,24 @@ func cloneNotReceivedGifts(gifts []store.NotReceivedGiftState) []store.NotReceiv
return out
}
func cloneReceivedGifts(gifts []ReceivedGiftState) []ReceivedGiftState {
out := make([]ReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = ReceivedGiftState{
GiftCommon: GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
GrantDatetime: gift.GiftCommon.GrantDatetime,
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
},
ReceivedDatetime: gift.ReceivedDatetime,
}
}
return out
}
func cloneSliceMap[T any](m map[string][]T) map[string][]T {
if m == nil {
return nil
@@ -129,21 +150,3 @@ func cloneSliceMap[T any](m map[string][]T) map[string][]T {
}
return out
}
func cloneReceivedGifts(gifts []store.ReceivedGiftState) []store.ReceivedGiftState {
out := make([]store.ReceivedGiftState, len(gifts))
for i, gift := range gifts {
out[i] = store.ReceivedGiftState{
GiftCommon: store.GiftCommonState{
PossessionType: gift.GiftCommon.PossessionType,
PossessionId: gift.GiftCommon.PossessionId,
Count: gift.GiftCommon.Count,
GrantDatetime: gift.GiftCommon.GrantDatetime,
DescriptionGiftTextId: gift.GiftCommon.DescriptionGiftTextId,
EquipmentData: append([]byte(nil), gift.GiftCommon.EquipmentData...),
},
ReceivedDatetime: gift.ReceivedDatetime,
}
}
return out
}
+30 -21
View File
@@ -5,6 +5,8 @@ import (
"log"
"sort"
"github.com/google/uuid"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/model"
)
@@ -139,7 +141,7 @@ func (g *PossessionGranter) GrantCostume(user *UserState, costumeId int32, nowMi
}
}
}
key := fmt.Sprintf("reward-costume-%d", costumeId)
key := uuid.New().String()
user.Costumes[key] = CostumeState{
UserCostumeUuid: key,
CostumeId: costumeId,
@@ -155,16 +157,7 @@ func (g *PossessionGranter) GrantCostume(user *UserState, costumeId int32, nowMi
}
func (g *PossessionGranter) GrantWeapon(user *UserState, weaponId int32, nowMillis int64) {
key := fmt.Sprintf("reward-weapon-%d-%d", weaponId, nowMillis)
if _, exists := user.Weapons[key]; exists {
for i := 2; ; i++ {
candidate := fmt.Sprintf("%s-%d", key, i)
if _, exists := user.Weapons[candidate]; !exists {
key = candidate
break
}
}
}
key := uuid.New().String()
user.Weapons[key] = WeaponState{
UserWeaponUuid: key,
WeaponId: weaponId,
@@ -269,16 +262,29 @@ func EnsureDefaultDeck(user *UserState, nowMillis int64) {
return
}
costumeUuid := FirstSortedKey(user.Costumes)
weaponUuid := FirstSortedKey(user.Weapons)
companionUuid := FirstSortedKey(user.Companions)
const rionCostumeId = int32(10100)
const rionWeaponId = int32(101001)
dcUuid := "default-deck-character-0001"
var costumeUuid, weaponUuid string
for k, v := range user.Costumes {
if v.CostumeId == rionCostumeId {
costumeUuid = k
break
}
}
for k, v := range user.Weapons {
if v.WeaponId == rionWeaponId {
weaponUuid = k
break
}
}
dcUuid := uuid.New().String()
user.DeckCharacters[dcUuid] = DeckCharacterState{
UserDeckCharacterUuid: dcUuid,
UserCompanionUuid: "",
UserCostumeUuid: costumeUuid,
MainUserWeaponUuid: weaponUuid,
UserCompanionUuid: companionUuid,
Power: 100,
LatestVersion: nowMillis,
}
@@ -324,14 +330,17 @@ func ApplyDeckReplacement(user *UserState, deckType model.DeckType, userDeckNumb
deck.Power = 100
}
uuids := []*string{&deck.UserDeckCharacterUuid01, &deck.UserDeckCharacterUuid02, &deck.UserDeckCharacterUuid03}
for i, uuid := range uuids {
uuidPtrs := []*string{&deck.UserDeckCharacterUuid01, &deck.UserDeckCharacterUuid02, &deck.UserDeckCharacterUuid03}
for i, uuidPtr := range uuidPtrs {
if i >= len(slots) || slots[i].UserCostumeUuid == "" {
*uuid = ""
*uuidPtr = ""
continue
}
slot := slots[i]
dcUuid := fmt.Sprintf("deck-%d-%d-%d", deckType, userDeckNumber, i+1)
dcUuid := *uuidPtr
if dcUuid == "" {
dcUuid = uuid.New().String()
}
dc := user.DeckCharacters[dcUuid]
dc.UserDeckCharacterUuid = dcUuid
dc.UserCostumeUuid = slot.UserCostumeUuid
@@ -343,7 +352,7 @@ func ApplyDeckReplacement(user *UserState, deckType model.DeckType, userDeckNumb
user.DeckCharacters[dcUuid] = dc
user.DeckSubWeapons[dcUuid] = slot.SubWeaponUuids
user.DeckParts[dcUuid] = slot.PartsUuids
*uuid = dcUuid
*uuidPtr = dcUuid
}
deck.LatestVersion = nowMillis
-198
View File
@@ -1,198 +0,0 @@
package memory
import (
"fmt"
"strings"
"sync"
"time"
"lunar-tear/server/internal/store"
)
type Option func(*MemoryStore)
func WithSnapshotDir(dir string) Option {
return func(s *MemoryStore) {
s.snapshotDir = dir
}
}
func WithSceneId(sceneId int32) Option {
return func(s *MemoryStore) {
s.bootstrapSceneId = sceneId
}
}
func WithStarterItems(v bool) Option {
return func(s *MemoryStore) {
s.starterItems = v
}
}
type MemoryStore struct {
mu sync.RWMutex
clock store.Clock
bootstrapSceneId int32
snapshotDir string
starterItems bool
lastSnapshotSceneId int32
nextUserId int64
users map[int64]*store.UserState
userIdsByUuid map[string]int64
sessionToUserId map[string]int64
sessions map[string]store.SessionState
gachaCatalog map[int32]store.GachaCatalogEntry
}
var (
_ store.UserRepository = (*MemoryStore)(nil)
_ store.SessionRepository = (*MemoryStore)(nil)
_ store.GachaRepository = (*MemoryStore)(nil)
)
func New(clock store.Clock, options ...Option) *MemoryStore {
if clock == nil {
clock = time.Now
}
s := &MemoryStore{
clock: clock,
nextUserId: defaultUserId,
users: make(map[int64]*store.UserState),
userIdsByUuid: make(map[string]int64),
sessionToUserId: make(map[string]int64),
sessions: make(map[string]store.SessionState),
gachaCatalog: make(map[int32]store.GachaCatalogEntry),
}
for _, opt := range options {
opt(s)
}
return s
}
func (s *MemoryStore) EnsureUser(uuid string) (store.UserState, error) {
s.mu.Lock()
defer s.mu.Unlock()
return cloneUserState(*s.getOrCreateLocked(normalizeUUID(uuid))), nil
}
func (s *MemoryStore) CreateSession(uuid string, ttl time.Duration) (store.UserState, store.SessionState, error) {
s.mu.Lock()
defer s.mu.Unlock()
user := s.getOrCreateLocked(normalizeUUID(uuid))
now := s.clock()
session := store.SessionState{
SessionKey: fmt.Sprintf("session_%d_%d", user.UserId, now.UnixNano()),
UserId: user.UserId,
Uuid: user.Uuid,
ExpireAt: now.Add(ttl),
}
s.sessionToUserId[session.SessionKey] = user.UserId
s.sessions[session.SessionKey] = session
return cloneUserState(*user), session, nil
}
func (s *MemoryStore) ResolveUserId(sessionKey string) (int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
userId, ok := s.sessionToUserId[sessionKey]
if !ok {
return 0, store.ErrNotFound
}
return userId, nil
}
func (s *MemoryStore) SnapshotUser(userId int64) (store.UserState, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[userId]
if !ok {
return store.UserState{}, store.ErrNotFound
}
return cloneUserState(*user), nil
}
func (s *MemoryStore) UpdateUser(userId int64, mutate func(*store.UserState)) (store.UserState, error) {
s.mu.Lock()
defer s.mu.Unlock()
user, ok := s.users[userId]
if !ok {
return store.UserState{}, store.ErrNotFound
}
mutate(user)
sceneId := user.MainQuest.CurrentQuestSceneId
if s.snapshotDir != "" && sceneId != 0 && sceneId != s.lastSnapshotSceneId {
saveSnapshot(user, s.snapshotDir)
s.lastSnapshotSceneId = sceneId
}
return cloneUserState(*user), nil
}
func (s *MemoryStore) DefaultUserId() (int64, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if _, ok := s.users[defaultUserId]; ok {
return defaultUserId, nil
}
if len(s.users) == 0 {
return defaultUserId, nil
}
var minUserId int64
for userId := range s.users {
if minUserId == 0 || userId < minUserId {
minUserId = userId
}
}
return minUserId, nil
}
func (s *MemoryStore) SnapshotCatalog() ([]store.GachaCatalogEntry, error) {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]store.GachaCatalogEntry, 0, len(s.gachaCatalog))
for _, entry := range s.gachaCatalog {
out = append(out, cloneGachaCatalogEntry(entry))
}
return out, nil
}
func (s *MemoryStore) ReplaceCatalog(entries []store.GachaCatalogEntry) error {
s.mu.Lock()
defer s.mu.Unlock()
s.gachaCatalog = make(map[int32]store.GachaCatalogEntry, len(entries))
for _, entry := range entries {
s.gachaCatalog[entry.GachaId] = cloneGachaCatalogEntry(entry)
}
return nil
}
func (s *MemoryStore) getOrCreateLocked(uuid string) *store.UserState {
if userId, ok := s.userIdsByUuid[uuid]; ok {
return s.users[userId]
}
userId := s.nextUserId
s.nextUserId++
user := seedUserState(userId, uuid, s.clock().UnixMilli(), s.bootstrapSceneId, s.snapshotDir, s.starterItems)
s.users[userId] = user
s.userIdsByUuid[uuid] = userId
return user
}
func normalizeUUID(uuid string) string {
uuid = strings.TrimSpace(uuid)
if uuid == "" {
return defaultUUID
}
return uuid
}
-222
View File
@@ -1,222 +0,0 @@
package memory
import (
"fmt"
"log"
"time"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
const (
defaultUUID = "default-user"
defaultUserId = int64(1001)
starterMissionId = int32(1)
starterMainQuestRouteId = int32(1)
starterMainQuestSeasonId = int32(1)
missionInProgress = int32(1)
giftUUIDPrefix = "default-gift"
defaultBirthYear = int32(2000)
defaultBirthMonth = int32(1)
defaultBackupToken = "mock-backup-token"
defaultChargeMoneyThisMonth = int64(0)
)
type starterItemDef struct {
Type model.PossessionType
Id int32
Qty int32
}
var defaultStarterItems = []starterItemDef{
{Type: model.PossessionTypeFreeGem, Id: 0, Qty: 300},
{Type: model.PossessionTypeConsumableItem, Id: 9001, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: model.ConsumableIdChapterTicket, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5001, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5002, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 5003, Qty: 1000},
{Type: model.PossessionTypeConsumableItem, Id: 1009, Qty: 1000},
}
func seedUserState(userId int64, uuid string, nowMillis int64, sceneId int32, snapshotDir string, grantStarterItems bool) *store.UserState {
if sceneId != 0 && snapshotDir != "" {
user, err := loadSnapshot(snapshotDir, sceneId)
if err != nil {
log.Fatalf("[bootstrap] no snapshot for scene=%d: %v", sceneId, err)
}
log.Printf("[bootstrap] loaded snapshot for scene=%d", sceneId)
if grantStarterItems {
applyStarterItems(user)
}
return user
}
user := &store.UserState{
UserId: userId,
Uuid: uuid,
PlayerId: userId,
OsType: 2,
PlatformType: 2,
UserRestrictionType: 0,
RegisterDatetime: nowMillis,
GameStartDatetime: nowMillis,
LatestVersion: 0,
BirthYear: defaultBirthYear,
BirthMonth: defaultBirthMonth,
BackupToken: defaultBackupToken,
ChargeMoneyThisMonth: defaultChargeMoneyThisMonth,
Setting: store.UserSettingState{
IsNotifyPurchaseAlert: false,
LatestVersion: 0,
},
Status: store.UserStatusState{
Level: 1,
Exp: 0,
StaminaMilliValue: 50000,
StaminaUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Gem: store.UserGemState{
PaidGem: 10000,
FreeGem: 10000,
},
Profile: store.UserProfileState{
Name: "",
NameUpdateDatetime: 0,
Message: "",
MessageUpdateDatetime: nowMillis,
FavoriteCostumeId: 0,
FavoriteCostumeIdUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Login: store.UserLoginState{
TotalLoginCount: 1,
ContinualLoginCount: 1,
MaxContinualLoginCount: 1,
LastLoginDatetime: nowMillis,
LastComebackLoginDatetime: 0,
LatestVersion: 0,
},
LoginBonus: store.UserLoginBonusState{
LoginBonusId: 1,
CurrentPageNumber: 1,
CurrentStampNumber: 0,
LatestRewardReceiveDatetime: 0,
LatestVersion: 0,
},
Tutorials: map[int32]store.TutorialProgressState{
1: {TutorialType: 1},
},
Battle: store.BattleState{},
Gifts: store.GiftState{
NotReceived: []store.NotReceivedGiftState{
{
GiftCommon: store.GiftCommonState{
PossessionType: int32(model.PossessionTypeFreeGem),
PossessionId: 0,
Count: 300,
GrantDatetime: nowMillis,
},
ExpirationDatetime: nowMillis + int64((7*24*time.Hour)/time.Millisecond),
UserGiftUuid: fmt.Sprintf("%s-%d-1", giftUUIDPrefix, userId),
},
},
Received: []store.ReceivedGiftState{},
},
Gacha: store.GachaState{
ConvertedGachaMedal: store.ConvertedGachaMedalState{
ConvertedMedalPossession: []store.ConsumableItemState{},
},
BannerStates: make(map[int32]store.GachaBannerState),
},
MainQuest: store.MainQuestState{
CurrentMainQuestRouteId: starterMainQuestRouteId,
MainQuestSeasonId: starterMainQuestSeasonId,
},
Notifications: store.NotificationState{
GiftNotReceiveCount: 1,
},
Characters: make(map[int32]store.CharacterState),
Costumes: make(map[string]store.CostumeState),
Weapons: make(map[string]store.WeaponState),
Companions: make(map[string]store.CompanionState),
DeckCharacters: make(map[string]store.DeckCharacterState),
Decks: make(map[store.DeckKey]store.DeckState),
DeckSubWeapons: make(map[string][]string),
DeckParts: make(map[string][]string),
Quests: make(map[int32]store.UserQuestState),
QuestMissions: make(map[store.QuestMissionKey]store.UserQuestMissionState),
SideStoryQuests: make(map[int32]store.SideStoryQuestProgress),
QuestLimitContentStatus: make(map[int32]store.QuestLimitContentStatus),
BigHuntMaxScores: make(map[int32]store.BigHuntMaxScore),
BigHuntStatuses: make(map[int32]store.BigHuntStatus),
BigHuntScheduleMaxScores: make(map[store.BigHuntScheduleScoreKey]store.BigHuntScheduleMaxScore),
BigHuntWeeklyMaxScores: make(map[store.BigHuntWeeklyScoreKey]store.BigHuntWeeklyMaxScore),
BigHuntWeeklyStatuses: make(map[int64]store.BigHuntWeeklyStatus),
WeaponStories: make(map[int32]store.WeaponStoryState),
Missions: map[int32]store.UserMissionState{
starterMissionId: {
MissionId: starterMissionId,
StartDatetime: nowMillis,
MissionProgressStatusType: missionInProgress,
},
},
Gimmick: store.GimmickState{
Progress: make(map[store.GimmickKey]store.GimmickProgressState),
OrnamentProgress: make(map[store.GimmickOrnamentKey]store.GimmickOrnamentProgressState),
Sequences: make(map[store.GimmickSequenceKey]store.GimmickSequenceState),
Unlocks: make(map[store.GimmickKey]store.GimmickUnlockState),
},
CageOrnamentRewards: make(map[int32]store.CageOrnamentRewardState),
ConsumableItems: make(map[int32]int32),
Materials: make(map[int32]int32),
Thoughts: make(map[string]store.ThoughtState),
Parts: make(map[string]store.PartsState),
PartsGroupNotes: make(map[int32]store.PartsGroupNoteState),
PartsPresets: make(map[int32]store.PartsPresetState),
ImportantItems: make(map[int32]int32),
CostumeActiveSkills: make(map[string]store.CostumeActiveSkillState),
WeaponSkills: make(map[string][]store.WeaponSkillState),
WeaponAbilities: make(map[string][]store.WeaponAbilityState),
DeckTypeNotes: make(map[model.DeckType]store.DeckTypeNoteState),
WeaponNotes: make(map[int32]store.WeaponNoteState),
NaviCutInPlayed: make(map[int32]bool),
ViewedMovies: make(map[int32]int64),
ContentsStories: make(map[int32]int64),
DrawnOmikuji: make(map[int32]int64),
PremiumItems: make(map[int32]int64),
DokanConfirmed: make(map[int32]bool),
ShopItems: make(map[int32]store.UserShopItemState),
ShopReplaceableLineup: make(map[int32]store.UserShopReplaceableLineupState),
ExploreScores: make(map[int32]store.ExploreScoreState),
CharacterBoards: make(map[int32]store.CharacterBoardState),
CharacterBoardAbilities: make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState),
CharacterBoardStatusUps: make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState),
CostumeAwakenStatusUps: make(map[store.CostumeAwakenStatusKey]store.CostumeAwakenStatusUpState),
AutoSaleSettings: make(map[int32]store.AutoSaleSettingState),
CharacterRebirths: make(map[int32]store.CharacterRebirthState),
}
store.EnsureDefaultDeck(user, nowMillis)
if grantStarterItems {
applyStarterItems(user)
}
return user
}
func applyStarterItems(user *store.UserState) {
for _, item := range defaultStarterItems {
switch item.Type {
case model.PossessionTypeFreeGem:
user.Gem.FreeGem += item.Qty
case model.PossessionTypeConsumableItem:
user.ConsumableItems[item.Id] += item.Qty
case model.PossessionTypeMaterial:
user.Materials[item.Id] += item.Qty
}
}
}
-95
View File
@@ -1,95 +0,0 @@
package memory
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"lunar-tear/server/internal/store"
)
func snapshotPath(dir string, sceneId int32) string {
return filepath.Join(dir, fmt.Sprintf("scene_%d.json", sceneId))
}
func saveSnapshot(user *store.UserState, dir string) {
sceneId := user.MainQuest.CurrentQuestSceneId
if sceneId == 0 {
return
}
data, err := json.MarshalIndent(user, "", " ")
if err != nil {
log.Printf("[snapshot] marshal error for scene=%d: %v", sceneId, err)
return
}
path := snapshotPath(dir, sceneId)
if err := os.WriteFile(path, data, 0644); err != nil {
log.Printf("[snapshot] write error for scene=%d: %v", sceneId, err)
return
}
log.Printf("[snapshot] saved scene=%d (%d bytes)", sceneId, len(data))
}
// parseSceneId extracts the numeric scene ID from a filename of the form "scene_<id>.json".
// Returns (0, false) if the name does not match the expected format.
func parseSceneId(name string) (int32, bool) {
if !strings.HasPrefix(name, "scene_") || !strings.HasSuffix(name, ".json") {
return 0, false
}
raw := strings.TrimSuffix(strings.TrimPrefix(name, "scene_"), ".json")
id, err := strconv.ParseInt(raw, 10, 32)
if err != nil {
return 0, false
}
return int32(id), true
}
// LatestSnapshotSceneId scans dir for scene_*.json files and returns the scene ID
// of the most recently modified snapshot. Returns (0, false) if none are found.
func LatestSnapshotSceneId(dir string) (int32, bool) {
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
var latestId int32
var latestMod int64
for _, e := range entries {
if e.IsDir() {
continue
}
id, ok := parseSceneId(e.Name())
if !ok {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if mt := info.ModTime().UnixNano(); mt > latestMod {
latestMod = mt
latestId = id
}
}
if latestId == 0 {
return 0, false
}
return latestId, true
}
func loadSnapshot(dir string, sceneId int32) (*store.UserState, error) {
path := snapshotPath(dir, sceneId)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read snapshot scene=%d: %w", sceneId, err)
}
var user store.UserState
if err := json.Unmarshal(data, &user); err != nil {
return nil, fmt.Errorf("unmarshal snapshot scene=%d: %w", sceneId, err)
}
user.EnsureMaps()
return &user, nil
}
+157
View File
@@ -0,0 +1,157 @@
package store
import (
"lunar-tear/server/internal/model"
)
const (
starterMissionId = int32(1)
starterMainQuestRouteId = int32(1)
starterMainQuestSeasonId = int32(1)
missionInProgress = int32(1)
defaultBirthYear = int32(2000)
defaultBirthMonth = int32(1)
defaultBackupToken = "mock-backup-token"
defaultChargeMoneyThisMonth = int64(0)
)
func SeedUserState(userId int64, uuid string, nowMillis int64) *UserState {
user := &UserState{
UserId: userId,
Uuid: uuid,
PlayerId: userId,
OsType: 2,
PlatformType: 2,
UserRestrictionType: 0,
RegisterDatetime: nowMillis,
GameStartDatetime: nowMillis,
LatestVersion: 0,
BirthYear: defaultBirthYear,
BirthMonth: defaultBirthMonth,
BackupToken: defaultBackupToken,
ChargeMoneyThisMonth: defaultChargeMoneyThisMonth,
Setting: UserSettingState{
IsNotifyPurchaseAlert: false,
LatestVersion: 0,
},
Status: UserStatusState{
Level: 1,
Exp: 0,
StaminaMilliValue: 50000,
StaminaUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Gem: UserGemState{
PaidGem: 0,
FreeGem: 0,
},
Profile: UserProfileState{
Name: "",
NameUpdateDatetime: 0,
Message: "",
MessageUpdateDatetime: nowMillis,
FavoriteCostumeId: 0,
FavoriteCostumeIdUpdateDatetime: nowMillis,
LatestVersion: 0,
},
Login: UserLoginState{
TotalLoginCount: 1,
ContinualLoginCount: 1,
MaxContinualLoginCount: 1,
LastLoginDatetime: nowMillis,
LastComebackLoginDatetime: 0,
LatestVersion: 0,
},
LoginBonus: UserLoginBonusState{
LoginBonusId: 1,
CurrentPageNumber: 1,
CurrentStampNumber: 0,
LatestRewardReceiveDatetime: 0,
LatestVersion: 0,
},
Tutorials: map[int32]TutorialProgressState{
1: {TutorialType: 1},
},
Battle: BattleState{},
Gifts: GiftState{
NotReceived: []NotReceivedGiftState{},
Received: []ReceivedGiftState{},
},
Gacha: GachaState{
ConvertedGachaMedal: ConvertedGachaMedalState{
ConvertedMedalPossession: []ConsumableItemState{},
},
BannerStates: make(map[int32]GachaBannerState),
},
MainQuest: MainQuestState{
CurrentMainQuestRouteId: starterMainQuestRouteId,
MainQuestSeasonId: starterMainQuestSeasonId,
},
Notifications: NotificationState{
GiftNotReceiveCount: 1,
},
Characters: make(map[int32]CharacterState),
Costumes: make(map[string]CostumeState),
Weapons: make(map[string]WeaponState),
Companions: make(map[string]CompanionState),
DeckCharacters: make(map[string]DeckCharacterState),
Decks: make(map[DeckKey]DeckState),
DeckSubWeapons: make(map[string][]string),
DeckParts: make(map[string][]string),
Quests: make(map[int32]UserQuestState),
QuestMissions: make(map[QuestMissionKey]UserQuestMissionState),
SideStoryQuests: make(map[int32]SideStoryQuestProgress),
QuestLimitContentStatus: make(map[int32]QuestLimitContentStatus),
BigHuntMaxScores: make(map[int32]BigHuntMaxScore),
BigHuntStatuses: make(map[int32]BigHuntStatus),
BigHuntScheduleMaxScores: make(map[BigHuntScheduleScoreKey]BigHuntScheduleMaxScore),
BigHuntWeeklyMaxScores: make(map[BigHuntWeeklyScoreKey]BigHuntWeeklyMaxScore),
BigHuntWeeklyStatuses: make(map[int64]BigHuntWeeklyStatus),
WeaponStories: make(map[int32]WeaponStoryState),
Missions: map[int32]UserMissionState{
starterMissionId: {
MissionId: starterMissionId,
StartDatetime: nowMillis,
MissionProgressStatusType: missionInProgress,
},
},
Gimmick: GimmickState{
Progress: make(map[GimmickKey]GimmickProgressState),
OrnamentProgress: make(map[GimmickOrnamentKey]GimmickOrnamentProgressState),
Sequences: make(map[GimmickSequenceKey]GimmickSequenceState),
Unlocks: make(map[GimmickKey]GimmickUnlockState),
},
CageOrnamentRewards: make(map[int32]CageOrnamentRewardState),
ConsumableItems: make(map[int32]int32),
Materials: make(map[int32]int32),
Thoughts: make(map[string]ThoughtState),
Parts: make(map[string]PartsState),
PartsGroupNotes: make(map[int32]PartsGroupNoteState),
PartsPresets: make(map[int32]PartsPresetState),
ImportantItems: make(map[int32]int32),
CostumeActiveSkills: make(map[string]CostumeActiveSkillState),
WeaponSkills: make(map[string][]WeaponSkillState),
WeaponAbilities: make(map[string][]WeaponAbilityState),
DeckTypeNotes: make(map[model.DeckType]DeckTypeNoteState),
WeaponNotes: make(map[int32]WeaponNoteState),
NaviCutInPlayed: make(map[int32]bool),
ViewedMovies: make(map[int32]int64),
ContentsStories: make(map[int32]int64),
DrawnOmikuji: make(map[int32]int64),
PremiumItems: make(map[int32]int64),
DokanConfirmed: make(map[int32]bool),
ShopItems: make(map[int32]UserShopItemState),
ShopReplaceableLineup: make(map[int32]UserShopReplaceableLineupState),
ExploreScores: make(map[int32]ExploreScoreState),
CharacterBoards: make(map[int32]CharacterBoardState),
CharacterBoardAbilities: make(map[CharacterBoardAbilityKey]CharacterBoardAbilityState),
CharacterBoardStatusUps: make(map[CharacterBoardStatusUpKey]CharacterBoardStatusUpState),
CostumeAwakenStatusUps: make(map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState),
AutoSaleSettings: make(map[int32]AutoSaleSettingState),
CharacterRebirths: make(map[int32]CharacterRebirthState),
}
return user
}
+724
View File
@@ -0,0 +1,724 @@
package sqlite
import (
"database/sql"
"fmt"
"lunar-tear/server/internal/model"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) LoadUser(userId int64) (store.UserState, error) {
var u store.UserState
err := s.db.QueryRow(`SELECT user_id, uuid, player_id, os_type, platform_type, user_restriction_type,
register_datetime, game_start_datetime, latest_version, birth_year, birth_month,
backup_token, charge_money_this_month FROM users WHERE user_id = ?`, userId).Scan(
&u.UserId, &u.Uuid, &u.PlayerId, &u.OsType, &u.PlatformType, &u.UserRestrictionType,
&u.RegisterDatetime, &u.GameStartDatetime, &u.LatestVersion, &u.BirthYear, &u.BirthMonth,
&u.BackupToken, &u.ChargeMoneyThisMonth)
if err == sql.ErrNoRows {
return u, store.ErrNotFound
}
if err != nil {
return u, fmt.Errorf("load users: %w", err)
}
initMaps(&u)
load1to1(s.db, userId, &u)
loadMapTables(s.db, userId, &u)
return u, nil
}
func initMaps(u *store.UserState) {
u.Tutorials = make(map[int32]store.TutorialProgressState)
u.Characters = make(map[int32]store.CharacterState)
u.Costumes = make(map[string]store.CostumeState)
u.Weapons = make(map[string]store.WeaponState)
u.Companions = make(map[string]store.CompanionState)
u.Thoughts = make(map[string]store.ThoughtState)
u.DeckCharacters = make(map[string]store.DeckCharacterState)
u.Decks = make(map[store.DeckKey]store.DeckState)
u.DeckSubWeapons = make(map[string][]string)
u.DeckParts = make(map[string][]string)
u.Quests = make(map[int32]store.UserQuestState)
u.QuestMissions = make(map[store.QuestMissionKey]store.UserQuestMissionState)
u.Missions = make(map[int32]store.UserMissionState)
u.WeaponStories = make(map[int32]store.WeaponStoryState)
u.WeaponNotes = make(map[int32]store.WeaponNoteState)
u.WeaponSkills = make(map[string][]store.WeaponSkillState)
u.WeaponAbilities = make(map[string][]store.WeaponAbilityState)
u.WeaponAwakens = make(map[string]store.WeaponAwakenState)
u.CostumeActiveSkills = make(map[string]store.CostumeActiveSkillState)
u.CostumeAwakenStatusUps = make(map[store.CostumeAwakenStatusKey]store.CostumeAwakenStatusUpState)
u.CostumeLotteryEffects = make(map[store.CostumeLotteryEffectKey]store.CostumeLotteryEffectState)
u.CostumeLotteryEffectPending = make(map[string]store.CostumeLotteryEffectPendingState)
u.Parts = make(map[string]store.PartsState)
u.PartsGroupNotes = make(map[int32]store.PartsGroupNoteState)
u.PartsPresets = make(map[int32]store.PartsPresetState)
u.DeckTypeNotes = make(map[model.DeckType]store.DeckTypeNoteState)
u.ConsumableItems = make(map[int32]int32)
u.Materials = make(map[int32]int32)
u.ImportantItems = make(map[int32]int32)
u.PremiumItems = make(map[int32]int64)
u.NaviCutInPlayed = make(map[int32]bool)
u.ViewedMovies = make(map[int32]int64)
u.ContentsStories = make(map[int32]int64)
u.DrawnOmikuji = make(map[int32]int64)
u.DokanConfirmed = make(map[int32]bool)
u.ShopItems = make(map[int32]store.UserShopItemState)
u.ShopReplaceableLineup = make(map[int32]store.UserShopReplaceableLineupState)
u.ExploreScores = make(map[int32]store.ExploreScoreState)
u.CageOrnamentRewards = make(map[int32]store.CageOrnamentRewardState)
u.CharacterBoards = make(map[int32]store.CharacterBoardState)
u.CharacterBoardAbilities = make(map[store.CharacterBoardAbilityKey]store.CharacterBoardAbilityState)
u.CharacterBoardStatusUps = make(map[store.CharacterBoardStatusUpKey]store.CharacterBoardStatusUpState)
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
u.BigHuntScheduleMaxScores = make(map[store.BigHuntScheduleScoreKey]store.BigHuntScheduleMaxScore)
u.BigHuntWeeklyMaxScores = make(map[store.BigHuntWeeklyScoreKey]store.BigHuntWeeklyMaxScore)
u.BigHuntWeeklyStatuses = make(map[int64]store.BigHuntWeeklyStatus)
u.Gacha.BannerStates = make(map[int32]store.GachaBannerState)
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = []store.ConsumableItemState{}
u.Gifts.NotReceived = []store.NotReceivedGiftState{}
u.Gifts.Received = []store.ReceivedGiftState{}
u.Gimmick.Progress = make(map[store.GimmickKey]store.GimmickProgressState)
u.Gimmick.OrnamentProgress = make(map[store.GimmickOrnamentKey]store.GimmickOrnamentProgressState)
u.Gimmick.Sequences = make(map[store.GimmickSequenceKey]store.GimmickSequenceState)
u.Gimmick.Unlocks = make(map[store.GimmickKey]store.GimmickUnlockState)
}
func load1to1(db *sql.DB, uid int64, u *store.UserState) {
var b int
_ = db.QueryRow(`SELECT is_notify_purchase_alert, latest_version FROM user_setting WHERE user_id=?`, uid).
Scan(&b, &u.Setting.LatestVersion)
u.Setting.IsNotifyPurchaseAlert = b != 0
_ = db.QueryRow(`SELECT level, exp, stamina_milli_value, stamina_update_datetime, latest_version FROM user_status WHERE user_id=?`, uid).
Scan(&u.Status.Level, &u.Status.Exp, &u.Status.StaminaMilliValue, &u.Status.StaminaUpdateDatetime, &u.Status.LatestVersion)
_ = db.QueryRow(`SELECT paid_gem, free_gem FROM user_gem WHERE user_id=?`, uid).
Scan(&u.Gem.PaidGem, &u.Gem.FreeGem)
_ = db.QueryRow(`SELECT name, name_update_datetime, message, message_update_datetime, favorite_costume_id,
favorite_costume_id_update_datetime, latest_version FROM user_profile WHERE user_id=?`, uid).
Scan(&u.Profile.Name, &u.Profile.NameUpdateDatetime, &u.Profile.Message, &u.Profile.MessageUpdateDatetime,
&u.Profile.FavoriteCostumeId, &u.Profile.FavoriteCostumeIdUpdateDatetime, &u.Profile.LatestVersion)
_ = db.QueryRow(`SELECT total_login_count, continual_login_count, max_continual_login_count,
last_login_datetime, last_comeback_login_datetime, latest_version FROM user_login WHERE user_id=?`, uid).
Scan(&u.Login.TotalLoginCount, &u.Login.ContinualLoginCount, &u.Login.MaxContinualLoginCount,
&u.Login.LastLoginDatetime, &u.Login.LastComebackLoginDatetime, &u.Login.LatestVersion)
_ = db.QueryRow(`SELECT login_bonus_id, current_page_number, current_stamp_number,
latest_reward_receive_datetime, latest_version FROM user_login_bonus WHERE user_id=?`, uid).
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
_ = db.QueryRow(`SELECT current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id,
head_quest_scene_id, is_reached_last_quest_scene, progress_quest_scene_id, progress_head_quest_scene_id,
progress_quest_flow_type, main_quest_season_id, latest_version, saved_current_quest_scene_id,
saved_head_quest_scene_id, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
FROM user_main_quest WHERE user_id=?`, uid).
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
&u.MainQuest.SavedCurrentQuestSceneId, &u.MainQuest.SavedHeadQuestSceneId,
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
u.MainQuest.IsReachedLastQuestScene = b != 0
_ = db.QueryRow(`SELECT current_event_quest_chapter_id, current_quest_id, current_quest_scene_id,
head_quest_scene_id, latest_version FROM user_event_quest WHERE user_id=?`, uid).
Scan(&u.EventQuest.CurrentEventQuestChapterId, &u.EventQuest.CurrentQuestId,
&u.EventQuest.CurrentQuestSceneId, &u.EventQuest.HeadQuestSceneId, &u.EventQuest.LatestVersion)
_ = db.QueryRow(`SELECT current_quest_id, current_quest_scene_id, head_quest_scene_id, latest_version
FROM user_extra_quest WHERE user_id=?`, uid).
Scan(&u.ExtraQuest.CurrentQuestId, &u.ExtraQuest.CurrentQuestSceneId,
&u.ExtraQuest.HeadQuestSceneId, &u.ExtraQuest.LatestVersion)
_ = db.QueryRow(`SELECT current_side_story_quest_id, current_side_story_quest_scene_id, latest_version
FROM user_side_story_active WHERE user_id=?`, uid).
Scan(&u.SideStoryActiveProgress.CurrentSideStoryQuestId,
&u.SideStoryActiveProgress.CurrentSideStoryQuestSceneId, &u.SideStoryActiveProgress.LatestVersion)
var isDryRun int
_ = db.QueryRow(`SELECT current_big_hunt_boss_quest_id, current_big_hunt_quest_id, current_quest_scene_id,
is_dry_run, latest_version, deck_type, user_triple_deck_number, boss_knock_down_count,
max_combo_count, total_damage, deck_number, battle_binary
FROM user_big_hunt_state WHERE user_id=?`, uid).
Scan(&u.BigHuntProgress.CurrentBigHuntBossQuestId, &u.BigHuntProgress.CurrentBigHuntQuestId,
&u.BigHuntProgress.CurrentQuestSceneId, &isDryRun, &u.BigHuntProgress.LatestVersion,
&u.BigHuntBattleDetail.DeckType, &u.BigHuntBattleDetail.UserTripleDeckNumber,
&u.BigHuntBattleDetail.BossKnockDownCount, &u.BigHuntBattleDetail.MaxComboCount,
&u.BigHuntBattleDetail.TotalDamage, &u.BigHuntDeckNumber, &u.BigHuntBattleBinary)
u.BigHuntProgress.IsDryRun = isDryRun != 0
var isActive, isUnread int
_ = db.QueryRow(`SELECT is_active, start_count, finish_count, last_started_at, last_finished_at,
last_user_party_count, last_npc_party_count, last_battle_binary_size, last_elapsed_frame_count
FROM user_battle WHERE user_id=?`, uid).
Scan(&isActive, &u.Battle.StartCount, &u.Battle.FinishCount, &u.Battle.LastStartedAt,
&u.Battle.LastFinishedAt, &u.Battle.LastUserPartyCount, &u.Battle.LastNpcPartyCount,
&u.Battle.LastBattleBinarySize, &u.Battle.LastElapsedFrameCount)
u.Battle.IsActive = isActive != 0
_ = db.QueryRow(`SELECT gift_not_receive_count, friend_request_receive_count, is_exist_unread_information
FROM user_notification WHERE user_id=?`, uid).
Scan(&u.Notifications.GiftNotReceiveCount, &u.Notifications.FriendRequestReceiveCount, &isUnread)
u.Notifications.IsExistUnreadInformation = isUnread != 0
var isCP int
_ = db.QueryRow(`SELECT is_current_progress, drop_item_start_datetime, current_drop_item_count, latest_version
FROM user_portal_cage WHERE user_id=?`, uid).
Scan(&isCP, &u.PortalCageStatus.DropItemStartDatetime, &u.PortalCageStatus.CurrentDropItemCount,
&u.PortalCageStatus.LatestVersion)
u.PortalCageStatus.IsCurrentProgress = isCP != 0
_ = db.QueryRow(`SELECT start_datetime, open_minutes, daily_opened_count, latest_version
FROM user_guerrilla_free_open WHERE user_id=?`, uid).
Scan(&u.GuerrillaFreeOpen.StartDatetime, &u.GuerrillaFreeOpen.OpenMinutes,
&u.GuerrillaFreeOpen.DailyOpenedCount, &u.GuerrillaFreeOpen.LatestVersion)
var isTicket int
_ = db.QueryRow(`SELECT is_use_explore_ticket, playing_explore_id, latest_play_datetime, latest_version
FROM user_explore WHERE user_id=?`, uid).
Scan(&isTicket, &u.Explore.PlayingExploreId, &u.Explore.LatestPlayDatetime, &u.Explore.LatestVersion)
u.Explore.IsUseExploreTicket = isTicket != 0
_ = db.QueryRow(`SELECT lineup_update_count, latest_lineup_update_datetime, latest_version
FROM user_shop_replaceable WHERE user_id=?`, uid).
Scan(&u.ShopReplaceable.LineupUpdateCount, &u.ShopReplaceable.LatestLineupUpdateDatetime,
&u.ShopReplaceable.LatestVersion)
var rewardAvail int
var obtainItemId, obtainCount sql.NullInt64
_ = db.QueryRow(`SELECT reward_available, todays_current_draw_count, daily_max_count,
last_reward_draw_date, obtain_consumable_item_id, obtain_count
FROM user_gacha WHERE user_id=?`, uid).
Scan(&rewardAvail, &u.Gacha.TodaysCurrentDrawCount, &u.Gacha.DailyMaxCount,
&u.Gacha.LastRewardDrawDate, &obtainItemId, &obtainCount)
u.Gacha.RewardAvailable = rewardAvail != 0
if obtainItemId.Valid {
u.Gacha.ConvertedGachaMedal.ObtainPossession = &store.ConsumableItemState{
ConsumableItemId: int32(obtainItemId.Int64),
Count: int32(obtainCount.Int64),
}
}
}
func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
queryRows(db, `SELECT character_id, level, exp, latest_version FROM user_characters WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterState
rows.Scan(&v.CharacterId, &v.Level, &v.Exp, &v.LatestVersion)
u.Characters[v.CharacterId] = v
})
queryRows(db, `SELECT user_costume_uuid, costume_id, limit_break_count, level, exp,
headup_display_view_id, acquisition_datetime, awaken_count,
costume_lottery_effect_unlocked_slot_count, latest_version
FROM user_costumes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CostumeState
rows.Scan(&v.UserCostumeUuid, &v.CostumeId, &v.LimitBreakCount, &v.Level, &v.Exp,
&v.HeadupDisplayViewId, &v.AcquisitionDatetime, &v.AwakenCount,
&v.CostumeLotteryEffectUnlockedSlotCount, &v.LatestVersion)
u.Costumes[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_weapon_uuid, weapon_id, level, exp, limit_break_count,
is_protected, acquisition_datetime, latest_version FROM user_weapons WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponState
var prot int
rows.Scan(&v.UserWeaponUuid, &v.WeaponId, &v.Level, &v.Exp, &v.LimitBreakCount,
&prot, &v.AcquisitionDatetime, &v.LatestVersion)
v.IsProtected = prot != 0
u.Weapons[v.UserWeaponUuid] = v
})
queryRows(db, `SELECT user_companion_uuid, companion_id, headup_display_view_id, level,
acquisition_datetime, latest_version FROM user_companions WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CompanionState
rows.Scan(&v.UserCompanionUuid, &v.CompanionId, &v.HeadupDisplayViewId, &v.Level,
&v.AcquisitionDatetime, &v.LatestVersion)
u.Companions[v.UserCompanionUuid] = v
})
queryRows(db, `SELECT user_thought_uuid, thought_id, acquisition_datetime, latest_version
FROM user_thoughts WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.ThoughtState
rows.Scan(&v.UserThoughtUuid, &v.ThoughtId, &v.AcquisitionDatetime, &v.LatestVersion)
u.Thoughts[v.UserThoughtUuid] = v
})
queryRows(db, `SELECT user_deck_character_uuid, user_costume_uuid, main_user_weapon_uuid,
user_companion_uuid, power, user_thought_uuid, dressup_costume_id, latest_version
FROM user_deck_characters WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.DeckCharacterState
rows.Scan(&v.UserDeckCharacterUuid, &v.UserCostumeUuid, &v.MainUserWeaponUuid,
&v.UserCompanionUuid, &v.Power, &v.UserThoughtUuid, &v.DressupCostumeId, &v.LatestVersion)
u.DeckCharacters[v.UserDeckCharacterUuid] = v
})
queryRows(db, `SELECT deck_type, user_deck_number, user_deck_character_uuid01, user_deck_character_uuid02,
user_deck_character_uuid03, name, power, latest_version FROM user_decks WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.DeckState
var dt int32
rows.Scan(&dt, &v.UserDeckNumber, &v.UserDeckCharacterUuid01, &v.UserDeckCharacterUuid02,
&v.UserDeckCharacterUuid03, &v.Name, &v.Power, &v.LatestVersion)
v.DeckType = model.DeckType(dt)
u.Decks[store.DeckKey{DeckType: v.DeckType, UserDeckNumber: v.UserDeckNumber}] = v
})
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_weapon_uuid
FROM user_deck_sub_weapons WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
func(rows *sql.Rows) {
var key, val string
var ord int
rows.Scan(&key, &ord, &val)
u.DeckSubWeapons[key] = append(u.DeckSubWeapons[key], val)
})
queryRows(db, `SELECT user_deck_character_uuid, ordinal, user_parts_uuid
FROM user_deck_parts WHERE user_id=? ORDER BY user_deck_character_uuid, ordinal`, uid,
func(rows *sql.Rows) {
var key, val string
var ord int
rows.Scan(&key, &ord, &val)
u.DeckParts[key] = append(u.DeckParts[key], val)
})
queryRows(db, `SELECT quest_id, quest_state_type, is_battle_only, user_deck_number, latest_start_datetime,
clear_count, daily_clear_count, last_clear_datetime, shortest_clear_frames, is_reward_granted, latest_version
FROM user_quests WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserQuestState
var bo, rg int
rows.Scan(&v.QuestId, &v.QuestStateType, &bo, &v.UserDeckNumber, &v.LatestStartDatetime,
&v.ClearCount, &v.DailyClearCount, &v.LastClearDatetime, &v.ShortestClearFrames, &rg, &v.LatestVersion)
v.IsBattleOnly = bo != 0
v.IsRewardGranted = rg != 0
u.Quests[v.QuestId] = v
})
queryRows(db, `SELECT quest_id, quest_mission_id, progress_value, is_clear, latest_clear_datetime, latest_version
FROM user_quest_missions WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserQuestMissionState
var ic int
rows.Scan(&v.QuestId, &v.QuestMissionId, &v.ProgressValue, &ic, &v.LatestClearDatetime, &v.LatestVersion)
v.IsClear = ic != 0
u.QuestMissions[store.QuestMissionKey{QuestId: v.QuestId, QuestMissionId: v.QuestMissionId}] = v
})
queryRows(db, `SELECT mission_id, start_datetime, progress_value, mission_progress_status_type,
clear_datetime, latest_version FROM user_missions WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserMissionState
rows.Scan(&v.MissionId, &v.StartDatetime, &v.ProgressValue, &v.MissionProgressStatusType,
&v.ClearDatetime, &v.LatestVersion)
u.Missions[v.MissionId] = v
})
queryRows(db, `SELECT tutorial_type, progress_phase, choice_id, latest_version
FROM user_tutorials WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.TutorialProgressState
rows.Scan(&v.TutorialType, &v.ProgressPhase, &v.ChoiceId, &v.LatestVersion)
u.Tutorials[v.TutorialType] = v
})
queryRows(db, `SELECT side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version
FROM user_side_story_quests WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id, head, st int32
var lv int64
rows.Scan(&id, &head, &st, &lv)
u.SideStoryQuests[id] = store.SideStoryQuestProgress{
HeadSideStoryQuestSceneId: head, SideStoryQuestStateType: model.SideStoryQuestStateType(st), LatestVersion: lv,
}
})
queryRows(db, `SELECT limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version
FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.QuestLimitContentStatus
rows.Scan(&id, &v.LimitContentQuestStatusType, &v.EventQuestChapterId, &v.LatestVersion)
u.QuestLimitContentStatus[id] = v
})
queryRows(db, `SELECT weapon_id, released_max_story_index, latest_version FROM user_weapon_stories WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponStoryState
rows.Scan(&v.WeaponId, &v.ReleasedMaxStoryIndex, &v.LatestVersion)
u.WeaponStories[v.WeaponId] = v
})
queryRows(db, `SELECT weapon_id, max_level, max_limit_break_count, first_acquisition_datetime, latest_version
FROM user_weapon_notes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.WeaponNoteState
rows.Scan(&v.WeaponId, &v.MaxLevel, &v.MaxLimitBreakCount, &v.FirstAcquisitionDatetime, &v.LatestVersion)
u.WeaponNotes[v.WeaponId] = v
})
queryRows(db, `SELECT user_weapon_uuid, slot_number, level FROM user_weapon_skills WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponSkillState
rows.Scan(&v.UserWeaponUuid, &v.SlotNumber, &v.Level)
u.WeaponSkills[v.UserWeaponUuid] = append(u.WeaponSkills[v.UserWeaponUuid], v)
})
queryRows(db, `SELECT user_weapon_uuid, slot_number, level FROM user_weapon_abilities WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponAbilityState
rows.Scan(&v.UserWeaponUuid, &v.SlotNumber, &v.Level)
u.WeaponAbilities[v.UserWeaponUuid] = append(u.WeaponAbilities[v.UserWeaponUuid], v)
})
queryRows(db, `SELECT user_weapon_uuid, latest_version FROM user_weapon_awakens WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.WeaponAwakenState
rows.Scan(&v.UserWeaponUuid, &v.LatestVersion)
u.WeaponAwakens[v.UserWeaponUuid] = v
})
queryRows(db, `SELECT user_costume_uuid, level, acquisition_datetime, latest_version
FROM user_costume_active_skills WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CostumeActiveSkillState
rows.Scan(&v.UserCostumeUuid, &v.Level, &v.AcquisitionDatetime, &v.LatestVersion)
u.CostumeActiveSkills[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_costume_uuid, status_calculation_type, hp, attack, vitality, agility,
critical_ratio, critical_attack, latest_version FROM user_costume_awaken_status_ups WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeAwakenStatusUpState
var sct int32
rows.Scan(&v.UserCostumeUuid, &sct, &v.Hp, &v.Attack, &v.Vitality, &v.Agility,
&v.CriticalRatio, &v.CriticalAttack, &v.LatestVersion)
v.StatusCalculationType = model.StatusCalculationType(sct)
u.CostumeAwakenStatusUps[store.CostumeAwakenStatusKey{
UserCostumeUuid: v.UserCostumeUuid, StatusCalculationType: v.StatusCalculationType,
}] = v
})
queryRows(db, `SELECT user_costume_uuid, slot_number, odds_number, latest_version
FROM user_costume_lottery_effects WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeLotteryEffectState
rows.Scan(&v.UserCostumeUuid, &v.SlotNumber, &v.OddsNumber, &v.LatestVersion)
u.CostumeLotteryEffects[store.CostumeLotteryEffectKey{
UserCostumeUuid: v.UserCostumeUuid, SlotNumber: v.SlotNumber,
}] = v
})
queryRows(db, `SELECT user_costume_uuid, slot_number, odds_number, latest_version
FROM user_costume_lottery_effect_pending WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CostumeLotteryEffectPendingState
rows.Scan(&v.UserCostumeUuid, &v.SlotNumber, &v.OddsNumber, &v.LatestVersion)
u.CostumeLotteryEffectPending[v.UserCostumeUuid] = v
})
queryRows(db, `SELECT user_parts_uuid, parts_id, level, parts_status_main_id, is_protected,
acquisition_datetime, latest_version FROM user_parts WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.PartsState
var prot int
rows.Scan(&v.UserPartsUuid, &v.PartsId, &v.Level, &v.PartsStatusMainId, &prot,
&v.AcquisitionDatetime, &v.LatestVersion)
v.IsProtected = prot != 0
u.Parts[v.UserPartsUuid] = v
})
queryRows(db, `SELECT parts_group_id, first_acquisition_datetime, latest_version
FROM user_parts_group_notes WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.PartsGroupNoteState
rows.Scan(&v.PartsGroupId, &v.FirstAcquisitionDatetime, &v.LatestVersion)
u.PartsGroupNotes[v.PartsGroupId] = v
})
queryRows(db, `SELECT user_parts_preset_number, user_parts_uuid01, user_parts_uuid02, user_parts_uuid03,
name, user_parts_preset_tag_number, latest_version FROM user_parts_presets WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.PartsPresetState
rows.Scan(&v.UserPartsPresetNumber, &v.UserPartsUuid01, &v.UserPartsUuid02, &v.UserPartsUuid03,
&v.Name, &v.UserPartsPresetTagNumber, &v.LatestVersion)
u.PartsPresets[v.UserPartsPresetNumber] = v
})
queryRows(db, `SELECT deck_type, max_deck_power, latest_version FROM user_deck_type_notes WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var dt int32
var v store.DeckTypeNoteState
rows.Scan(&dt, &v.MaxDeckPower, &v.LatestVersion)
v.DeckType = model.DeckType(dt)
u.DeckTypeNotes[v.DeckType] = v
})
loadSimpleMap(db, uid, `SELECT consumable_item_id, count FROM user_consumable_items WHERE user_id=?`, u.ConsumableItems)
loadSimpleMap(db, uid, `SELECT material_id, count FROM user_materials WHERE user_id=?`, u.Materials)
loadSimpleMap(db, uid, `SELECT important_item_id, count FROM user_important_items WHERE user_id=?`, u.ImportantItems)
queryRows(db, `SELECT premium_item_id, count FROM user_premium_items WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var k int32
var v int64
rows.Scan(&k, &v)
u.PremiumItems[k] = v
})
queryRows(db, `SELECT explore_id, max_score, max_score_update_datetime, latest_version
FROM user_explore_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.ExploreScoreState
rows.Scan(&v.ExploreId, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.ExploreScores[v.ExploreId] = v
})
queryRows(db, `SELECT possession_auto_sale_item_type, possession_auto_sale_item_value
FROM user_auto_sale_settings WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.AutoSaleSettingState
rows.Scan(&v.PossessionAutoSaleItemType, &v.PossessionAutoSaleItemValue)
u.AutoSaleSettings[v.PossessionAutoSaleItemType] = v
})
queryRows(db, `SELECT navi_cutin_id FROM user_navi_cutin_played WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var id int32
rows.Scan(&id)
u.NaviCutInPlayed[id] = true
})
loadTimestampMap(db, uid, `SELECT movie_id, timestamp FROM user_viewed_movies WHERE user_id=?`, u.ViewedMovies)
loadTimestampMap(db, uid, `SELECT contents_story_id, timestamp FROM user_contents_stories WHERE user_id=?`, u.ContentsStories)
loadTimestampMap(db, uid, `SELECT omikuji_id, timestamp FROM user_drawn_omikuji WHERE user_id=?`, u.DrawnOmikuji)
queryRows(db, `SELECT dokan_id FROM user_dokan_confirmed WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var id int32
rows.Scan(&id)
u.DokanConfirmed[id] = true
})
// Gifts
queryRows(db, `SELECT user_gift_uuid, is_received, possession_type, possession_id, count, grant_datetime,
description_gift_text_id, equipment_data, expiration_datetime, received_datetime
FROM user_gifts WHERE user_id=?`, uid, func(rows *sql.Rows) {
var uuid string
var isRecv int
var gc store.GiftCommonState
var expDt, recvDt sql.NullInt64
var equipData []byte
rows.Scan(&uuid, &isRecv, &gc.PossessionType, &gc.PossessionId, &gc.Count, &gc.GrantDatetime,
&gc.DescriptionGiftTextId, &equipData, &expDt, &recvDt)
gc.EquipmentData = equipData
if isRecv == 0 {
u.Gifts.NotReceived = append(u.Gifts.NotReceived, store.NotReceivedGiftState{
GiftCommon: gc, ExpirationDatetime: expDt.Int64, UserGiftUuid: uuid,
})
} else {
u.Gifts.Received = append(u.Gifts.Received, store.ReceivedGiftState{
GiftCommon: gc, ReceivedDatetime: recvDt.Int64,
})
}
})
// Gacha converted medals
queryRows(db, `SELECT consumable_item_id, count FROM user_gacha_converted_medals WHERE user_id=? ORDER BY ordinal`, uid,
func(rows *sql.Rows) {
var v store.ConsumableItemState
rows.Scan(&v.ConsumableItemId, &v.Count)
u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession = append(u.Gacha.ConvertedGachaMedal.ConvertedMedalPossession, v)
})
// Gacha banners
queryRows(db, `SELECT gacha_id, medal_count, step_number, loop_count, draw_count, box_number
FROM user_gacha_banners WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.GachaBannerState
rows.Scan(&v.GachaId, &v.MedalCount, &v.StepNumber, &v.LoopCount, &v.DrawCount, &v.BoxNumber)
v.BoxDrewCounts = make(map[int32]int32)
u.Gacha.BannerStates[v.GachaId] = v
})
queryRows(db, `SELECT gacha_id, box_item_id, count FROM user_gacha_banner_box_drew_counts WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var gachaId, boxItemId, count int32
rows.Scan(&gachaId, &boxItemId, &count)
if bs, ok := u.Gacha.BannerStates[gachaId]; ok {
bs.BoxDrewCounts[boxItemId] = count
u.Gacha.BannerStates[gachaId] = bs
}
})
// Character boards
queryRows(db, `SELECT character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3,
panel_release_bit4, latest_version FROM user_character_boards WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterBoardState
rows.Scan(&v.CharacterBoardId, &v.PanelReleaseBit1, &v.PanelReleaseBit2,
&v.PanelReleaseBit3, &v.PanelReleaseBit4, &v.LatestVersion)
u.CharacterBoards[v.CharacterBoardId] = v
})
queryRows(db, `SELECT character_id, ability_id, level, latest_version
FROM user_character_board_abilities WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CharacterBoardAbilityState
rows.Scan(&v.CharacterId, &v.AbilityId, &v.Level, &v.LatestVersion)
u.CharacterBoardAbilities[store.CharacterBoardAbilityKey{CharacterId: v.CharacterId, AbilityId: v.AbilityId}] = v
})
queryRows(db, `SELECT character_id, status_calculation_type, hp, attack, vitality, agility,
critical_ratio, critical_attack, latest_version FROM user_character_board_status_ups WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterBoardStatusUpState
rows.Scan(&v.CharacterId, &v.StatusCalculationType, &v.Hp, &v.Attack, &v.Vitality, &v.Agility,
&v.CriticalRatio, &v.CriticalAttack, &v.LatestVersion)
u.CharacterBoardStatusUps[store.CharacterBoardStatusUpKey{
CharacterId: v.CharacterId, StatusCalculationType: v.StatusCalculationType,
}] = v
})
queryRows(db, `SELECT character_id, rebirth_count, latest_version FROM user_character_rebirths WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.CharacterRebirthState
rows.Scan(&v.CharacterId, &v.RebirthCount, &v.LatestVersion)
u.CharacterRebirths[v.CharacterId] = v
})
queryRows(db, `SELECT cage_ornament_id, acquisition_datetime, latest_version
FROM user_cage_ornament_rewards WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.CageOrnamentRewardState
rows.Scan(&v.CageOrnamentId, &v.AcquisitionDatetime, &v.LatestVersion)
u.CageOrnamentRewards[v.CageOrnamentId] = v
})
queryRows(db, `SELECT shop_item_id, bought_count, latest_bought_count_changed_datetime, latest_version
FROM user_shop_items WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.UserShopItemState
rows.Scan(&v.ShopItemId, &v.BoughtCount, &v.LatestBoughtCountChangedDatetime, &v.LatestVersion)
u.ShopItems[v.ShopItemId] = v
})
queryRows(db, `SELECT slot_number, shop_item_id, latest_version FROM user_shop_replaceable_lineup WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.UserShopReplaceableLineupState
rows.Scan(&v.SlotNumber, &v.ShopItemId, &v.LatestVersion)
u.ShopReplaceableLineup[v.SlotNumber] = v
})
// Gimmick tables
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
is_gimmick_cleared, start_datetime, latest_version FROM user_gimmick_progress WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickProgressState
var ic int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&ic, &v.StartDatetime, &v.LatestVersion)
v.IsGimmickCleared = ic != 0
u.Gimmick.Progress[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
gimmick_ornament_index, progress_value_bit, base_datetime, latest_version
FROM user_gimmick_ornament_progress WHERE user_id=?`, uid, func(rows *sql.Rows) {
var v store.GimmickOrnamentProgressState
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&v.Key.GimmickOrnamentIndex, &v.ProgressValueBit, &v.BaseDatetime, &v.LatestVersion)
u.Gimmick.OrnamentProgress[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id,
is_gimmick_sequence_cleared, clear_datetime, latest_version FROM user_gimmick_sequences WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickSequenceState
var ic int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId,
&ic, &v.ClearDatetime, &v.LatestVersion)
v.IsGimmickSequenceCleared = ic != 0
u.Gimmick.Sequences[v.Key] = v
})
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
is_unlocked, latest_version FROM user_gimmick_unlocks WHERE user_id=?`, uid,
func(rows *sql.Rows) {
var v store.GimmickUnlockState
var iu int
rows.Scan(&v.Key.GimmickSequenceScheduleId, &v.Key.GimmickSequenceId, &v.Key.GimmickId,
&iu, &v.LatestVersion)
v.IsUnlocked = iu != 0
u.Gimmick.Unlocks[v.Key] = v
})
// Big hunt maps
queryRows(db, `SELECT big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
FROM user_big_hunt_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.BigHuntMaxScore
rows.Scan(&id, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.BigHuntMaxScores[id] = v
})
queryRows(db, `SELECT big_hunt_boss_id, daily_challenge_count, latest_challenge_datetime, latest_version
FROM user_big_hunt_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
var id int32
var v store.BigHuntStatus
rows.Scan(&id, &v.DailyChallengeCount, &v.LatestChallengeDatetime, &v.LatestVersion)
u.BigHuntStatuses[id] = v
})
queryRows(db, `SELECT big_hunt_schedule_id, big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
FROM user_big_hunt_schedule_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var k store.BigHuntScheduleScoreKey
var v store.BigHuntScheduleMaxScore
rows.Scan(&k.BigHuntScheduleId, &k.BigHuntBossId, &v.MaxScore, &v.MaxScoreUpdateDatetime, &v.LatestVersion)
u.BigHuntScheduleMaxScores[k] = v
})
queryRows(db, `SELECT big_hunt_weekly_version, attribute_type, max_score, latest_version
FROM user_big_hunt_weekly_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
var k store.BigHuntWeeklyScoreKey
var v store.BigHuntWeeklyMaxScore
rows.Scan(&k.BigHuntWeeklyVersion, &k.AttributeType, &v.MaxScore, &v.LatestVersion)
u.BigHuntWeeklyMaxScores[k] = v
})
queryRows(db, `SELECT big_hunt_weekly_version, is_received_weekly_reward, latest_version
FROM user_big_hunt_weekly_statuses WHERE user_id=?`, uid, func(rows *sql.Rows) {
var ver int64
var ir int
var lv int64
rows.Scan(&ver, &ir, &lv)
u.BigHuntWeeklyStatuses[ver] = store.BigHuntWeeklyStatus{IsReceivedWeeklyReward: ir != 0, LatestVersion: lv}
})
}
func queryRows(db *sql.DB, query string, uid int64, scan func(*sql.Rows)) {
rows, err := db.Query(query, uid)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
scan(rows)
}
}
func loadSimpleMap(db *sql.DB, uid int64, query string, m map[int32]int32) {
queryRows(db, query, uid, func(rows *sql.Rows) {
var k, v int32
rows.Scan(&k, &v)
m[k] = v
})
}
func loadTimestampMap(db *sql.DB, uid int64, query string, m map[int32]int64) {
queryRows(db, query, uid, func(rows *sql.Rows) {
var k int32
var v int64
rows.Scan(&k, &v)
m[k] = v
})
}
File diff suppressed because it is too large Load Diff
+56
View File
@@ -0,0 +1,56 @@
package sqlite
import (
"fmt"
"time"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) CreateSession(uuid string, ttl time.Duration) (store.SessionState, error) {
var userId int64
err := s.db.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&userId)
if err != nil {
return store.SessionState{}, store.ErrNotFound
}
now := s.clock()
sessionKey := fmt.Sprintf("session_%d_%d", userId, now.UnixNano())
expireAt := now.Add(ttl)
_, err = s.db.Exec(
`INSERT INTO sessions (session_key, user_id, uuid, expire_at) VALUES (?, ?, ?, ?)`,
sessionKey, userId, uuid, expireAt.Format(time.RFC3339Nano),
)
if err != nil {
return store.SessionState{}, fmt.Errorf("insert session: %w", err)
}
return store.SessionState{
SessionKey: sessionKey,
UserId: userId,
Uuid: uuid,
ExpireAt: expireAt,
}, nil
}
func (s *SQLiteStore) ResolveUserId(sessionKey string) (int64, error) {
var userId int64
var expireStr string
err := s.db.QueryRow(
`SELECT user_id, expire_at FROM sessions WHERE session_key = ?`, sessionKey,
).Scan(&userId, &expireStr)
if err != nil {
return 0, store.ErrNotFound
}
expireAt, err := time.Parse(time.RFC3339Nano, expireStr)
if err != nil {
return 0, store.ErrNotFound
}
if s.clock().After(expireAt) {
return 0, store.ErrNotFound
}
return userId, nil
}
+25
View File
@@ -0,0 +1,25 @@
package sqlite
import (
"database/sql"
"time"
"lunar-tear/server/internal/store"
)
type SQLiteStore struct {
db *sql.DB
clock store.Clock
}
var (
_ store.UserRepository = (*SQLiteStore)(nil)
_ store.SessionRepository = (*SQLiteStore)(nil)
)
func New(db *sql.DB, clock store.Clock) *SQLiteStore {
if clock == nil {
clock = time.Now
}
return &SQLiteStore{db: db, clock: clock}
}
+218
View File
@@ -0,0 +1,218 @@
package sqlite
import (
"database/sql"
"fmt"
"lunar-tear/server/internal/store"
)
func (s *SQLiteStore) CreateUser(uuid string) (int64, error) {
tx, err := s.db.Begin()
if err != nil {
return 0, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
var existingId int64
err = tx.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&existingId)
if err == nil {
return existingId, nil
}
nowMillis := s.clock().UnixMilli()
res, err := tx.Exec(`INSERT INTO users (uuid, player_id, os_type, platform_type, user_restriction_type,
register_datetime, game_start_datetime, latest_version, birth_year, birth_month,
backup_token, charge_money_this_month) VALUES (?, 0, 2, 2, 0, ?, ?, 0, 2000, 1, 'mock-backup-token', 0)`,
uuid, nowMillis, nowMillis)
if err != nil {
return 0, fmt.Errorf("insert user: %w", err)
}
userId, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("last insert id: %w", err)
}
// player_id = user_id
if _, err := tx.Exec(`UPDATE users SET player_id = ? WHERE user_id = ?`, userId, userId); err != nil {
return 0, fmt.Errorf("update player_id: %w", err)
}
user := store.SeedUserState(userId, uuid, nowMillis)
if err := writeUserState(tx, userId, user); err != nil {
return 0, fmt.Errorf("write seed state: %w", err)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("commit: %w", err)
}
return userId, nil
}
func (s *SQLiteStore) GetUserByUUID(uuid string) (int64, error) {
var userId int64
err := s.db.QueryRow(`SELECT user_id FROM users WHERE uuid = ?`, uuid).Scan(&userId)
if err == sql.ErrNoRows {
return 0, store.ErrNotFound
}
if err != nil {
return 0, fmt.Errorf("query user: %w", err)
}
return userId, nil
}
func (s *SQLiteStore) DefaultUserId() (int64, error) {
var userId int64
err := s.db.QueryRow(`SELECT min(user_id) FROM users`).Scan(&userId)
if err != nil || userId == 0 {
return 0, store.ErrNotFound
}
return userId, nil
}
// ImportUser replaces all data for u.UserId in the database with the
// contents of u. Any pre-existing rows for that user are deleted first.
func (s *SQLiteStore) ImportUser(u *store.UserState) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
uid := u.UserId
// Child tables in reverse-dependency order (matches schema's goose Down).
childTables := []string{
"user_cage_ornament_rewards",
"user_shop_replaceable_lineup",
"user_shop_items",
"user_gacha_banner_box_drew_counts",
"user_gacha_banners",
"user_gacha_converted_medals",
"user_gifts",
"user_dokan_confirmed",
"user_drawn_omikuji",
"user_contents_stories",
"user_viewed_movies",
"user_navi_cutin_played",
"user_auto_sale_settings",
"user_explore_scores",
"user_tutorials",
"user_premium_items",
"user_important_items",
"user_materials",
"user_consumable_items",
"user_gimmick_unlocks",
"user_gimmick_sequences",
"user_gimmick_ornament_progress",
"user_gimmick_progress",
"user_big_hunt_weekly_statuses",
"user_big_hunt_weekly_max_scores",
"user_big_hunt_schedule_max_scores",
"user_big_hunt_statuses",
"user_big_hunt_max_scores",
"user_quest_limit_content_status",
"user_side_story_quests",
"user_missions",
"user_quest_missions",
"user_quests",
"user_deck_type_notes",
"user_deck_parts",
"user_deck_sub_weapons",
"user_decks",
"user_deck_characters",
"user_parts_presets",
"user_parts_group_notes",
"user_parts",
"user_thoughts",
"user_companions",
"user_weapon_notes",
"user_weapon_stories",
"user_weapon_awakens",
"user_weapon_abilities",
"user_weapon_skills",
"user_weapons",
"user_costume_awaken_status_ups",
"user_costume_active_skills",
"user_costumes",
"user_character_rebirths",
"user_character_board_status_ups",
"user_character_board_abilities",
"user_character_boards",
"user_characters",
"user_gacha",
"user_shop_replaceable",
"user_explore",
"user_guerrilla_free_open",
"user_portal_cage",
"user_notification",
"user_battle",
"user_big_hunt_state",
"user_side_story_active",
"user_extra_quest",
"user_event_quest",
"user_main_quest",
"user_login_bonus",
"user_login",
"user_profile",
"user_gem",
"user_status",
"user_setting",
"sessions",
}
for _, t := range childTables {
if _, err := tx.Exec(fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, t), uid); err != nil {
return fmt.Errorf("delete from %s: %w", t, err)
}
}
if _, err := tx.Exec(`DELETE FROM users WHERE user_id = ?`, uid); err != nil {
return fmt.Errorf("delete user: %w", err)
}
if _, err := tx.Exec(`INSERT INTO users (user_id, uuid, player_id, os_type, platform_type,
user_restriction_type, register_datetime, game_start_datetime, latest_version,
birth_year, birth_month, backup_token, charge_money_this_month)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
uid, u.Uuid, u.PlayerId, u.OsType, u.PlatformType, u.UserRestrictionType,
u.RegisterDatetime, u.GameStartDatetime, u.LatestVersion,
u.BirthYear, u.BirthMonth, u.BackupToken, u.ChargeMoneyThisMonth); err != nil {
return fmt.Errorf("insert user: %w", err)
}
if err := writeUserState(tx, uid, u); err != nil {
return fmt.Errorf("write user state: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit: %w", err)
}
return nil
}
func (s *SQLiteStore) UpdateUser(userId int64, mutate func(*store.UserState)) (store.UserState, error) {
before, err := s.LoadUser(userId)
if err != nil {
return store.UserState{}, err
}
after := store.CloneUserState(before)
mutate(&after)
tx, err := s.db.Begin()
if err != nil {
return store.UserState{}, fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback()
if err := diffAndSave(tx, userId, &before, &after); err != nil {
return store.UserState{}, fmt.Errorf("diff and save: %w", err)
}
if err := tx.Commit(); err != nil {
return store.UserState{}, fmt.Errorf("commit: %w", err)
}
return after, nil
}
+4 -8
View File
@@ -10,18 +10,14 @@ var ErrNotFound = errors.New("store: not found")
type Clock func() time.Time
type UserRepository interface {
EnsureUser(uuid string) (UserState, error)
SnapshotUser(userId int64) (UserState, error)
CreateUser(uuid string) (int64, error)
GetUserByUUID(uuid string) (int64, error)
LoadUser(userId int64) (UserState, error)
UpdateUser(userId int64, mutate func(*UserState)) (UserState, error)
DefaultUserId() (int64, error)
}
type SessionRepository interface {
CreateSession(uuid string, ttl time.Duration) (UserState, SessionState, error)
CreateSession(uuid string, ttl time.Duration) (SessionState, error)
ResolveUserId(sessionKey string) (int64, error)
}
type GachaRepository interface {
SnapshotCatalog() ([]GachaCatalogEntry, error)
ReplaceCatalog(entries []GachaCatalogEntry) error
}
+59 -12
View File
@@ -107,9 +107,11 @@ type UserState struct {
CharacterBoardAbilities map[CharacterBoardAbilityKey]CharacterBoardAbilityState
CharacterBoardStatusUps map[CharacterBoardStatusUpKey]CharacterBoardStatusUpState
CostumeAwakenStatusUps map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState
AutoSaleSettings map[int32]AutoSaleSettingState
CharacterRebirths map[int32]CharacterRebirthState
CostumeAwakenStatusUps map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState
CostumeLotteryEffects map[CostumeLotteryEffectKey]CostumeLotteryEffectState
CostumeLotteryEffectPending map[string]CostumeLotteryEffectPendingState // key: userCostumeUuid
AutoSaleSettings map[int32]AutoSaleSettingState
CharacterRebirths map[int32]CharacterRebirthState
}
func (u *UserState) EnsureMaps() {
@@ -254,6 +256,12 @@ func (u *UserState) EnsureMaps() {
if u.CostumeAwakenStatusUps == nil {
u.CostumeAwakenStatusUps = make(map[CostumeAwakenStatusKey]CostumeAwakenStatusUpState)
}
if u.CostumeLotteryEffects == nil {
u.CostumeLotteryEffects = make(map[CostumeLotteryEffectKey]CostumeLotteryEffectState)
}
if u.CostumeLotteryEffectPending == nil {
u.CostumeLotteryEffectPending = make(map[string]CostumeLotteryEffectPendingState)
}
if u.AutoSaleSettings == nil {
u.AutoSaleSettings = make(map[int32]AutoSaleSettingState)
}
@@ -358,15 +366,16 @@ type CharacterState struct {
}
type CostumeState struct {
UserCostumeUuid string
CostumeId int32
LimitBreakCount int32
Level int32
Exp int32
HeadupDisplayViewId int32
AcquisitionDatetime int64
AwakenCount int32
LatestVersion int64
UserCostumeUuid string
CostumeId int32
LimitBreakCount int32
Level int32
Exp int32
HeadupDisplayViewId int32
AcquisitionDatetime int64
AwakenCount int32
CostumeLotteryEffectUnlockedSlotCount int32
LatestVersion int64
}
type WeaponState struct {
@@ -1070,3 +1079,41 @@ type CharacterRebirthState struct {
RebirthCount int32
LatestVersion int64
}
type CostumeLotteryEffectKey struct {
UserCostumeUuid string
SlotNumber int32
}
func (k CostumeLotteryEffectKey) MarshalText() ([]byte, error) {
return fmt.Appendf(nil, "%s:%d", k.UserCostumeUuid, k.SlotNumber), nil
}
func (k *CostumeLotteryEffectKey) UnmarshalText(text []byte) error {
s := string(text)
idx := strings.LastIndex(s, ":")
if idx < 0 {
return fmt.Errorf("invalid CostumeLotteryEffectKey: %s", text)
}
k.UserCostumeUuid = s[:idx]
v, err := strconv.ParseInt(s[idx+1:], 10, 32)
if err != nil {
return err
}
k.SlotNumber = int32(v)
return nil
}
type CostumeLotteryEffectState struct {
UserCostumeUuid string
SlotNumber int32
OddsNumber int32
LatestVersion int64
}
type CostumeLotteryEffectPendingState struct {
UserCostumeUuid string
SlotNumber int32
OddsNumber int32
LatestVersion int64
}
+60 -12
View File
@@ -105,12 +105,18 @@ func init() {
s, _ := encodeJSONMaps(SortedWeaponAwakenRecords(user)...)
return s
})
register("IUserCostumeLotteryEffect", func(user store.UserState) string {
s, _ := encodeJSONMaps(sortedCostumeLotteryEffectRecords(user)...)
return s
})
register("IUserCostumeLotteryEffectPending", func(user store.UserState) string {
s, _ := encodeJSONMaps(SortedCostumeLotteryEffectPendingRecords(user)...)
return s
})
registerStatic(
"IUserCostumeLevelBonusReleaseStatus",
"IUserCostumeLotteryEffect",
"IUserCostumeLotteryEffectAbility",
"IUserCostumeLotteryEffectStatusUp",
"IUserCostumeLotteryEffectPending",
"IUserPartsPresetTag",
"IUserPartsStatusSub",
)
@@ -143,16 +149,17 @@ func sortedCostumeRecords(user store.UserState) []map[string]any {
for _, key := range keys {
row := user.Costumes[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"costumeId": row.CostumeId,
"limitBreakCount": row.LimitBreakCount,
"level": row.Level,
"exp": row.Exp,
"headupDisplayViewId": row.HeadupDisplayViewId,
"acquisitionDatetime": row.AcquisitionDatetime,
"awakenCount": row.AwakenCount,
"latestVersion": row.LatestVersion,
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"costumeId": row.CostumeId,
"limitBreakCount": row.LimitBreakCount,
"level": row.Level,
"exp": row.Exp,
"headupDisplayViewId": row.HeadupDisplayViewId,
"acquisitionDatetime": row.AcquisitionDatetime,
"awakenCount": row.AwakenCount,
"costumeLotteryEffectUnlockedSlotCount": row.CostumeLotteryEffectUnlockedSlotCount,
"latestVersion": row.LatestVersion,
})
}
return records
@@ -619,3 +626,44 @@ func sortedCageOrnamentRewardRecords(user store.UserState) []map[string]any {
}
return records
}
func SortedCostumeLotteryEffectPendingRecords(user store.UserState) []map[string]any {
keys := sortedStringKeys(user.CostumeLotteryEffectPending)
records := make([]map[string]any, 0, len(keys))
for _, key := range keys {
row := user.CostumeLotteryEffectPending[key]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"slotNumber": row.SlotNumber,
"oddsNumber": row.OddsNumber,
"latestVersion": row.LatestVersion,
})
}
return records
}
func sortedCostumeLotteryEffectRecords(user store.UserState) []map[string]any {
keys := make([]store.CostumeLotteryEffectKey, 0, len(user.CostumeLotteryEffects))
for k := range user.CostumeLotteryEffects {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].UserCostumeUuid != keys[j].UserCostumeUuid {
return keys[i].UserCostumeUuid < keys[j].UserCostumeUuid
}
return keys[i].SlotNumber < keys[j].SlotNumber
})
records := make([]map[string]any, 0, len(keys))
for _, k := range keys {
row := user.CostumeLotteryEffects[k]
records = append(records, map[string]any{
"userId": user.UserId,
"userCostumeUuid": row.UserCostumeUuid,
"slotNumber": row.SlotNumber,
"oddsNumber": row.OddsNumber,
"latestVersion": row.LatestVersion,
})
}
return records
}
@@ -138,6 +138,14 @@ func SelectTables(all map[string]string, requested []string) map[string]string {
return selected
}
func ProjectTables(user store.UserState, requested []string) map[string]string {
result := make(map[string]string, len(requested))
for _, table := range requested {
result[table] = projectTable(table, user)
}
return result
}
func BuildDiffFromTables(tables map[string]string) map[string]*pb.DiffData {
diff := make(map[string]*pb.DiffData, len(tables))
for table, payload := range tables {