mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Add SQLite persistence, import-snapshot tool, and karma functionality
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user