mirror of
https://github.com/Walter-Sparrow/lunar-tear.git
synced 2026-07-02 05:43:41 +03:00
Fix map replay flow and quest mission rewards
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:
@@ -12,34 +12,6 @@ type BattleDropInfo struct {
|
||||
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 {
|
||||
SceneById map[int32]EntityMQuestScene
|
||||
MissionById map[int32]EntityMQuestMission
|
||||
@@ -62,7 +34,6 @@ type QuestCatalog struct {
|
||||
TutorialUnlockConditions []EntityMTutorialUnlockCondition
|
||||
ChapterLastSceneByQuestId 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
|
||||
|
||||
@@ -148,22 +119,6 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
||||
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")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load quest first clear reward switch table: %w", err)
|
||||
@@ -600,7 +555,6 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
||||
TutorialUnlockConditions: tutorialUnlockConds,
|
||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||
SeasonIdByRouteId: seasonIdByRouteId,
|
||||
OrderedSeasonRoutes: orderedSeasonRoutes,
|
||||
QuestsWithDifficulty: questsWithDifficulty,
|
||||
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||
|
||||
|
||||
@@ -12,6 +12,15 @@ const (
|
||||
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
|
||||
)
|
||||
|
||||
// IsReplayQuestFlowType reports whether the flow type indicates an active
|
||||
// replay session — either same-route REPLAY_FLOW or cross-route
|
||||
// ANOTHER_ROUTE_REPLAY_FLOW. Mirrors the client's Story.IsReplayQuestFlowType
|
||||
// predicate (dump.cs:768202).
|
||||
func IsReplayQuestFlowType(t int32) bool {
|
||||
return t == int32(QuestFlowTypeReplayFlow) ||
|
||||
t == int32(QuestFlowTypeAnotherRouteReplayFlow)
|
||||
}
|
||||
|
||||
func (t QuestFlowType) String() string {
|
||||
switch t {
|
||||
case QuestFlowTypeUnknown:
|
||||
|
||||
@@ -35,7 +35,7 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
@@ -72,8 +72,7 @@ func (h *QuestHandler) HandleEventQuestRestart(user *store.UserState, eventQuest
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||
return
|
||||
}
|
||||
@@ -84,8 +83,4 @@ func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, ques
|
||||
}
|
||||
|
||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||
|
||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (h *QuestHandler) HandleExtraQuestFinish(user *store.UserState, questId int
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||
}
|
||||
|
||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||
@@ -67,8 +67,7 @@ func (h *QuestHandler) HandleExtraQuestRestart(user *store.UserState, questId in
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||
return
|
||||
}
|
||||
@@ -79,8 +78,4 @@ func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, ques
|
||||
}
|
||||
|
||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
||||
|
||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
||||
h.clearQuestMissions(user, scene.QuestId, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,13 +78,9 @@ func (h *QuestHandler) handleQuestStartInternal(user *store.UserState, questId i
|
||||
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)
|
||||
case isReplayFlow:
|
||||
h.applyReplayStart(user, questId, isBattleOnly, nowMillis)
|
||||
return
|
||||
}
|
||||
|
||||
if isCleared {
|
||||
@@ -129,6 +125,27 @@ func snapshotMainQuestIfNeeded(user *store.UserState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve CurrentQuestFlowType when HandleReplayFlowSceneProgress already
|
||||
// set it: replay-variant ids (30000+) aren't in RouteIdByQuestId.
|
||||
func (h *QuestHandler) applyReplayStart(user *store.UserState, questId int32, isBattleOnly bool, nowMillis int64) {
|
||||
flowType := h.replayFlowTypeFromQuestId(user, questId)
|
||||
if model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType) {
|
||||
flowType = model.QuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||
}
|
||||
user.MainQuest.CurrentQuestFlowType = int32(flowType)
|
||||
user.MainQuest.LatestVersion = nowMillis
|
||||
|
||||
questState := user.Quests[questId]
|
||||
questState.QuestStateType = model.UserQuestStateTypeActive
|
||||
questState.LatestStartDatetime = nowMillis
|
||||
user.Quests[questId] = questState
|
||||
|
||||
log.Printf("[HandleQuestStart] replay quest=%d flowType=%s isBattleOnly=%v current=%d head=%d",
|
||||
questId, flowType, isBattleOnly,
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId,
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
||||
if isBattleOnly {
|
||||
if v, ok := h.BattleOnlyTargetSceneIdFor(questId); ok {
|
||||
@@ -141,14 +158,24 @@ func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
||||
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, wasReplay bool) {
|
||||
questState := user.Quests[questId]
|
||||
if !questState.IsRewardGranted {
|
||||
h.applyQuestRewards(user, questId, nowMillis)
|
||||
h.applyExpAndGoldRewards(user, questId, nowMillis)
|
||||
if !wasReplay {
|
||||
h.applyFirstClearItemRewards(user, questId, nowMillis)
|
||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)...)
|
||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)...)
|
||||
}
|
||||
|
||||
for _, r := range outcome.MissionClearRewards {
|
||||
h.applyRewardPossession(user, r.PossessionType, r.PossessionId, r.Count, nowMillis)
|
||||
}
|
||||
for _, r := range outcome.MissionClearCompleteRewards {
|
||||
h.applyRewardPossession(user, r.PossessionType, r.PossessionId, r.Count, nowMillis)
|
||||
}
|
||||
questState.IsRewardGranted = true
|
||||
}
|
||||
for _, drop := range outcome.DropRewards {
|
||||
@@ -197,13 +224,13 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
||||
h.initQuestState(user, questId)
|
||||
|
||||
outcome := h.evaluateFinishOutcome(user, questId)
|
||||
wasReplay := user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow)
|
||||
wasReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||
|
||||
if !isRetired {
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
||||
h.applyQuestVictory(user, questId, &outcome, nowMillis, wasReplay)
|
||||
|
||||
if isMainQuestPlayable(quest) && !wasReplay && !wasMenuReplay {
|
||||
if isMainQuestPlayable(quest) && !wasMenuReplay {
|
||||
lastSceneId := h.getLastMainFlowSceneId(questId)
|
||||
h.advanceMainFlowScene(user, questId, lastSceneId)
|
||||
}
|
||||
@@ -229,8 +256,12 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
||||
|
||||
user.MainQuest.ProgressQuestSceneId = 0
|
||||
user.MainQuest.ProgressHeadQuestSceneId = 0
|
||||
if !wasReplay {
|
||||
// Keep replay flow types on replay finish so the client's
|
||||
// Story.ApplyNewestPlayingScene keeps _isReplayed=true (popup result UI).
|
||||
user.MainQuest.ProgressQuestFlowType = 0
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeUnknown)
|
||||
}
|
||||
|
||||
if wasMenuReplay {
|
||||
ctx := user.MainQuest.SavedContext
|
||||
@@ -248,12 +279,6 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
||||
ctx.CurrentQuestSceneId, ctx.HeadQuestSceneId, ctx.PortalCageInProgress)
|
||||
}
|
||||
|
||||
if wasReplay {
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
||||
log.Printf("[HandleQuestFinish] replay flow ended for quest %d", questId)
|
||||
}
|
||||
|
||||
h.clearQuestMissions(user, questId, nowMillis)
|
||||
|
||||
return outcome
|
||||
|
||||
@@ -51,7 +51,9 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
||||
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
||||
}
|
||||
|
||||
if !questState.IsRewardGranted {
|
||||
isReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||
|
||||
if !questState.IsRewardGranted && !isReplay {
|
||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
|
||||
@@ -62,7 +64,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
||||
}
|
||||
}
|
||||
|
||||
if user.MainQuest.CurrentQuestFlowType == int32(model.QuestFlowTypeReplayFlow) && questDef.QuestReplayFlowRewardGroupId > 0 {
|
||||
if isReplay && questDef.QuestReplayFlowRewardGroupId > 0 {
|
||||
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
|
||||
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
||||
PossessionType: model.PossessionType(reward.PossessionType),
|
||||
@@ -72,6 +74,10 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
||||
}
|
||||
}
|
||||
|
||||
// Mission rewards / BigWin are first-clear concepts. Reference
|
||||
// IUserQuestMissionTable has no rows for replay-variant ids (30000+):
|
||||
// the popup is empty on replay in the original game.
|
||||
if !isReplay {
|
||||
pendingClearCount := 0
|
||||
regularMissionCount := 0
|
||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||
@@ -115,6 +121,7 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
||||
}
|
||||
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
||||
}
|
||||
}
|
||||
|
||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
||||
return outcome
|
||||
@@ -240,7 +247,7 @@ func (h *QuestHandler) resolveDeckUnits(user *store.UserState, questId int32) (c
|
||||
return costumeUuids, characterIds
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
func (h *QuestHandler) applyExpAndGoldRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
return
|
||||
@@ -252,13 +259,24 @@ func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, n
|
||||
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
||||
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyFirstClearItemRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
questDef, ok := h.QuestById[questId]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||
h.applyRewardPossession(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, nowMillis int64) {
|
||||
h.applyExpAndGoldRewards(user, questId, nowMillis)
|
||||
h.applyFirstClearItemRewards(user, questId, nowMillis)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
|
||||
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package questflow
|
||||
|
||||
// MainQuest scene-field families mirror three client entity tables:
|
||||
//
|
||||
// MainFlow* — EntityIUserMainQuestMainFlowStatus (#11443)
|
||||
// Progress* — EntityIUserMainQuestProgressStatus (#11444)
|
||||
// ReplayFlow* — EntityIUserMainQuestReplayFlowStatus (#11445)
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -45,10 +51,8 @@ func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, scen
|
||||
}
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Backs IUserMainQuestSeasonRoute: the client needs the history to load
|
||||
// scene metadata when cage menu-replay jumps to older chapters.
|
||||
func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) {
|
||||
if seasonId <= 0 || routeId <= 0 {
|
||||
return
|
||||
@@ -133,11 +137,48 @@ func (h *QuestHandler) getChapterLastSceneId(questId int32) int32 {
|
||||
|
||||
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||
if user.MainQuest.ReplayFlowHeadQuestSceneId == 0 || h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||
}
|
||||
|
||||
user.PortalCageStatus.IsCurrentProgress = false
|
||||
user.PortalCageStatus.LatestVersion = nowMillis
|
||||
|
||||
flowType := h.replayFlowType(user, questSceneId)
|
||||
user.MainQuest.CurrentQuestFlowType = int32(flowType)
|
||||
user.MainQuest.LatestVersion = nowMillis
|
||||
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d replayHead=%d", questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId)
|
||||
log.Printf("[HandleReplayFlowSceneProgress] sceneId=%d flowType=%s", questSceneId, flowType)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) replayFlowType(user *store.UserState, questSceneId int32) model.QuestFlowType {
|
||||
scene, ok := h.SceneById[questSceneId]
|
||||
if !ok {
|
||||
return model.QuestFlowTypeReplayFlow
|
||||
}
|
||||
routeId, ok := h.RouteIdByQuestId[scene.QuestId]
|
||||
if !ok {
|
||||
return model.QuestFlowTypeReplayFlow
|
||||
}
|
||||
return h.replayFlowTypeForRoute(user, routeId)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) replayFlowTypeForRoute(user *store.UserState, routeId int32) model.QuestFlowType {
|
||||
seasonId, ok := h.SeasonIdByRouteId[routeId]
|
||||
if !ok {
|
||||
return model.QuestFlowTypeReplayFlow
|
||||
}
|
||||
for key, entry := range user.MainQuestSeasonRoutes {
|
||||
if key.MainQuestSeasonId == seasonId && entry.MainQuestRouteId != routeId {
|
||||
return model.QuestFlowTypeAnotherRouteReplayFlow
|
||||
}
|
||||
}
|
||||
return model.QuestFlowTypeReplayFlow
|
||||
}
|
||||
|
||||
func (h *QuestHandler) replayFlowTypeFromQuestId(user *store.UserState, questId int32) model.QuestFlowType {
|
||||
routeId, ok := h.RouteIdByQuestId[questId]
|
||||
if !ok {
|
||||
return model.QuestFlowTypeReplayFlow
|
||||
}
|
||||
return h.replayFlowTypeForRoute(user, routeId)
|
||||
}
|
||||
|
||||
func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, questSceneId int32) {
|
||||
@@ -153,22 +194,27 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
||||
|
||||
if prevSceneId := user.MainQuest.ProgressQuestSceneId; prevSceneId != 0 {
|
||||
if prevScene, ok := h.SceneById[prevSceneId]; ok && prevScene.QuestId != quest.QuestId {
|
||||
// Skip if the previous quest is playable — it has its own FinishMainQuest;
|
||||
// chain-finalizing here would double-increment ClearCount.
|
||||
if prevQuest, ok := h.QuestById[prevScene.QuestId]; ok && !isMainQuestPlayable(prevQuest) {
|
||||
h.finalizeChainPreviousQuest(user, prevScene.QuestId, gametime.NowMillis())
|
||||
}
|
||||
}
|
||||
|
||||
if isMainQuestPlayable(quest) {
|
||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
||||
nowMillis := gametime.NowMillis()
|
||||
h.clearQuestMissions(user, quest.QuestId, nowMillis)
|
||||
}
|
||||
|
||||
isReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||
|
||||
if isMainQuestPlayable(quest) {
|
||||
user.MainQuest.ProgressQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
||||
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
||||
}
|
||||
if isReplay {
|
||||
user.MainQuest.ProgressQuestFlowType = user.MainQuest.CurrentQuestFlowType
|
||||
} else {
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||
}
|
||||
} else {
|
||||
user.MainQuest.CurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||
@@ -177,4 +223,12 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
||||
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
||||
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
||||
}
|
||||
|
||||
if isReplay {
|
||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||
if h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
||||
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||
}
|
||||
user.MainQuest.LatestVersion = gametime.NowMillis()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,12 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
|
||||
now := gametime.NowMillis()
|
||||
user.PortalCageStatus.IsCurrentProgress = true
|
||||
user.PortalCageStatus.LatestVersion = now
|
||||
// Mama's Room ends any active replay — flip flow type to MainFlow;
|
||||
// ReplayFlow* stay sticky (matches original userdata snapshot).
|
||||
if user.MainQuest.CurrentQuestFlowType != int32(model.QuestFlowTypeMainFlow) {
|
||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
||||
user.MainQuest.LatestVersion = now
|
||||
}
|
||||
})
|
||||
return &pb.UpdatePortalCageSceneProgressResponse{}, nil
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
pb "lunar-tear/server/gen/proto"
|
||||
"lunar-tear/server/internal/gametime"
|
||||
"lunar-tear/server/internal/model"
|
||||
"lunar-tear/server/internal/questflow"
|
||||
"lunar-tear/server/internal/runtime"
|
||||
"lunar-tear/server/internal/store"
|
||||
)
|
||||
@@ -96,24 +95,7 @@ func (s *UserServiceServer) GameStart(ctx context.Context, _ *emptypb.Empty) (*p
|
||||
|
||||
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
user.GameStartDatetime = gametime.NowMillis()
|
||||
})
|
||||
|
||||
return &pb.GameStartResponse{}, nil
|
||||
|
||||
@@ -537,7 +537,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
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) {
|
||||
@@ -560,7 +559,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
@@ -568,7 +566,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
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
|
||||
@@ -586,7 +583,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
}
|
||||
})
|
||||
|
||||
// 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) {
|
||||
@@ -642,7 +638,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
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) {
|
||||
@@ -685,7 +680,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
||||
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
|
||||
|
||||
@@ -15,8 +15,7 @@ func boolToInt(b bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// writeUserState inserts all child table rows for a newly created user.
|
||||
// The users row must already exist.
|
||||
// Precondition: the users row must already exist.
|
||||
func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||
exec := func(query string, args ...any) error {
|
||||
_, err := tx.Exec(query, args...)
|
||||
@@ -123,7 +122,6 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Map tables
|
||||
for _, v := range u.Characters {
|
||||
if err := exec(`INSERT INTO user_characters (user_id, character_id, level, exp, latest_version) VALUES (?,?,?,?,?)`,
|
||||
uid, v.CharacterId, v.Level, v.Exp, v.LatestVersion); err != nil {
|
||||
@@ -507,18 +505,13 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// diffAndSave compares before/after UserState and writes only changed rows.
|
||||
// For 1:1 tables, it UPDATEs if any field changed.
|
||||
// For map tables, it uses INSERT OR REPLACE for added/modified entries and DELETE for removed ones.
|
||||
// For slice-based data (gifts, medals, deck sub-weapons/parts, weapon skills/abilities),
|
||||
// it does DELETE-all then INSERT-all for simplicity.
|
||||
// 1:1 tables update on field-change; maps INSERT OR REPLACE + DELETE; slice tables DELETE-all then INSERT-all.
|
||||
func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
exec := func(query string, args ...any) error {
|
||||
_, err := tx.Exec(query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// users table
|
||||
if before.PlayerId != after.PlayerId || before.OsType != after.OsType || before.PlatformType != after.PlatformType ||
|
||||
before.UserRestrictionType != after.UserRestrictionType || before.RegisterDatetime != after.RegisterDatetime ||
|
||||
before.GameStartDatetime != after.GameStartDatetime || before.LatestVersion != after.LatestVersion ||
|
||||
@@ -653,7 +646,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Gacha scalar
|
||||
if before.Gacha.RewardAvailable != after.Gacha.RewardAvailable || before.Gacha.TodaysCurrentDrawCount != after.Gacha.TodaysCurrentDrawCount ||
|
||||
before.Gacha.DailyMaxCount != after.Gacha.DailyMaxCount || before.Gacha.LastRewardDrawDate != after.Gacha.LastRewardDrawDate {
|
||||
var obtainItemId, obtainCount sql.NullInt64
|
||||
@@ -668,7 +660,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Map tables — use generic diff helpers
|
||||
diffMapInt32(tx, uid, before.Characters, after.Characters, "user_characters", "character_id",
|
||||
func(v store.CharacterState) []any { return []any{v.CharacterId, v.Level, v.Exp, v.LatestVersion} },
|
||||
"character_id, level, exp, latest_version")
|
||||
@@ -693,7 +684,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.UserDeckCharacterUuid, v.UserCostumeUuid, v.MainUserWeaponUuid, v.UserCompanionUuid, v.Power, v.UserThoughtUuid, v.DressupCostumeId, v.LatestVersion}
|
||||
}, "user_deck_character_uuid, user_costume_uuid, main_user_weapon_uuid, user_companion_uuid, power, user_thought_uuid, dressup_costume_id, latest_version")
|
||||
|
||||
// Decks (composite key)
|
||||
for k, v := range after.Decks {
|
||||
if old, ok := before.Decks[k]; !ok || old != v {
|
||||
exec(fmt.Sprintf(`INSERT OR REPLACE INTO user_decks (user_id, deck_type, user_deck_number, user_deck_character_uuid01, user_deck_character_uuid02, user_deck_character_uuid03, name, power, latest_version) VALUES (?,?,?,?,?,?,?,?,?)`),
|
||||
@@ -706,7 +696,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Slice-based tables: delete all + reinsert
|
||||
replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) {
|
||||
for i, uuid := range uuids {
|
||||
exec(`INSERT INTO user_deck_sub_weapons (user_id, user_deck_character_uuid, ordinal, user_weapon_uuid) VALUES (?,?,?,?)`, uid, key, i, uuid)
|
||||
@@ -723,7 +712,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.QuestId, int32(v.QuestStateType), boolToInt(v.IsBattleOnly), v.UserDeckNumber, v.LatestStartDatetime, v.ClearCount, v.DailyClearCount, v.LastClearDatetime, v.ShortestClearFrames, boolToInt(v.IsRewardGranted), v.LatestVersion}
|
||||
}, "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")
|
||||
|
||||
// Quest missions (composite key)
|
||||
for k, v := range after.QuestMissions {
|
||||
if old, ok := before.QuestMissions[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_quest_missions (user_id, quest_id, quest_mission_id, progress_value, is_clear, latest_clear_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
||||
@@ -776,7 +764,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.WeaponId, v.MaxLevel, v.MaxLimitBreakCount, v.FirstAcquisitionDatetime, v.LatestVersion}
|
||||
}, "weapon_id, max_level, max_limit_break_count, first_acquisition_datetime, latest_version")
|
||||
|
||||
// Weapon skills/abilities: slice-based, delete+reinsert
|
||||
exec(`DELETE FROM user_weapon_skills WHERE user_id=?`, uid)
|
||||
for _, skills := range after.WeaponSkills {
|
||||
for _, v := range skills {
|
||||
@@ -798,7 +785,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.UserCostumeUuid, v.Level, v.AcquisitionDatetime, v.LatestVersion}
|
||||
}, "user_costume_uuid, level, acquisition_datetime, latest_version")
|
||||
|
||||
// Costume awaken status ups (composite key)
|
||||
for k, v := range after.CostumeAwakenStatusUps {
|
||||
if old, ok := before.CostumeAwakenStatusUps[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_costume_awaken_status_ups (user_id, user_costume_uuid, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
@@ -854,7 +840,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Deck type notes (key is model.DeckType which is int32-based)
|
||||
for k, v := range after.DeckTypeNotes {
|
||||
if old, ok := before.DeckTypeNotes[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_deck_type_notes (user_id, deck_type, max_deck_power, latest_version) VALUES (?,?,?,?)`,
|
||||
@@ -888,7 +873,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
diffTimestampMap(tx, uid, before.DrawnOmikuji, after.DrawnOmikuji, "user_drawn_omikuji", "omikuji_id")
|
||||
diffBoolMap(tx, uid, before.DokanConfirmed, after.DokanConfirmed, "user_dokan_confirmed", "dokan_id")
|
||||
|
||||
// Gifts: delete all + reinsert
|
||||
exec(`DELETE FROM user_gifts WHERE user_id=?`, uid)
|
||||
for _, g := range after.Gifts.NotReceived {
|
||||
var expDt sql.NullInt64
|
||||
@@ -904,19 +888,16 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
uid, uuid, g.GiftCommon.PossessionType, g.GiftCommon.PossessionId, g.GiftCommon.Count, g.GiftCommon.GrantDatetime, g.GiftCommon.DescriptionGiftTextId, g.GiftCommon.EquipmentData, g.ReceivedDatetime)
|
||||
}
|
||||
|
||||
// Gacha converted medals: delete+reinsert
|
||||
exec(`DELETE FROM user_gacha_converted_medals WHERE user_id=?`, uid)
|
||||
for i, v := range after.Gacha.ConvertedGachaMedal.ConvertedMedalPossession {
|
||||
exec(`INSERT INTO user_gacha_converted_medals (user_id, ordinal, consumable_item_id, count) VALUES (?,?,?,?)`, uid, i, v.ConsumableItemId, v.Count)
|
||||
}
|
||||
|
||||
// Gacha banners
|
||||
for id, v := range after.Gacha.BannerStates {
|
||||
if old, ok := before.Gacha.BannerStates[id]; !ok || old.MedalCount != v.MedalCount || old.StepNumber != v.StepNumber || old.LoopCount != v.LoopCount || old.DrawCount != v.DrawCount || old.BoxNumber != v.BoxNumber {
|
||||
exec(`INSERT OR REPLACE INTO user_gacha_banners (user_id, gacha_id, medal_count, step_number, loop_count, draw_count, box_number) VALUES (?,?,?,?,?,?,?)`,
|
||||
uid, v.GachaId, v.MedalCount, v.StepNumber, v.LoopCount, v.DrawCount, v.BoxNumber)
|
||||
}
|
||||
// Box drew counts: always delete+reinsert for this gacha
|
||||
exec(`DELETE FROM user_gacha_banner_box_drew_counts WHERE user_id=? AND gacha_id=?`, uid, id)
|
||||
for itemId, count := range v.BoxDrewCounts {
|
||||
exec(`INSERT INTO user_gacha_banner_box_drew_counts (user_id, gacha_id, box_item_id, count) VALUES (?,?,?,?)`, uid, id, itemId, count)
|
||||
@@ -934,7 +915,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return []any{v.CharacterBoardId, v.PanelReleaseBit1, v.PanelReleaseBit2, v.PanelReleaseBit3, v.PanelReleaseBit4, v.LatestVersion}
|
||||
}, "character_board_id, panel_release_bit1, panel_release_bit2, panel_release_bit3, panel_release_bit4, latest_version")
|
||||
|
||||
// Character board abilities (composite key)
|
||||
for k, v := range after.CharacterBoardAbilities {
|
||||
if old, ok := before.CharacterBoardAbilities[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_character_board_abilities (user_id, character_id, ability_id, level, latest_version) VALUES (?,?,?,?,?)`,
|
||||
@@ -947,7 +927,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Character board status ups (composite key)
|
||||
for k, v := range after.CharacterBoardStatusUps {
|
||||
if old, ok := before.CharacterBoardStatusUps[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_character_board_status_ups (user_id, character_id, status_calculation_type, hp, attack, vitality, agility, critical_ratio, critical_attack, latest_version) VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
@@ -980,7 +959,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
},
|
||||
"slot_number, shop_item_id, latest_version")
|
||||
|
||||
// Gimmick tables (composite keys)
|
||||
for k, v := range after.Gimmick.Progress {
|
||||
if old, ok := before.Gimmick.Progress[k]; !ok || old != v {
|
||||
exec(`INSERT OR REPLACE INTO user_gimmick_progress (user_id, gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id, is_gimmick_cleared, start_datetime, latest_version) VALUES (?,?,?,?,?,?,?)`,
|
||||
@@ -1030,7 +1008,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Big hunt maps
|
||||
diffMapInt32(tx, uid, before.BigHuntMaxScores, after.BigHuntMaxScores, "user_big_hunt_max_scores", "big_hunt_boss_id",
|
||||
func(v store.BigHuntMaxScore) []any {
|
||||
return []any{0, v.MaxScore, v.MaxScoreUpdateDatetime, v.LatestVersion}
|
||||
@@ -1079,7 +1056,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generic diff helpers for map tables with int32 keys
|
||||
func diffMapInt32[V comparable](tx *sql.Tx, uid int64, before, after map[int32]V, table, keyCol string, vals func(V) []any, cols string) {
|
||||
for k, v := range after {
|
||||
if old, ok := before[k]; !ok || old != v {
|
||||
|
||||
@@ -153,6 +153,9 @@ func init() {
|
||||
return s
|
||||
})
|
||||
register("IUserMainQuestReplayFlowStatus", func(user store.UserState) string {
|
||||
if user.MainQuest.ReplayFlowCurrentQuestSceneId == 0 && user.MainQuest.ReplayFlowHeadQuestSceneId == 0 {
|
||||
return "[]"
|
||||
}
|
||||
s, _ := utils.EncodeJSONMaps(map[string]any{
|
||||
"userId": user.UserId,
|
||||
"currentHeadQuestSceneId": user.MainQuest.ReplayFlowHeadQuestSceneId,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- +goose Up
|
||||
-- For each user past season 2 with no season-2 row yet, infer which sun/moon
|
||||
-- route they actually played from their cleared quests in user_quests, and
|
||||
-- insert the matching (season=2, route=X) row. Heals players who progressed
|
||||
-- past season 2 before per-scene RecordSeasonRoute existed (commit 9a2cc92).
|
||||
--
|
||||
-- Quest-ID ranges per route (from master_data/EntityMMainQuestSequenceTable.json):
|
||||
-- Route 2 (sun/moon variant A): quests 301..370
|
||||
-- Route 3 (sun/moon variant B): quests 401..470
|
||||
--
|
||||
-- The NOT EXISTS clause matches on (user, season) regardless of route, so any
|
||||
-- existing real RecordSeasonRoute write is preserved verbatim.
|
||||
|
||||
-- Rule 1: user has cleared a route-3 quest -> they picked variant B.
|
||||
INSERT INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version)
|
||||
SELECT umq.user_id, 2, 3, umq.latest_version
|
||||
FROM user_main_quest umq
|
||||
WHERE umq.main_quest_season_id > 2
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_main_quest_season_routes r
|
||||
WHERE r.user_id = umq.user_id AND r.main_quest_season_id = 2
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM user_quests q
|
||||
WHERE q.user_id = umq.user_id AND q.clear_count > 0
|
||||
AND q.quest_id BETWEEN 401 AND 470
|
||||
);
|
||||
|
||||
-- Rule 2: otherwise insert route 2 (covers users who picked variant A and
|
||||
-- users with no observable route-2/route-3 history -- last-resort default).
|
||||
INSERT INTO user_main_quest_season_routes (user_id, main_quest_season_id, main_quest_route_id, latest_version)
|
||||
SELECT umq.user_id, 2, 2, umq.latest_version
|
||||
FROM user_main_quest umq
|
||||
WHERE umq.main_quest_season_id > 2
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_main_quest_season_routes r
|
||||
WHERE r.user_id = umq.user_id AND r.main_quest_season_id = 2
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
SELECT 1;
|
||||
Reference in New Issue
Block a user