From 810adcf990788333fbf12adb8e3ef2b56702048f Mon Sep 17 00:00:00 2001 From: Ilya Groshev Date: Fri, 22 May 2026 17:24:30 +0300 Subject: [PATCH] Derive main-quest season routes at projection time --- server/cmd/claim-account/main.go | 1 + server/internal/masterdata/quest.go | 49 ++++++++++++++++++++++ server/internal/questflow/scene.go | 45 ++++++++++---------- server/internal/runtime/build.go | 2 + server/internal/service/quest_main.go | 1 - server/internal/store/sqlite/load.go | 11 ----- server/internal/store/sqlite/save.go | 17 -------- server/internal/store/types.go | 15 ------- server/internal/userdata/changed_tables.go | 6 ++- server/internal/userdata/proj_quest.go | 43 ++++++++----------- server/internal/userdata/projector.go | 7 ++++ 11 files changed, 101 insertions(+), 96 deletions(-) diff --git a/server/cmd/claim-account/main.go b/server/cmd/claim-account/main.go index f93a32f..0c24048 100644 --- a/server/cmd/claim-account/main.go +++ b/server/cmd/claim-account/main.go @@ -40,6 +40,7 @@ var childTables = []string{ "user_big_hunt_max_scores", "user_quest_limit_content_status", "user_side_story_quests", + "user_main_quest_season_routes", "user_missions", "user_quest_missions", "user_quests", diff --git a/server/internal/masterdata/quest.go b/server/internal/masterdata/quest.go index f8a1e87..5649d07 100644 --- a/server/internal/masterdata/quest.go +++ b/server/internal/masterdata/quest.go @@ -34,6 +34,8 @@ type QuestCatalog struct { TutorialUnlockConditions []EntityMTutorialUnlockCondition ChapterLastSceneByQuestId map[int32]int32 SeasonIdByRouteId map[int32]int32 + RoutesBySeason map[int32][]int32 + RouteCompletionQuestId map[int32]int32 BattleOnlyTargetSceneByQuestId map[int32]int32 UserExpThresholds []int32 @@ -114,8 +116,53 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { return nil, fmt.Errorf("load main quest route table: %w", err) } seasonIdByRouteId := make(map[int32]int32, len(routes)) + routesBySeason := make(map[int32][]int32, len(routes)) + sortOrderByRoute := make(map[int32]int32, len(routes)) for _, r := range routes { seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId + routesBySeason[r.MainQuestSeasonId] = append(routesBySeason[r.MainQuestSeasonId], r.MainQuestRouteId) + sortOrderByRoute[r.MainQuestRouteId] = r.SortOrder + } + for seasonId, ids := range routesBySeason { + s := ids + sort.Slice(s, func(i, j int) bool { return sortOrderByRoute[s[i]] > sortOrderByRoute[s[j]] }) + routesBySeason[seasonId] = s + } + + anotherReplayConds, err := utils.ReadTable[EntityMMainQuestRouteAnotherReplayFlowUnlockCondition]("m_main_quest_route_another_replay_flow_unlock_condition") + if err != nil { + return nil, fmt.Errorf("load main quest route another replay flow unlock condition table: %w", err) + } + evaluateConds, err := utils.ReadTable[EntityMEvaluateCondition]("m_evaluate_condition") + if err != nil { + return nil, fmt.Errorf("load evaluate condition table: %w", err) + } + valueGroupByConditionId := make(map[int32]int32, len(evaluateConds)) + for _, c := range evaluateConds { + valueGroupByConditionId[c.EvaluateConditionId] = c.EvaluateConditionValueGroupId + } + evaluateValueGroups, err := utils.ReadTable[EntityMEvaluateConditionValueGroup]("m_evaluate_condition_value_group") + if err != nil { + return nil, fmt.Errorf("load evaluate condition value group table: %w", err) + } + valueByGroupId := make(map[int32]int32, len(evaluateValueGroups)) + for _, vg := range evaluateValueGroups { + if _, exists := valueByGroupId[vg.EvaluateConditionValueGroupId]; exists { + continue + } + valueByGroupId[vg.EvaluateConditionValueGroupId] = int32(vg.Value) + } + routeCompletionQuestId := make(map[int32]int32, len(anotherReplayConds)) + for _, c := range anotherReplayConds { + valueGroupId, ok := valueGroupByConditionId[c.UnlockEvaluateConditionId] + if !ok { + continue + } + questId, ok := valueByGroupId[valueGroupId] + if !ok { + continue + } + routeCompletionQuestId[c.MainQuestRouteId] = questId } firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch") @@ -539,6 +586,8 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) { TutorialUnlockConditions: tutorialUnlockConds, ChapterLastSceneByQuestId: chapterLastSceneByQuestId, SeasonIdByRouteId: seasonIdByRouteId, + RoutesBySeason: routesBySeason, + RouteCompletionQuestId: routeCompletionQuestId, BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId, UserExpThresholds: BuildExpThresholds(paramMapRows, 1), diff --git a/server/internal/questflow/scene.go b/server/internal/questflow/scene.go index ea5f0c2..ff031f5 100644 --- a/server/internal/questflow/scene.go +++ b/server/internal/questflow/scene.go @@ -46,7 +46,6 @@ func (h *QuestHandler) advanceMainFlowScene(user *store.UserState, questId, scen user.MainQuest.CurrentMainQuestRouteId = routeId if seasonId, ok := h.SeasonIdByRouteId[routeId]; ok { user.MainQuest.MainQuestSeasonId = seasonId - RecordSeasonRoute(user, seasonId, routeId, gametime.NowMillis()) } } } @@ -59,22 +58,27 @@ func (h *QuestHandler) advanceReplayFlowScene(user *store.UserState, sceneId int user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId } -func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) { - if seasonId <= 0 || routeId <= 0 { - return +func (h *QuestHandler) SeasonRoutesFor(user *store.UserState) map[int32]int32 { + out := make(map[int32]int32) + for seasonId, routes := range h.RoutesBySeason { + if seasonId <= 1 { + continue + } + for _, routeId := range routes { + finalQuestId, ok := h.RouteCompletionQuestId[routeId] + if !ok { + continue + } + if q, ok := user.Quests[finalQuestId]; ok && q.ClearCount > 0 { + out[seasonId] = routeId + break + } + } } - 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, + if cur := user.MainQuest.MainQuestSeasonId; cur >= 2 && user.MainQuest.CurrentMainQuestRouteId > 0 { + out[cur] = user.MainQuest.CurrentMainQuestRouteId } + return out } func (h *QuestHandler) HandleMainFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) { @@ -177,15 +181,8 @@ func (h *QuestHandler) replayFlowTypeForRoute(user *store.UserState, routeId int if !ok { return model.QuestFlowTypeReplayFlow } - for key, entry := range user.MainQuestSeasonRoutes { - if key.MainQuestSeasonId == seasonId && entry.MainQuestRouteId != routeId { - return model.QuestFlowTypeAnotherRouteReplayFlow - } - } - if len(user.MainQuestSeasonRoutes) == 0 && - user.MainQuest.MainQuestSeasonId == seasonId && - user.MainQuest.CurrentMainQuestRouteId != 0 && - user.MainQuest.CurrentMainQuestRouteId != routeId { + pairs := h.SeasonRoutesFor(user) + if recorded, ok := pairs[seasonId]; ok && recorded != routeId { return model.QuestFlowTypeAnotherRouteReplayFlow } return model.QuestFlowTypeReplayFlow diff --git a/server/internal/runtime/build.go b/server/internal/runtime/build.go index 678cd7e..835a7e2 100644 --- a/server/internal/runtime/build.go +++ b/server/internal/runtime/build.go @@ -8,6 +8,7 @@ import ( "lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata/memorydb" "lunar-tear/server/internal/questflow" + "lunar-tear/server/internal/userdata" ) // buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever @@ -35,6 +36,7 @@ func buildCatalogs() (*Catalogs, error) { } sideStoryCatalog := masterdata.LoadSideStoryCatalog() questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog) + userdata.SetQuestHandler(questHandler) gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() if err != nil { diff --git a/server/internal/service/quest_main.go b/server/internal/service/quest_main.go index b668921..48b7c4b 100644 --- a/server/internal/service/quest_main.go +++ b/server/internal/service/quest_main.go @@ -198,7 +198,6 @@ func (s *QuestServiceServer) SetRoute(ctx context.Context, req *pb.SetRouteReque user.MainQuest.CurrentMainQuestRouteId = req.MainQuestRouteId if seasonId, ok := engine.SeasonIdByRouteId[req.MainQuestRouteId]; ok { user.MainQuest.MainQuestSeasonId = seasonId - questflow.RecordSeasonRoute(user, seasonId, req.MainQuestRouteId, gametime.NowMillis()) } now := gametime.NowMillis() user.PortalCageStatus.IsCurrentProgress = false diff --git a/server/internal/store/sqlite/load.go b/server/internal/store/sqlite/load.go index ee93181..51a1d66 100644 --- a/server/internal/store/sqlite/load.go +++ b/server/internal/store/sqlite/load.go @@ -86,7 +86,6 @@ func initMaps(u *store.UserState) { u.CharacterRebirths = make(map[int32]store.CharacterRebirthState) u.AutoSaleSettings = make(map[int32]store.AutoSaleSettingState) u.SideStoryQuests = make(map[int32]store.SideStoryQuestProgress) - u.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry) u.QuestLimitContentStatus = make(map[int32]store.QuestLimitContentStatus) u.BigHuntMaxScores = make(map[int32]store.BigHuntMaxScore) u.BigHuntStatuses = make(map[int32]store.BigHuntStatus) @@ -378,16 +377,6 @@ 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 FROM user_quest_limit_content_status WHERE user_id=?`, uid, func(rows *sql.Rows) { var id int32 diff --git a/server/internal/store/sqlite/save.go b/server/internal/store/sqlite/save.go index 7689f48..e72dcb5 100644 --- a/server/internal/store/sqlite/save.go +++ b/server/internal/store/sqlite/save.go @@ -224,12 +224,6 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error { 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 { 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 { @@ -798,17 +792,6 @@ func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error { 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") - 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", func(v store.QuestLimitContentStatus) []any { return []any{0, v.LimitContentQuestStatusType, v.EventQuestChapterId, v.LatestVersion} diff --git a/server/internal/store/types.go b/server/internal/store/types.go index 6ae3752..863be51 100644 --- a/server/internal/store/types.go +++ b/server/internal/store/types.go @@ -41,7 +41,6 @@ type UserState struct { LoginBonus UserLoginBonusState Tutorials map[int32]TutorialProgressState MainQuest MainQuestState - MainQuestSeasonRoutes map[SeasonRouteKey]SeasonRouteEntry EventQuest EventQuestState ExtraQuest ExtraQuestState SideStoryQuests map[int32]SideStoryQuestProgress @@ -162,9 +161,6 @@ func (u *UserState) EnsureMaps() { if u.SideStoryQuests == nil { u.SideStoryQuests = make(map[int32]SideStoryQuestProgress) } - if u.MainQuestSeasonRoutes == nil { - u.MainQuestSeasonRoutes = make(map[SeasonRouteKey]SeasonRouteEntry) - } if u.QuestLimitContentStatus == nil { u.QuestLimitContentStatus = make(map[int32]QuestLimitContentStatus) } @@ -590,17 +586,6 @@ type SideStoryActiveProgress struct { LatestVersion int64 } -type SeasonRouteKey struct { - MainQuestSeasonId int32 - MainQuestRouteId int32 -} - -type SeasonRouteEntry struct { - MainQuestSeasonId int32 - MainQuestRouteId int32 - LatestVersion int64 -} - type QuestLimitContentStatus struct { LimitContentQuestStatusType int32 EventQuestChapterId int32 diff --git a/server/internal/userdata/changed_tables.go b/server/internal/userdata/changed_tables.go index 0e89dff..4ac58a9 100644 --- a/server/internal/userdata/changed_tables.go +++ b/server/internal/userdata/changed_tables.go @@ -101,8 +101,9 @@ func ChangedTables(before, after *store.UserState) []string { add("IUserMainQuestMainFlowStatus") add("IUserMainQuestProgressStatus") add("IUserMainQuestReplayFlowStatus") - } - if !mapsEqualStruct(before.MainQuestSeasonRoutes, after.MainQuestSeasonRoutes) { + // IUserMainQuestSeasonRoute is derived from MainQuest + Quests at projection + // time (see proj_quest.go / questflow.QuestHandler.SeasonRoutesFor) — flag it + // whenever either of those upstream inputs changes. add("IUserMainQuestSeasonRoute") } if before.EventQuest != after.EventQuest { @@ -202,6 +203,7 @@ func ChangedTables(before, after *store.UserState) []string { } if !mapsEqualStruct(before.Quests, after.Quests) { add("IUserQuest") + add("IUserMainQuestSeasonRoute") } if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) { add("IUserQuestMission") diff --git a/server/internal/userdata/proj_quest.go b/server/internal/userdata/proj_quest.go index 68e38fc..3c74105 100644 --- a/server/internal/userdata/proj_quest.go +++ b/server/internal/userdata/proj_quest.go @@ -116,38 +116,29 @@ func init() { return s }) register("IUserMainQuestSeasonRoute", func(user store.UserState) string { - if len(user.MainQuestSeasonRoutes) == 0 { - // Fallback to current (season, route) for legacy saves with no history. - s, _ := utils.EncodeJSONMaps(map[string]any{ - "userId": user.UserId, - "mainQuestSeasonId": user.MainQuest.MainQuestSeasonId, - "mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId, - "latestVersion": user.MainQuest.LatestVersion, - }) - return s + if questHandler == nil { + return "[]" } - keys := make([]store.SeasonRouteKey, 0, len(user.MainQuestSeasonRoutes)) - for k := range user.MainQuestSeasonRoutes { - keys = append(keys, k) + pairs := questHandler.SeasonRoutesFor(&user) + if len(pairs) == 0 { + return "[]" } - 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] + seasons := make([]int32, 0, len(pairs)) + for s := range pairs { + seasons = append(seasons, s) + } + sort.Slice(seasons, func(i, j int) bool { return seasons[i] < seasons[j] }) + records := make([]map[string]any, 0, len(seasons)) + for _, s := range seasons { records = append(records, map[string]any{ "userId": user.UserId, - "mainQuestSeasonId": e.MainQuestSeasonId, - "mainQuestRouteId": e.MainQuestRouteId, - "latestVersion": e.LatestVersion, + "mainQuestSeasonId": s, + "mainQuestRouteId": pairs[s], + "latestVersion": user.MainQuest.LatestVersion, }) } - s, _ := utils.EncodeJSONMaps(records...) - return s + out, _ := utils.EncodeJSONMaps(records...) + return out }) register("IUserEventQuestProgressStatus", func(user store.UserState) string { s, _ := utils.EncodeJSONMaps(map[string]any{ diff --git a/server/internal/userdata/projector.go b/server/internal/userdata/projector.go index ee9b60b..10efb18 100644 --- a/server/internal/userdata/projector.go +++ b/server/internal/userdata/projector.go @@ -3,6 +3,7 @@ package userdata import ( "sort" + "lunar-tear/server/internal/questflow" "lunar-tear/server/internal/store" ) @@ -10,6 +11,12 @@ type Projector func(user store.UserState) string var projectors = make(map[string]Projector) +var questHandler *questflow.QuestHandler + +func SetQuestHandler(h *questflow.QuestHandler) { + questHandler = h +} + func register(tableName string, fn Projector) { projectors[tableName] = fn }