mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Fix Main Quests replay and weapon awaken level cap
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled
Build and Push Docker images to Docker Hub / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -88,7 +88,7 @@ func registerServices(
|
|||||||
pubPort, _ := strconv.Atoi(pubPortStr)
|
pubPort, _ := strconv.Atoi(pubPortStr)
|
||||||
|
|
||||||
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder))
|
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder))
|
||||||
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL, noRegister))
|
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, holder, authURL, noRegister))
|
||||||
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
|
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
|
||||||
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
|
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
|
||||||
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
|
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
|
||||||
|
|||||||
@@ -12,6 +12,34 @@ type BattleDropInfo struct {
|
|||||||
BattleDropCategoryId int32
|
BattleDropCategoryId int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SeasonRoutePair pairs a main-quest season with one of its routes, used to
|
||||||
|
// reconstruct a player's progression history for IUserMainQuestSeasonRoute.
|
||||||
|
type SeasonRoutePair struct {
|
||||||
|
MainQuestSeasonId int32
|
||||||
|
MainQuestRouteId int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeasonRoutesUpToCurrent returns every (season, route) pair from
|
||||||
|
// OrderedSeasonRoutes whose ordering is <= the given (seasonId, routeId)
|
||||||
|
// pair, inclusive. Used at user-load time to backfill
|
||||||
|
// IUserMainQuestSeasonRoute history so the client can compute the next
|
||||||
|
// route correctly when the player advances past a chapter end. Returns
|
||||||
|
// nil if the given pair isn't found.
|
||||||
|
func (q *QuestCatalog) SeasonRoutesUpToCurrent(seasonId, routeId int32) []SeasonRoutePair {
|
||||||
|
if q == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]SeasonRoutePair, 0, len(q.OrderedSeasonRoutes))
|
||||||
|
for _, p := range q.OrderedSeasonRoutes {
|
||||||
|
out = append(out, p)
|
||||||
|
if p.MainQuestSeasonId == seasonId && p.MainQuestRouteId == routeId {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pair not found in masterdata — don't return a partial list.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type QuestCatalog struct {
|
type QuestCatalog struct {
|
||||||
SceneById map[int32]EntityMQuestScene
|
SceneById map[int32]EntityMQuestScene
|
||||||
MissionById map[int32]EntityMQuestMission
|
MissionById map[int32]EntityMQuestMission
|
||||||
@@ -34,6 +62,9 @@ type QuestCatalog struct {
|
|||||||
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
||||||
ChapterLastSceneByQuestId map[int32]int32
|
ChapterLastSceneByQuestId map[int32]int32
|
||||||
SeasonIdByRouteId map[int32]int32
|
SeasonIdByRouteId map[int32]int32
|
||||||
|
OrderedSeasonRoutes []SeasonRoutePair
|
||||||
|
QuestsWithDifficulty map[int32]bool // any questId referenced in m_quest_relation_main_flow
|
||||||
|
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||||
|
|
||||||
UserExpThresholds []int32
|
UserExpThresholds []int32
|
||||||
CharacterExpThresholds []int32
|
CharacterExpThresholds []int32
|
||||||
@@ -117,6 +148,22 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderedRoutes := make([]EntityMMainQuestRoute, len(routes))
|
||||||
|
copy(orderedRoutes, routes)
|
||||||
|
sort.Slice(orderedRoutes, func(i, j int) bool {
|
||||||
|
if orderedRoutes[i].MainQuestSeasonId != orderedRoutes[j].MainQuestSeasonId {
|
||||||
|
return orderedRoutes[i].MainQuestSeasonId < orderedRoutes[j].MainQuestSeasonId
|
||||||
|
}
|
||||||
|
return orderedRoutes[i].SortOrder < orderedRoutes[j].SortOrder
|
||||||
|
})
|
||||||
|
orderedSeasonRoutes := make([]SeasonRoutePair, 0, len(orderedRoutes))
|
||||||
|
for _, r := range orderedRoutes {
|
||||||
|
orderedSeasonRoutes = append(orderedSeasonRoutes, SeasonRoutePair{
|
||||||
|
MainQuestSeasonId: r.MainQuestSeasonId,
|
||||||
|
MainQuestRouteId: r.MainQuestRouteId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("load quest first clear reward switch table: %w", err)
|
return nil, fmt.Errorf("load quest first clear reward switch table: %w", err)
|
||||||
@@ -238,6 +285,30 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
|
return nil, fmt.Errorf("load tutorial unlock condition table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relations, err := utils.ReadTable[EntityMQuestRelationMainFlow]("m_quest_relation_main_flow")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load quest relation main flow table: %w", err)
|
||||||
|
}
|
||||||
|
questsWithDifficulty := make(map[int32]bool, len(relations)*3)
|
||||||
|
for _, r := range relations {
|
||||||
|
questsWithDifficulty[r.MainFlowQuestId] = true
|
||||||
|
if r.ReplayFlowQuestId != 0 {
|
||||||
|
questsWithDifficulty[r.ReplayFlowQuestId] = true
|
||||||
|
}
|
||||||
|
if r.SubFlowQuestId != 0 {
|
||||||
|
questsWithDifficulty[r.SubFlowQuestId] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
battleOnlyTargetSceneByQuestId := make(map[int32]int32)
|
||||||
|
for _, scene := range scenes {
|
||||||
|
if scene.IsBattleOnlyTarget {
|
||||||
|
if _, exists := battleOnlyTargetSceneByQuestId[scene.QuestId]; !exists {
|
||||||
|
battleOnlyTargetSceneByQuestId[scene.QuestId] = scene.QuestSceneId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
paramMapRows, err := LoadParameterMap()
|
paramMapRows, err := LoadParameterMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -529,6 +600,9 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
TutorialUnlockConditions: tutorialUnlockConds,
|
TutorialUnlockConditions: tutorialUnlockConds,
|
||||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||||
SeasonIdByRouteId: seasonIdByRouteId,
|
SeasonIdByRouteId: seasonIdByRouteId,
|
||||||
|
OrderedSeasonRoutes: orderedSeasonRoutes,
|
||||||
|
QuestsWithDifficulty: questsWithDifficulty,
|
||||||
|
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||||
|
|
||||||
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
|
||||||
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
CharacterExpThresholds: BuildExpThresholds(paramMapRows, 31),
|
||||||
@@ -545,3 +619,12 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
PartsCatalog: partsCatalog,
|
PartsCatalog: partsCatalog,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *QuestCatalog) BattleOnlyTargetSceneIdFor(questId int32) (int32, bool) {
|
||||||
|
v, ok := q.BattleOnlyTargetSceneByQuestId[questId]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QuestCatalog) QuestHasDifficulty(questId int32) bool {
|
||||||
|
return q.QuestsWithDifficulty[questId]
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,9 +53,11 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.EventQuest.CurrentEventQuestChapterId = 0
|
||||||
user.EventQuest.CurrentQuestId = 0
|
user.EventQuest.CurrentQuestId = 0
|
||||||
user.EventQuest.CurrentQuestSceneId = 0
|
user.EventQuest.CurrentQuestSceneId = 0
|
||||||
user.EventQuest.HeadQuestSceneId = 0
|
user.EventQuest.HeadQuestSceneId = 0
|
||||||
|
user.EventQuest.LatestVersion = nowMillis
|
||||||
|
|
||||||
h.clearQuestMissions(user, questId, nowMillis)
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
|
|
||||||
|
|||||||
@@ -38,49 +38,62 @@ func (h *QuestHandler) clearQuestMissions(user *store.UserState, questId int32,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestStart(user *store.UserState, questId int32, isBattleOnly, isMainFlow bool, userDeckNumber int32, nowMillis int64) {
|
||||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, false, nowMillis)
|
h.handleQuestStartInternal(user, questId, isBattleOnly, isMainFlow, userDeckNumber, false, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestStartReplay(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, nowMillis int64) {
|
||||||
h.handleQuestStartInternal(user, questId, isBattleOnly, userDeckNumber, true, nowMillis)
|
h.handleQuestStartInternal(user, questId, isBattleOnly, false, userDeckNumber, true, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
|
func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId int32, isBattleOnly, isMainFlow bool, userDeckNumber int32, isReplayFlow bool, nowMillis int64) {
|
||||||
quest, ok := h.QuestById[questId]
|
quest, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestStart", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
if quest.Stamina > 0 {
|
if quest.Stamina > 0 {
|
||||||
maxMillis := h.MaxStaminaByLevel[user.Status.Level] * 1000
|
store.ConsumeStamina(user, quest.Stamina, h.MaxStaminaByLevel[user.Status.Level]*1000, nowMillis)
|
||||||
store.ConsumeStamina(user, quest.Stamina, maxMillis, nowMillis)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
if questState.QuestStateType == model.UserQuestStateTypeCleared {
|
questState.IsBattleOnly = isBattleOnly
|
||||||
if isReplayFlow {
|
questState.UserDeckNumber = userDeckNumber
|
||||||
user.MainQuest.SavedCurrentQuestSceneId = user.MainQuest.CurrentQuestSceneId
|
|
||||||
user.MainQuest.SavedHeadQuestSceneId = user.MainQuest.HeadQuestSceneId
|
isCleared := questState.QuestStateType == model.UserQuestStateTypeCleared
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
|
isMenuPick := !isReplayFlow && !isMainFlow && (isCleared || h.QuestHasDifficulty(questId))
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
switch {
|
||||||
user.MainQuest.LatestVersion = nowMillis
|
case isMenuPick:
|
||||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
snapshotMainQuestIfNeeded(user)
|
||||||
questState.LatestStartDatetime = nowMillis
|
sceneId := h.menuPickSceneId(questId, isBattleOnly)
|
||||||
questState.IsBattleOnly = isBattleOnly
|
user.MainQuest.ProgressQuestSceneId = sceneId
|
||||||
questState.UserDeckNumber = userDeckNumber
|
user.MainQuest.ProgressHeadQuestSceneId = sceneId
|
||||||
user.Quests[questId] = questState
|
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
log.Printf("[HandleQuestStart] replay flow started for quest %d, saved scene=%d head=%d",
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
questId, user.MainQuest.SavedCurrentQuestSceneId, user.MainQuest.SavedHeadQuestSceneId)
|
user.PortalCageStatus.LatestVersion = nowMillis
|
||||||
}
|
user.SideStoryActiveProgress = store.SideStoryActiveProgress{LatestVersion: nowMillis}
|
||||||
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
|
log.Printf("[HandleQuestStart] QuestMenuPick quest=%d isBattleOnly=%v scene=%d cleared=%v",
|
||||||
|
questId, isBattleOnly, sceneId, isCleared)
|
||||||
|
|
||||||
|
case isCleared && isReplayFlow:
|
||||||
|
snapshotMainQuestIfNeeded(user)
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
|
||||||
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
||||||
|
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
||||||
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
|
log.Printf("[HandleQuestStart] MapPlay quest=%d isBattleOnly=%v", questId, isBattleOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCleared {
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||||
|
questState.LatestStartDatetime = nowMillis
|
||||||
|
user.Quests[questId] = questState
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
questState.IsBattleOnly = isBattleOnly
|
|
||||||
questState.UserDeckNumber = userDeckNumber
|
|
||||||
if isMainQuestPlayable(quest) {
|
if isMainQuestPlayable(quest) {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||||
@@ -90,7 +103,6 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
questState.ClearCount = 1
|
questState.ClearCount = 1
|
||||||
questState.DailyClearCount = 1
|
questState.DailyClearCount = 1
|
||||||
questState.LastClearDatetime = nowMillis
|
questState.LastClearDatetime = nowMillis
|
||||||
|
|
||||||
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
if sceneIds := h.SceneIdsByQuestId[questId]; len(sceneIds) > 0 {
|
||||||
firstSceneId := sceneIds[0]
|
firstSceneId := sceneIds[0]
|
||||||
prevSceneId := user.MainQuest.CurrentQuestSceneId
|
prevSceneId := user.MainQuest.CurrentQuestSceneId
|
||||||
@@ -102,6 +114,33 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
|||||||
user.Quests[questId] = questState
|
user.Quests[questId] = questState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func snapshotMainQuestIfNeeded(user *store.UserState) {
|
||||||
|
if user.MainQuest.SavedContext.Active {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.MainQuest.SavedContext = store.SavedQuestContext{
|
||||||
|
Active: true,
|
||||||
|
CurrentQuestSceneId: user.MainQuest.CurrentQuestSceneId,
|
||||||
|
HeadQuestSceneId: user.MainQuest.HeadQuestSceneId,
|
||||||
|
CurrentMainQuestRouteId: user.MainQuest.CurrentMainQuestRouteId,
|
||||||
|
MainQuestSeasonId: user.MainQuest.MainQuestSeasonId,
|
||||||
|
IsReachedLastQuestScene: user.MainQuest.IsReachedLastQuestScene,
|
||||||
|
PortalCageInProgress: user.PortalCageStatus.IsCurrentProgress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
||||||
|
if isBattleOnly {
|
||||||
|
if v, ok := h.BattleOnlyTargetSceneIdFor(questId); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scenes := h.SceneIdsByQuestId[questId]; len(scenes) > 0 {
|
||||||
|
return scenes[0]
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome *FinishOutcome, nowMillis int64) {
|
func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, outcome *FinishOutcome, nowMillis int64) {
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
if !questState.IsRewardGranted {
|
if !questState.IsRewardGranted {
|
||||||
@@ -122,22 +161,49 @@ func (h *QuestHandler) applyQuestVictory(user *store.UserState, questId int32, o
|
|||||||
questState.ClearCount++
|
questState.ClearCount++
|
||||||
questState.DailyClearCount++
|
questState.DailyClearCount++
|
||||||
questState.LastClearDatetime = nowMillis
|
questState.LastClearDatetime = nowMillis
|
||||||
|
questState.IsBattleOnly = false
|
||||||
user.Quests[questId] = questState
|
user.Quests[questId] = questState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *QuestHandler) finalizeChainPreviousQuest(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
|
if _, ok := h.QuestById[questId]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.initQuestState(user, questId)
|
||||||
|
questState := user.Quests[questId]
|
||||||
|
if questState.QuestStateType == model.UserQuestStateTypeCleared {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !questState.IsRewardGranted {
|
||||||
|
h.applyQuestRewards(user, questId, nowMillis)
|
||||||
|
questState.IsRewardGranted = true
|
||||||
|
}
|
||||||
|
questState.QuestStateType = model.UserQuestStateTypeCleared
|
||||||
|
questState.ClearCount++
|
||||||
|
questState.DailyClearCount++
|
||||||
|
questState.LastClearDatetime = nowMillis
|
||||||
|
questState.IsBattleOnly = false
|
||||||
|
user.Quests[questId] = questState
|
||||||
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
|
log.Printf("[HandleMainQuestSceneProgress] finalized chain-previous quest %d (cleared)", questId)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, isRetired, isAnnihilated bool, nowMillis int64) FinishOutcome {
|
||||||
quest, ok := h.QuestById[questId]
|
quest, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleQuestFinish", questId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
|
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
|
||||||
|
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||||
|
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
||||||
|
|
||||||
if isMainQuestPlayable(quest) && !wasReplay {
|
if isMainQuestPlayable(quest) && !wasReplay && !wasMenuReplay {
|
||||||
lastSceneId := h.getLastMainFlowSceneId(questId)
|
lastSceneId := h.getLastMainFlowSceneId(questId)
|
||||||
h.advanceMainFlowScene(user, questId, lastSceneId)
|
h.advanceMainFlowScene(user, questId, lastSceneId)
|
||||||
}
|
}
|
||||||
@@ -149,24 +215,43 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
store.RecoverStamina(user, refund*1000, maxMillis, nowMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On retire of a previously-cleared quest (cage Menu Pick replay or
|
||||||
|
// Map Play replay), HandleQuestStart marked QuestStateType=Active for
|
||||||
|
// the run. With applyQuestVictory skipped on retire, that Active sticks
|
||||||
|
// and the cage UI shows the quest as locked. Restore Cleared.
|
||||||
|
if isRetired {
|
||||||
|
qs := user.Quests[questId]
|
||||||
|
if qs.ClearCount > 0 && qs.QuestStateType == model.UserQuestStateTypeActive {
|
||||||
|
qs.QuestStateType = model.UserQuestStateTypeCleared
|
||||||
|
user.Quests[questId] = qs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.MainQuest.ProgressQuestSceneId = 0
|
user.MainQuest.ProgressQuestSceneId = 0
|
||||||
user.MainQuest.ProgressHeadQuestSceneId = 0
|
user.MainQuest.ProgressHeadQuestSceneId = 0
|
||||||
user.MainQuest.ProgressQuestFlowType = 0
|
user.MainQuest.ProgressQuestFlowType = 0
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
|
|
||||||
|
if wasMenuReplay {
|
||||||
|
ctx := user.MainQuest.SavedContext
|
||||||
|
user.MainQuest.CurrentQuestSceneId = ctx.CurrentQuestSceneId
|
||||||
|
user.MainQuest.HeadQuestSceneId = ctx.HeadQuestSceneId
|
||||||
|
user.MainQuest.CurrentMainQuestRouteId = ctx.CurrentMainQuestRouteId
|
||||||
|
user.MainQuest.MainQuestSeasonId = ctx.MainQuestSeasonId
|
||||||
|
user.MainQuest.IsReachedLastQuestScene = ctx.IsReachedLastQuestScene
|
||||||
|
user.PortalCageStatus.IsCurrentProgress = ctx.PortalCageInProgress
|
||||||
|
user.PortalCageStatus.LatestVersion = nowMillis
|
||||||
|
user.MainQuest.SavedContext = store.SavedQuestContext{}
|
||||||
|
user.MainQuest.LatestVersion = nowMillis
|
||||||
|
log.Printf("[HandleQuestFinish] restored snapshot for quest %d (route=%d season=%d scene=%d head=%d cage=%v)",
|
||||||
|
questId, ctx.CurrentMainQuestRouteId, ctx.MainQuestSeasonId,
|
||||||
|
ctx.CurrentQuestSceneId, ctx.HeadQuestSceneId, ctx.PortalCageInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
if wasReplay {
|
if wasReplay {
|
||||||
if user.MainQuest.SavedCurrentQuestSceneId > 0 {
|
|
||||||
user.MainQuest.CurrentQuestSceneId = user.MainQuest.SavedCurrentQuestSceneId
|
|
||||||
}
|
|
||||||
if user.MainQuest.SavedHeadQuestSceneId > 0 {
|
|
||||||
user.MainQuest.HeadQuestSceneId = user.MainQuest.SavedHeadQuestSceneId
|
|
||||||
}
|
|
||||||
user.MainQuest.SavedCurrentQuestSceneId = 0
|
|
||||||
user.MainQuest.SavedHeadQuestSceneId = 0
|
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
||||||
log.Printf("[HandleQuestFinish] replay flow ended for quest %d, restored scene=%d head=%d",
|
log.Printf("[HandleQuestFinish] replay flow ended for quest %d", questId)
|
||||||
questId, user.MainQuest.CurrentQuestSceneId, user.MainQuest.HeadQuestSceneId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h.clearQuestMissions(user, questId, nowMillis)
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
@@ -215,14 +300,16 @@ func (h *QuestHandler) HandleQuestSkip(user *store.UserState, questId, skipCount
|
|||||||
|
|
||||||
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleQuestRestart(user *store.UserState, questId int32, nowMillis int64) {
|
||||||
questDef, ok := h.QuestById[questId]
|
questDef, ok := h.QuestById[questId]
|
||||||
if ok && isMainQuestPlayable(questDef) {
|
// Only seed CurrentQuestFlowType when it's not already set (initial
|
||||||
|
// natural progression). Don't clobber an in-flight ReplayFlow (Map Play
|
||||||
|
// resume).
|
||||||
|
if ok && isMainQuestPlayable(questDef) && user.MainQuest.CurrentQuestFlowType == 0 {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||||
}
|
}
|
||||||
|
|
||||||
quest := user.Quests[questId]
|
quest := user.Quests[questId]
|
||||||
quest.QuestId = questId
|
quest.QuestId = questId
|
||||||
quest.QuestStateType = model.UserQuestStateTypeActive
|
quest.QuestStateType = model.UserQuestStateTypeActive
|
||||||
quest.IsBattleOnly = false
|
|
||||||
quest.LatestStartDatetime = nowMillis
|
quest.LatestStartDatetime = nowMillis
|
||||||
user.Quests[questId] = quest
|
user.Quests[questId] = quest
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ func (h *QuestHandler) isSceneAhead(newSceneId, currentHeadId int32) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, sceneId int32) {
|
func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, sceneId int32) {
|
||||||
user.MainQuest.CurrentQuestSceneId = sceneId
|
if !h.isSceneAhead(sceneId, user.MainQuest.HeadQuestSceneId) {
|
||||||
if h.isSceneAhead(sceneId, user.MainQuest.HeadQuestSceneId) {
|
return
|
||||||
user.MainQuest.HeadQuestSceneId = sceneId
|
|
||||||
}
|
}
|
||||||
|
user.MainQuest.CurrentQuestSceneId = sceneId
|
||||||
|
user.MainQuest.HeadQuestSceneId = sceneId
|
||||||
|
|
||||||
lastSceneId := h.getChapterLastSceneId(questId)
|
lastSceneId := h.getChapterLastSceneId(questId)
|
||||||
user.MainQuest.IsReachedLastQuestScene = sceneId == lastSceneId
|
user.MainQuest.IsReachedLastQuestScene = sceneId == lastSceneId
|
||||||
@@ -39,10 +40,33 @@ func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, scen
|
|||||||
user.MainQuest.CurrentMainQuestRouteId = routeId
|
user.MainQuest.CurrentMainQuestRouteId = routeId
|
||||||
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
|
if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok {
|
||||||
user.MainQuest.MainQuestSeasonId = seasonId
|
user.MainQuest.MainQuestSeasonId = seasonId
|
||||||
|
RecordSeasonRoute(user, seasonId, routeId, gametime.NowMillis())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RecordSeasonRoute upserts the (season, route) pair into the player's history,
|
||||||
|
// bumping LatestVersion on first insert. The history backs IUserMainQuestSeasonRoute,
|
||||||
|
// which the client uses to know which chapters' scene metadata to load (so cage
|
||||||
|
// menu-replay can transition to quests from older chapters without crashing).
|
||||||
|
func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) {
|
||||||
|
if seasonId <= 0 || routeId <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if user.MainQuestSeasonRoutes == nil {
|
||||||
|
user.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry)
|
||||||
|
}
|
||||||
|
key := store.SeasonRouteKey{MainQuestSeasonId: seasonId, MainQuestRouteId: routeId}
|
||||||
|
if _, exists := user.MainQuestSeasonRoutes[key]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.MainQuestSeasonRoutes[key] = store.SeasonRouteEntry{
|
||||||
|
MainQuestSeasonId: seasonId,
|
||||||
|
MainQuestRouteId: routeId,
|
||||||
|
LatestVersion: nowMillis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
scene, ok := h.SceneById[questSceneId]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -127,6 +151,12 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
|
panic(fmt.Sprintf("unknown questId=%d for HandleMainQuestSceneProgress", questSceneId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prevSceneId := user.MainQuest.ProgressQuestSceneId; prevSceneId != 0 {
|
||||||
|
if prevScene, ok := h.SceneById[prevSceneId]; ok && prevScene.QuestId != quest.QuestId {
|
||||||
|
h.finalizeChainPreviousQuest(user, prevScene.QuestId, gametime.NowMillis())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isMainQuestPlayable(quest) {
|
if isMainQuestPlayable(quest) {
|
||||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
||||||
nowMillis := gametime.NowMillis()
|
nowMillis := gametime.NowMillis()
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (s *QuestServiceServer) StartMainQuest(ctx context.Context, req *pb.StartMa
|
|||||||
if req.IsReplayFlow {
|
if req.IsReplayFlow {
|
||||||
engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
engine.HandleQuestStartReplay(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
||||||
} else {
|
} else {
|
||||||
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.UserDeckNumber, nowMillis)
|
engine.HandleQuestStart(user, req.QuestId, req.IsBattleOnly, req.IsMainFlow, req.UserDeckNumber, nowMillis)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -198,6 +198,7 @@ func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteReque
|
|||||||
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId
|
||||||
if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok {
|
||||||
user.MainQuest.MainQuestSeasonId = seasonId
|
user.MainQuest.MainQuestSeasonId = seasonId
|
||||||
|
questflow.RecordSeasonRoute(user, seasonId, req.MainQuestRouteId, gametime.NowMillis())
|
||||||
}
|
}
|
||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
user.PortalCageStatus.IsCurrentProgress = false
|
user.PortalCageStatus.IsCurrentProgress = false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import (
|
|||||||
pb "lunar-tear/server/gen/proto"
|
pb "lunar-tear/server/gen/proto"
|
||||||
"lunar-tear/server/internal/gametime"
|
"lunar-tear/server/internal/gametime"
|
||||||
"lunar-tear/server/internal/model"
|
"lunar-tear/server/internal/model"
|
||||||
|
"lunar-tear/server/internal/questflow"
|
||||||
|
"lunar-tear/server/internal/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,15 +28,16 @@ type UserServiceServer struct {
|
|||||||
pb.UnimplementedUserServiceServer
|
pb.UnimplementedUserServiceServer
|
||||||
users store.UserRepository
|
users store.UserRepository
|
||||||
sessions store.SessionRepository
|
sessions store.SessionRepository
|
||||||
|
holder *runtime.Holder
|
||||||
authURL string
|
authURL string
|
||||||
noRegister bool
|
noRegister bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, authURL string, noRegister bool) *UserServiceServer {
|
func NewUserServiceServer(users store.UserRepository, sessions store.SessionRepository, holder *runtime.Holder, authURL string, noRegister bool) *UserServiceServer {
|
||||||
if authURL != "" && !strings.Contains(authURL, "://") {
|
if authURL != "" && !strings.Contains(authURL, "://") {
|
||||||
authURL = "http://" + authURL
|
authURL = "http://" + authURL
|
||||||
}
|
}
|
||||||
return &UserServiceServer{users: users, sessions: sessions, authURL: authURL, noRegister: noRegister}
|
return &UserServiceServer{users: users, sessions: sessions, holder: holder, authURL: authURL, noRegister: noRegister}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
|
func (s *UserServiceServer) RegisterUser(ctx context.Context, req *pb.RegisterUserRequest) (*pb.RegisterUserResponse, error) {
|
||||||
@@ -93,7 +96,24 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
|
|||||||
|
|
||||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
user.GameStartDatetime = gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
|
user.GameStartDatetime = now
|
||||||
|
|
||||||
|
// Backfill IUserMainQuestSeasonRoute history so the client can compute
|
||||||
|
// the next route after a chapter end. Idempotent: RecordSeasonRoute
|
||||||
|
// is a no-op for entries that already exist.
|
||||||
|
if catalog := s.holder.Get(); catalog != nil && catalog.QuestHandler != nil {
|
||||||
|
if user.MainQuest.MainQuestSeasonId > 0 && user.MainQuest.CurrentMainQuestRouteId > 0 {
|
||||||
|
before := len(user.MainQuestSeasonRoutes)
|
||||||
|
for _, p := range catalog.QuestHandler.SeasonRoutesUpToCurrent(user.MainQuest.MainQuestSeasonId, user.MainQuest.CurrentMainQuestRouteId) {
|
||||||
|
questflow.RecordSeasonRoute(user, p.MainQuestSeasonId, p.MainQuestRouteId, now)
|
||||||
|
}
|
||||||
|
if added := len(user.MainQuestSeasonRoutes) - before; added > 0 {
|
||||||
|
user.MainQuest.LatestVersion = now
|
||||||
|
log.Printf("[UserService] GameStart: backfilled %d MainQuestSeasonRoute entries", added)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return &pb.GameStartResponse{}, nil
|
return &pb.GameStartResponse{}, nil
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func (s *WeaponServiceServer) EnhanceByMaterial(ctx context.Context, req *pb.Enh
|
|||||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
cap := maxFunc.Evaluate(weapon.LimitBreakCount)
|
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
if weapon.Level > cap {
|
if weapon.Level > cap {
|
||||||
weapon.Level = cap
|
weapon.Level = cap
|
||||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||||
@@ -748,7 +748,7 @@ func (s *WeaponServiceServer) EnhanceByWeapon(ctx context.Context, req *pb.Enhan
|
|||||||
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
if thresholds, ok := catalog.ExpByEnhanceId[levelingEnhanceId]; ok {
|
||||||
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
weapon.Level, weapon.Exp = gameutil.LevelAndCap(weapon.Exp, thresholds)
|
||||||
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
if maxFunc, ok := catalog.MaxLevelByEnhanceId[wm.WeaponSpecificEnhanceId]; ok {
|
||||||
cap := maxFunc.Evaluate(weapon.LimitBreakCount)
|
cap := awakenedLevelCap(catalog, user, weapon, req.UserWeaponUuid, maxFunc.Evaluate(weapon.LimitBreakCount))
|
||||||
if weapon.Level > cap {
|
if weapon.Level > cap {
|
||||||
weapon.Level = cap
|
weapon.Level = cap
|
||||||
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
if int(cap) >= 0 && int(cap) < len(thresholds) {
|
||||||
@@ -878,3 +878,14 @@ func (s *WeaponServiceServer) Awaken(ctx context.Context, req *pb.WeaponAwakenRe
|
|||||||
|
|
||||||
return &pb.WeaponAwakenResponse{}, nil
|
return &pb.WeaponAwakenResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func awakenedLevelCap(catalog *masterdata.WeaponCatalog, user *store.UserState, weapon store.WeaponState, weaponUuid string, baseCap int32) int32 {
|
||||||
|
if _, awoken := user.WeaponAwakens[weaponUuid]; !awoken {
|
||||||
|
return baseCap
|
||||||
|
}
|
||||||
|
row, ok := catalog.AwakenByWeaponId[weapon.WeaponId]
|
||||||
|
if !ok {
|
||||||
|
return baseCap
|
||||||
|
}
|
||||||
|
return baseCap + row.LevelLimitUp
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ func initMaps(u *store.UserState) {
|
|||||||
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
|
u.CharacterRebirths = make(map[int32]store.CharacterRebirthState)
|
||||||
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
|
u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState)
|
||||||
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
|
u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress)
|
||||||
|
u.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry)
|
||||||
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
|
u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus)
|
||||||
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
|
u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore)
|
||||||
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
|
u.BigHuntStatuses = make(map[int32]store.BigHuntStatus)
|
||||||
@@ -124,17 +125,26 @@ func load1to1(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
|
Scan(&u.LoginBonus.LoginBonusId, &u.LoginBonus.CurrentPageNumber, &u.LoginBonus.CurrentStampNumber,
|
||||||
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
|
&u.LoginBonus.LatestRewardReceiveDatetime, &u.LoginBonus.LatestVersion)
|
||||||
|
|
||||||
|
var ctxActive, ctxIsLast, ctxCage int
|
||||||
_ = db.QueryRow(`SELECT current_quest_flow_type, current_main_quest_route_id, current_quest_scene_id,
|
_ = 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,
|
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,
|
progress_quest_flow_type, main_quest_season_id, latest_version,
|
||||||
saved_head_quest_scene_id, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
|
saved_ctx_active, saved_ctx_current_quest_scene_id, saved_ctx_head_quest_scene_id,
|
||||||
|
saved_ctx_current_main_quest_route_id, saved_ctx_main_quest_season_id,
|
||||||
|
saved_ctx_is_reached_last_quest_scene, saved_ctx_portal_cage_in_progress,
|
||||||
|
replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id
|
||||||
FROM user_main_quest WHERE user_id=?`, uid).
|
FROM user_main_quest WHERE user_id=?`, uid).
|
||||||
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
|
Scan(&u.MainQuest.CurrentQuestFlowType, &u.MainQuest.CurrentMainQuestRouteId, &u.MainQuest.CurrentQuestSceneId,
|
||||||
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
|
&u.MainQuest.HeadQuestSceneId, &b, &u.MainQuest.ProgressQuestSceneId, &u.MainQuest.ProgressHeadQuestSceneId,
|
||||||
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
|
&u.MainQuest.ProgressQuestFlowType, &u.MainQuest.MainQuestSeasonId, &u.MainQuest.LatestVersion,
|
||||||
&u.MainQuest.SavedCurrentQuestSceneId, &u.MainQuest.SavedHeadQuestSceneId,
|
&ctxActive, &u.MainQuest.SavedContext.CurrentQuestSceneId, &u.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
&u.MainQuest.SavedContext.CurrentMainQuestRouteId, &u.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
&ctxIsLast, &ctxCage,
|
||||||
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
|
&u.MainQuest.ReplayFlowCurrentQuestSceneId, &u.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||||
u.MainQuest.IsReachedLastQuestScene = b != 0
|
u.MainQuest.IsReachedLastQuestScene = b != 0
|
||||||
|
u.MainQuest.SavedContext.Active = ctxActive != 0
|
||||||
|
u.MainQuest.SavedContext.IsReachedLastQuestScene = ctxIsLast != 0
|
||||||
|
u.MainQuest.SavedContext.PortalCageInProgress = ctxCage != 0
|
||||||
|
|
||||||
_ = db.QueryRow(`SELECT current_event_quest_chapter_id, current_quest_id, current_quest_scene_id,
|
_ = 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).
|
head_quest_scene_id, latest_version FROM user_event_quest WHERE user_id=?`, uid).
|
||||||
@@ -346,6 +356,16 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryRows(db, `SELECT main_quest_season_id, main_quest_route_id, latest_version
|
||||||
|
FROM user_main_quest_season_routes WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
|
var seasonId, routeId int32
|
||||||
|
var lv int64
|
||||||
|
rows.Scan(&seasonId, &routeId, &lv)
|
||||||
|
u.MainQuestSeasonRoutes[store.SeasonRouteKey{MainQuestSeasonId: seasonId, MainQuestRouteId: routeId}] = store.SeasonRouteEntry{
|
||||||
|
MainQuestSeasonId: seasonId, MainQuestRouteId: routeId, LatestVersion: lv,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
queryRows(db, `SELECT limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version
|
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) {
|
FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var id int32
|
var id int32
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
u.LoginBonus.LatestRewardReceiveDatetime, u.LoginBonus.LatestVersion); err != nil {
|
u.LoginBonus.LatestRewardReceiveDatetime, u.LoginBonus.LatestVersion); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := exec(`INSERT INTO user_main_quest (user_id, 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) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_main_quest (user_id, 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_ctx_active, saved_ctx_current_quest_scene_id, saved_ctx_head_quest_scene_id, saved_ctx_current_main_quest_route_id, saved_ctx_main_quest_season_id, saved_ctx_is_reached_last_quest_scene, saved_ctx_portal_cage_in_progress, replay_flow_current_quest_scene_id, replay_flow_head_quest_scene_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
uid, u.MainQuest.CurrentQuestFlowType, u.MainQuest.CurrentMainQuestRouteId, u.MainQuest.CurrentQuestSceneId,
|
uid, u.MainQuest.CurrentQuestFlowType, u.MainQuest.CurrentMainQuestRouteId, u.MainQuest.CurrentQuestSceneId,
|
||||||
u.MainQuest.HeadQuestSceneId, boolToInt(u.MainQuest.IsReachedLastQuestScene), u.MainQuest.ProgressQuestSceneId,
|
u.MainQuest.HeadQuestSceneId, boolToInt(u.MainQuest.IsReachedLastQuestScene), u.MainQuest.ProgressQuestSceneId,
|
||||||
u.MainQuest.ProgressHeadQuestSceneId, u.MainQuest.ProgressQuestFlowType, u.MainQuest.MainQuestSeasonId,
|
u.MainQuest.ProgressHeadQuestSceneId, u.MainQuest.ProgressQuestFlowType, u.MainQuest.MainQuestSeasonId,
|
||||||
u.MainQuest.LatestVersion, u.MainQuest.SavedCurrentQuestSceneId, u.MainQuest.SavedHeadQuestSceneId,
|
u.MainQuest.LatestVersion,
|
||||||
|
boolToInt(u.MainQuest.SavedContext.Active),
|
||||||
|
u.MainQuest.SavedContext.CurrentQuestSceneId, u.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
u.MainQuest.SavedContext.CurrentMainQuestRouteId, u.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
boolToInt(u.MainQuest.SavedContext.IsReachedLastQuestScene),
|
||||||
|
boolToInt(u.MainQuest.SavedContext.PortalCageInProgress),
|
||||||
u.MainQuest.ReplayFlowCurrentQuestSceneId, u.MainQuest.ReplayFlowHeadQuestSceneId); err != nil {
|
u.MainQuest.ReplayFlowCurrentQuestSceneId, u.MainQuest.ReplayFlowHeadQuestSceneId); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -208,6 +213,12 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for k, v := range u.MainQuestSeasonRoutes {
|
||||||
|
if err := exec(`INSERT INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version) VALUES (?,?,?,?)`,
|
||||||
|
uid, k.MainQuestSeasonId, k.MainQuestRouteId, v.LatestVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for id, v := range u.QuestLimitContentStatus {
|
for id, v := range u.QuestLimitContentStatus {
|
||||||
if err := exec(`INSERT INTO user_quest_limit_content_status (user_id, limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version) VALUES (?,?,?,?,?)`,
|
if err := exec(`INSERT INTO user_quest_limit_content_status (user_id, limit_content_id, limit_content_quest_status_type, event_quest_chapter_id, latest_version) VALUES (?,?,?,?,?)`,
|
||||||
uid, id, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion); err != nil {
|
uid, id, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion); err != nil {
|
||||||
@@ -562,11 +573,16 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if before.MainQuest != after.MainQuest {
|
if before.MainQuest != after.MainQuest {
|
||||||
if err := exec(`UPDATE user_main_quest SET 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=? WHERE user_id=?`,
|
if err := exec(`UPDATE user_main_quest SET 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_ctx_active=?, saved_ctx_current_quest_scene_id=?, saved_ctx_head_quest_scene_id=?, saved_ctx_current_main_quest_route_id=?, saved_ctx_main_quest_season_id=?, saved_ctx_is_reached_last_quest_scene=?, saved_ctx_portal_cage_in_progress=?, replay_flow_current_quest_scene_id=?, replay_flow_head_quest_scene_id=? WHERE user_id=?`,
|
||||||
after.MainQuest.CurrentQuestFlowType, after.MainQuest.CurrentMainQuestRouteId, after.MainQuest.CurrentQuestSceneId,
|
after.MainQuest.CurrentQuestFlowType, after.MainQuest.CurrentMainQuestRouteId, after.MainQuest.CurrentQuestSceneId,
|
||||||
after.MainQuest.HeadQuestSceneId, boolToInt(after.MainQuest.IsReachedLastQuestScene), after.MainQuest.ProgressQuestSceneId,
|
after.MainQuest.HeadQuestSceneId, boolToInt(after.MainQuest.IsReachedLastQuestScene), after.MainQuest.ProgressQuestSceneId,
|
||||||
after.MainQuest.ProgressHeadQuestSceneId, after.MainQuest.ProgressQuestFlowType, after.MainQuest.MainQuestSeasonId,
|
after.MainQuest.ProgressHeadQuestSceneId, after.MainQuest.ProgressQuestFlowType, after.MainQuest.MainQuestSeasonId,
|
||||||
after.MainQuest.LatestVersion, after.MainQuest.SavedCurrentQuestSceneId, after.MainQuest.SavedHeadQuestSceneId,
|
after.MainQuest.LatestVersion,
|
||||||
|
boolToInt(after.MainQuest.SavedContext.Active),
|
||||||
|
after.MainQuest.SavedContext.CurrentQuestSceneId, after.MainQuest.SavedContext.HeadQuestSceneId,
|
||||||
|
after.MainQuest.SavedContext.CurrentMainQuestRouteId, after.MainQuest.SavedContext.MainQuestSeasonId,
|
||||||
|
boolToInt(after.MainQuest.SavedContext.IsReachedLastQuestScene),
|
||||||
|
boolToInt(after.MainQuest.SavedContext.PortalCageInProgress),
|
||||||
after.MainQuest.ReplayFlowCurrentQuestSceneId, after.MainQuest.ReplayFlowHeadQuestSceneId, uid); err != nil {
|
after.MainQuest.ReplayFlowCurrentQuestSceneId, after.MainQuest.ReplayFlowHeadQuestSceneId, uid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -734,6 +750,18 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
|||||||
func(v store.SideStoryQuestProgress) []any {
|
func(v store.SideStoryQuestProgress) []any {
|
||||||
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
return []any{0, v.HeadSideStoryQuestSceneId, int32(v.SideStoryQuestStateType), v.LatestVersion}
|
||||||
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
}, "side_story_quest_id, head_side_story_quest_scene_id, side_story_quest_state_type, latest_version")
|
||||||
|
|
||||||
|
for k, v := range after.MainQuestSeasonRoutes {
|
||||||
|
if old, ok := before.MainQuestSeasonRoutes[k]; !ok || old != v {
|
||||||
|
exec(`INSERT OR REPLACE INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version) VALUES (?,?,?,?)`,
|
||||||
|
uid, k.MainQuestSeasonId, k.MainQuestRouteId, v.LatestVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k := range before.MainQuestSeasonRoutes {
|
||||||
|
if _, ok := after.MainQuestSeasonRoutes[k]; !ok {
|
||||||
|
exec(`DELETE FROM user_main_quest_season_routes WHERE user_id=? AND main_quest_season_id=? AND main_quest_route_id=?`, uid, k.MainQuestSeasonId, k.MainQuestRouteId)
|
||||||
|
}
|
||||||
|
}
|
||||||
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
diffMapInt32(tx, uid, before.QuestLimitContentStatus, after.QuestLimitContentStatus, "user_quest_limit_content_status", "limit_content_id",
|
||||||
func(v store.QuestLimitContentStatus) []any {
|
func(v store.QuestLimitContentStatus) []any {
|
||||||
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion}
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func (s *SQLiteStore) ImportUser(u *store.UserState) error {
|
|||||||
"user_big_hunt_max_scores",
|
"user_big_hunt_max_scores",
|
||||||
"user_quest_limit_content_status",
|
"user_quest_limit_content_status",
|
||||||
"user_side_story_quests",
|
"user_side_story_quests",
|
||||||
|
"user_main_quest_season_routes",
|
||||||
"user_missions",
|
"user_missions",
|
||||||
"user_quest_missions",
|
"user_quest_missions",
|
||||||
"user_quests",
|
"user_quests",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type UserState struct {
|
|||||||
LoginBonus UserLoginBonusState
|
LoginBonus UserLoginBonusState
|
||||||
Tutorials map[int32]TutorialProgressState
|
Tutorials map[int32]TutorialProgressState
|
||||||
MainQuest MainQuestState
|
MainQuest MainQuestState
|
||||||
|
MainQuestSeasonRoutes map[SeasonRouteKey]SeasonRouteEntry
|
||||||
EventQuest EventQuestState
|
EventQuest EventQuestState
|
||||||
ExtraQuest ExtraQuestState
|
ExtraQuest ExtraQuestState
|
||||||
SideStoryQuests map[int32]SideStoryQuestProgress
|
SideStoryQuests map[int32]SideStoryQuestProgress
|
||||||
@@ -153,6 +154,9 @@ func (u *UserState) EnsureMaps() {
|
|||||||
if u.SideStoryQuests == nil {
|
if u.SideStoryQuests == nil {
|
||||||
u.SideStoryQuests = make(map[int32]SideStoryQuestProgress)
|
u.SideStoryQuests = make(map[int32]SideStoryQuestProgress)
|
||||||
}
|
}
|
||||||
|
if u.MainQuestSeasonRoutes == nil {
|
||||||
|
u.MainQuestSeasonRoutes = make(map[SeasonRouteKey]SeasonRouteEntry)
|
||||||
|
}
|
||||||
if u.QuestLimitContentStatus == nil {
|
if u.QuestLimitContentStatus == nil {
|
||||||
u.QuestLimitContentStatus = make(map[int32]QuestLimitContentStatus)
|
u.QuestLimitContentStatus = make(map[int32]QuestLimitContentStatus)
|
||||||
}
|
}
|
||||||
@@ -510,12 +514,24 @@ type MainQuestState struct {
|
|||||||
MainQuestSeasonId int32
|
MainQuestSeasonId int32
|
||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
|
|
||||||
SavedCurrentQuestSceneId int32
|
SavedContext SavedQuestContext
|
||||||
SavedHeadQuestSceneId int32
|
|
||||||
ReplayFlowCurrentQuestSceneId int32
|
ReplayFlowCurrentQuestSceneId int32
|
||||||
ReplayFlowHeadQuestSceneId int32
|
ReplayFlowHeadQuestSceneId int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SavedQuestContext snapshots player state when entering a menu-replay (cleared
|
||||||
|
// quest started from the Main Quest List menu). On finish, every field is
|
||||||
|
// restored atomically so the player returns to the exact pre-replay state.
|
||||||
|
type SavedQuestContext struct {
|
||||||
|
Active bool
|
||||||
|
CurrentQuestSceneId int32
|
||||||
|
HeadQuestSceneId int32
|
||||||
|
CurrentMainQuestRouteId int32
|
||||||
|
MainQuestSeasonId int32
|
||||||
|
IsReachedLastQuestScene bool
|
||||||
|
PortalCageInProgress bool
|
||||||
|
}
|
||||||
|
|
||||||
type EventQuestState struct {
|
type EventQuestState struct {
|
||||||
CurrentEventQuestChapterId int32
|
CurrentEventQuestChapterId int32
|
||||||
CurrentQuestId int32
|
CurrentQuestId int32
|
||||||
@@ -543,6 +559,17 @@ type SideStoryActiveProgress struct {
|
|||||||
LatestVersion int64
|
LatestVersion int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SeasonRouteKey struct {
|
||||||
|
MainQuestSeasonId int32
|
||||||
|
MainQuestRouteId int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type SeasonRouteEntry struct {
|
||||||
|
MainQuestSeasonId int32
|
||||||
|
MainQuestRouteId int32
|
||||||
|
LatestVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
type QuestLimitContentStatus struct {
|
type QuestLimitContentStatus struct {
|
||||||
LimitContentQuestStatusType int32
|
LimitContentQuestStatusType int32
|
||||||
EventQuestChapterId int32
|
EventQuestChapterId int32
|
||||||
|
|||||||
@@ -100,9 +100,11 @@ func ChangedTables(before, after *store.UserState) []string {
|
|||||||
add("IUserMainQuestFlowStatus")
|
add("IUserMainQuestFlowStatus")
|
||||||
add("IUserMainQuestMainFlowStatus")
|
add("IUserMainQuestMainFlowStatus")
|
||||||
add("IUserMainQuestProgressStatus")
|
add("IUserMainQuestProgressStatus")
|
||||||
add("IUserMainQuestSeasonRoute")
|
|
||||||
add("IUserMainQuestReplayFlowStatus")
|
add("IUserMainQuestReplayFlowStatus")
|
||||||
}
|
}
|
||||||
|
if !mapsEqualStruct(before.MainQuestSeasonRoutes, after.MainQuestSeasonRoutes) {
|
||||||
|
add("IUserMainQuestSeasonRoute")
|
||||||
|
}
|
||||||
if before.EventQuest != after.EventQuest {
|
if before.EventQuest != after.EventQuest {
|
||||||
add("IUserEventQuestProgressStatus")
|
add("IUserEventQuestProgressStatus")
|
||||||
}
|
}
|
||||||
@@ -434,6 +436,8 @@ func keyFieldsForTable(table string) []string {
|
|||||||
return []string{"userId", "dokanId"}
|
return []string{"userId", "dokanId"}
|
||||||
case "IUserSideStoryQuest":
|
case "IUserSideStoryQuest":
|
||||||
return []string{"userId", "sideStoryQuestId"}
|
return []string{"userId", "sideStoryQuestId"}
|
||||||
|
case "IUserMainQuestSeasonRoute":
|
||||||
|
return []string{"userId", "mainQuestSeasonId", "mainQuestRouteId"}
|
||||||
case "IUserQuestLimitContentStatus":
|
case "IUserQuestLimitContentStatus":
|
||||||
return []string{"userId", "questId"}
|
return []string{"userId", "questId"}
|
||||||
case "IUserBigHuntMaxScore":
|
case "IUserBigHuntMaxScore":
|
||||||
|
|||||||
@@ -98,12 +98,37 @@ func init() {
|
|||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
|
register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
|
||||||
s, _ := utils.EncodeJSONMaps(map[string]any{
|
if len(user.MainQuestSeasonRoutes) == 0 {
|
||||||
"userId": user.UserId,
|
// Fallback to current (season, route) for legacy saves with no history.
|
||||||
"mainQuestSeasonId": user.MainQuest.MainQuestSeasonId,
|
s, _ := utils.EncodeJSONMaps(map[string]any{
|
||||||
"mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
|
"userId": user.UserId,
|
||||||
"latestVersion": user.MainQuest.LatestVersion,
|
"mainQuestSeasonId": user.MainQuest.MainQuestSeasonId,
|
||||||
|
"mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
|
||||||
|
"latestVersion": user.MainQuest.LatestVersion,
|
||||||
|
})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
keys := make([]store.SeasonRouteKey, 0, len(user.MainQuestSeasonRoutes))
|
||||||
|
for k := range user.MainQuestSeasonRoutes {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
|
if keys[i].MainQuestSeasonId != keys[j].MainQuestSeasonId {
|
||||||
|
return keys[i].MainQuestSeasonId < keys[j].MainQuestSeasonId
|
||||||
|
}
|
||||||
|
return keys[i].MainQuestRouteId < keys[j].MainQuestRouteId
|
||||||
})
|
})
|
||||||
|
records := make([]map[string]any, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
e := user.MainQuestSeasonRoutes[k]
|
||||||
|
records = append(records, map[string]any{
|
||||||
|
"userId": user.UserId,
|
||||||
|
"mainQuestSeasonId": e.MainQuestSeasonId,
|
||||||
|
"mainQuestRouteId": e.MainQuestRouteId,
|
||||||
|
"latestVersion": e.LatestVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s, _ := utils.EncodeJSONMaps(records...)
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
register("IUserEventQuestProgressStatus", func(user store.UserState) string {
|
register("IUserEventQuestProgressStatus", func(user store.UserState) string {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- +goose Up
|
||||||
|
CREATE TABLE user_main_quest_season_routes (
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(user_id),
|
||||||
|
main_quest_season_id INTEGER NOT NULL,
|
||||||
|
main_quest_route_id INTEGER NOT NULL,
|
||||||
|
latest_version INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (user_id, main_quest_season_id, main_quest_route_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Backfill: seed each user's current (season, route) so existing saves immediately
|
||||||
|
-- have at least one entry. Players who progressed through prior seasons in our
|
||||||
|
-- server won't get historical entries — accept that for the immediate fix.
|
||||||
|
INSERT INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version)
|
||||||
|
SELECT user_id, main_quest_season_id, current_main_quest_route_id, latest_version
|
||||||
|
FROM user_main_quest
|
||||||
|
WHERE main_quest_season_id > 0 AND current_main_quest_route_id > 0;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP TABLE IF EXISTS user_main_quest_season_routes;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_active INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_current_quest_scene_id INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_head_quest_scene_id INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_current_main_quest_route_id INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_main_quest_season_id INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_is_reached_last_quest_scene INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE user_main_quest ADD COLUMN saved_ctx_portal_cage_in_progress INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_active;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_current_quest_scene_id;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_head_quest_scene_id;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_current_main_quest_route_id;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_main_quest_season_id;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_is_reached_last_quest_scene;
|
||||||
|
ALTER TABLE user_main_quest DROP COLUMN saved_ctx_portal_cage_in_progress;
|
||||||
Reference in New Issue
Block a user