Derive main-quest season routes at projection time

This commit is contained in:
Ilya Groshev
2026-05-22 17:24:30 +03:00
parent ef69c54949
commit 810adcf990
11 changed files with 101 additions and 96 deletions
+1
View File
@@ -40,6 +40,7 @@ var childTables = []string{
"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",
+49
View File
@@ -34,6 +34,8 @@ type QuestCatalog struct {
TutorialUnlockConditions []EntityMTutorialUnlockCondition TutorialUnlockConditions []EntityMTutorialUnlockCondition
ChapterLastSceneByQuestId map[int32]int32 ChapterLastSceneByQuestId map[int32]int32
SeasonIdByRouteId map[int32]int32 SeasonIdByRouteId map[int32]int32
RoutesBySeason map[int32][]int32
RouteCompletionQuestId map[int32]int32
BattleOnlyTargetSceneByQuestId map[int32]int32 BattleOnlyTargetSceneByQuestId map[int32]int32
UserExpThresholds []int32 UserExpThresholds []int32
@@ -114,8 +116,53 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
return nil, fmt.Errorf("load main quest route table: %w", err) return nil, fmt.Errorf("load main quest route table: %w", err)
} }
seasonIdByRouteId := make(map[int32]int32, len(routes)) 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 { for _, r := range routes {
seasonIdByRouteId[r.MainQuestRouteId] = r.MainQuestSeasonId 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") firstClearSwitches, err := utils.ReadTable[EntityMQuestFirstClearRewardSwitch]("m_quest_first_clear_reward_switch")
@@ -539,6 +586,8 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
TutorialUnlockConditions: tutorialUnlockConds, TutorialUnlockConditions: tutorialUnlockConds,
ChapterLastSceneByQuestId: chapterLastSceneByQuestId, ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
SeasonIdByRouteId: seasonIdByRouteId, SeasonIdByRouteId: seasonIdByRouteId,
RoutesBySeason: routesBySeason,
RouteCompletionQuestId: routeCompletionQuestId,
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId, BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
UserExpThresholds: BuildExpThresholds(paramMapRows, 1), UserExpThresholds: BuildExpThresholds(paramMapRows, 1),
+19 -22
View File
@@ -46,7 +46,6 @@ 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())
} }
} }
} }
@@ -59,22 +58,27 @@ func (h *QuestHandler) advanceReplayFlowScene(user *store.UserState, sceneId int
user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId user.MainQuest.ReplayFlowHeadQuestSceneId = sceneId
} }
func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) { func (h *QuestHandler) SeasonRoutesFor(user *store.UserState) map[int32]int32 {
if seasonId <= 0 || routeId <= 0 { out := make(map[int32]int32)
return for seasonId, routes := range h.RoutesBySeason {
if seasonId <= 1 {
continue
} }
if user.MainQuestSeasonRoutes == nil { for _, routeId := range routes {
user.MainQuestSeasonRoutes = make(map[store.SeasonRouteKey]store.SeasonRouteEntry) finalQuestId, ok := h.RouteCompletionQuestId[routeId]
if !ok {
continue
} }
key := store.SeasonRouteKey{MainQuestSeasonId: seasonId, MainQuestRouteId: routeId} if q, ok := user.Quests[finalQuestId]; ok && q.ClearCount > 0 {
if _, exists := user.MainQuestSeasonRoutes[key]; exists { out[seasonId] = routeId
return break
} }
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) { 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 { if !ok {
return model.QuestFlowTypeReplayFlow return model.QuestFlowTypeReplayFlow
} }
for key, entry := range user.MainQuestSeasonRoutes { pairs := h.SeasonRoutesFor(user)
if key.MainQuestSeasonId == seasonId && entry.MainQuestRouteId != routeId { if recorded, ok := pairs[seasonId]; ok && recorded != routeId {
return model.QuestFlowTypeAnotherRouteReplayFlow
}
}
if len(user.MainQuestSeasonRoutes) == 0 &&
user.MainQuest.MainQuestSeasonId == seasonId &&
user.MainQuest.CurrentMainQuestRouteId != 0 &&
user.MainQuest.CurrentMainQuestRouteId != routeId {
return model.QuestFlowTypeAnotherRouteReplayFlow return model.QuestFlowTypeAnotherRouteReplayFlow
} }
return model.QuestFlowTypeReplayFlow return model.QuestFlowTypeReplayFlow
+2
View File
@@ -8,6 +8,7 @@ import (
"lunar-tear/server/internal/masterdata" "lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/masterdata/memorydb" "lunar-tear/server/internal/masterdata/memorydb"
"lunar-tear/server/internal/questflow" "lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/userdata"
) )
// buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever // buildCatalogs runs the full Load*/Build*/Enrich* sequence against whatever
@@ -35,6 +36,7 @@ func buildCatalogs() (*Catalogs, error) {
} }
sideStoryCatalog := masterdata.LoadSideStoryCatalog() sideStoryCatalog := masterdata.LoadSideStoryCatalog()
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog) questHandler := questflow.NewQuestHandler(questCatalog, gameConfig, sideStoryCatalog)
userdata.SetQuestHandler(questHandler)
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog() gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
if err != nil { if err != nil {
-1
View File
@@ -198,7 +198,6 @@ 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
-11
View File
@@ -86,7 +86,6 @@ 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)
@@ -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 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
-17
View File
@@ -224,12 +224,6 @@ 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 {
@@ -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} 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}
-15
View File
@@ -41,7 +41,6 @@ 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
@@ -162,9 +161,6 @@ 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)
} }
@@ -590,17 +586,6 @@ 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
+4 -2
View File
@@ -101,8 +101,9 @@ func ChangedTables(before, after *store.UserState) []string {
add("IUserMainQuestMainFlowStatus") add("IUserMainQuestMainFlowStatus")
add("IUserMainQuestProgressStatus") add("IUserMainQuestProgressStatus")
add("IUserMainQuestReplayFlowStatus") add("IUserMainQuestReplayFlowStatus")
} // IUserMainQuestSeasonRoute is derived from MainQuest + Quests at projection
if !mapsEqualStruct(before.MainQuestSeasonRoutes, after.MainQuestSeasonRoutes) { // time (see proj_quest.go / questflow.QuestHandler.SeasonRoutesFor) — flag it
// whenever either of those upstream inputs changes.
add("IUserMainQuestSeasonRoute") add("IUserMainQuestSeasonRoute")
} }
if before.EventQuest != after.EventQuest { if before.EventQuest != after.EventQuest {
@@ -202,6 +203,7 @@ func ChangedTables(before, after *store.UserState) []string {
} }
if !mapsEqualStruct(before.Quests, after.Quests) { if !mapsEqualStruct(before.Quests, after.Quests) {
add("IUserQuest") add("IUserQuest")
add("IUserMainQuestSeasonRoute")
} }
if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) { if !mapsEqualStruct(before.QuestMissions, after.QuestMissions) {
add("IUserQuestMission") add("IUserQuestMission")
+16 -25
View File
@@ -116,38 +116,29 @@ func init() {
return s return s
}) })
register("IUserMainQuestSeasonRoute", func(user store.UserState) string { register("IUserMainQuestSeasonRoute", func(user store.UserState) string {
if len(user.MainQuestSeasonRoutes) == 0 { if questHandler == nil {
// Fallback to current (season, route) for legacy saves with no history. return "[]"
s, _ := utils.EncodeJSONMaps(map[string]any{
"userId": user.UserId,
"mainQuestSeasonId": user.MainQuest.MainQuestSeasonId,
"mainQuestRouteId": user.MainQuest.CurrentMainQuestRouteId,
"latestVersion": user.MainQuest.LatestVersion,
})
return s
} }
keys := make([]store.SeasonRouteKey, 0, len(user.MainQuestSeasonRoutes)) pairs := questHandler.SeasonRoutesFor(&user)
for k := range user.MainQuestSeasonRoutes { if len(pairs) == 0 {
keys = append(keys, k) return "[]"
} }
sort.Slice(keys, func(i, j int) bool { seasons := make([]int32, 0, len(pairs))
if keys[i].MainQuestSeasonId != keys[j].MainQuestSeasonId { for s := range pairs {
return keys[i].MainQuestSeasonId < keys[j].MainQuestSeasonId seasons = append(seasons, s)
} }
return keys[i].MainQuestRouteId < keys[j].MainQuestRouteId sort.Slice(seasons, func(i, j int) bool { return seasons[i] < seasons[j] })
}) records := make([]map[string]any, 0, len(seasons))
records := make([]map[string]any, 0, len(keys)) for _, s := range seasons {
for _, k := range keys {
e := user.MainQuestSeasonRoutes[k]
records = append(records, map[string]any{ records = append(records, map[string]any{
"userId": user.UserId, "userId": user.UserId,
"mainQuestSeasonId": e.MainQuestSeasonId, "mainQuestSeasonId": s,
"mainQuestRouteId": e.MainQuestRouteId, "mainQuestRouteId": pairs[s],
"latestVersion": e.LatestVersion, "latestVersion": user.MainQuest.LatestVersion,
}) })
} }
s, _ := utils.EncodeJSONMaps(records...) out, _ := utils.EncodeJSONMaps(records...)
return s return out
}) })
register("IUserEventQuestProgressStatus", func(user store.UserState) string { register("IUserEventQuestProgressStatus", func(user store.UserState) string {
s, _ := utils.EncodeJSONMaps(map[string]any{ s, _ := utils.EncodeJSONMaps(map[string]any{
+7
View File
@@ -3,6 +3,7 @@ package userdata
import ( import (
"sort" "sort"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/store" "lunar-tear/server/internal/store"
) )
@@ -10,6 +11,12 @@ type Projector func(user store.UserState) string
var projectors = make(map[string]Projector) var projectors = make(map[string]Projector)
var questHandler *questflow.QuestHandler
func SetQuestHandler(h *questflow.QuestHandler) {
questHandler = h
}
func register(tableName string, fn Projector) { func register(tableName string, fn Projector) {
projectors[tableName] = fn projectors[tableName] = fn
} }