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
|
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
|
||||||
@@ -62,7 +34,6 @@ 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
|
QuestsWithDifficulty map[int32]bool // any questId referenced in m_quest_relation_main_flow
|
||||||
BattleOnlyTargetSceneByQuestId map[int32]int32
|
BattleOnlyTargetSceneByQuestId map[int32]int32
|
||||||
|
|
||||||
@@ -148,22 +119,6 @@ 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)
|
||||||
@@ -600,7 +555,6 @@ func LoadQuestCatalog(partsCatalog *PartsCatalog) (*QuestCatalog, error) {
|
|||||||
TutorialUnlockConditions: tutorialUnlockConds,
|
TutorialUnlockConditions: tutorialUnlockConds,
|
||||||
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
ChapterLastSceneByQuestId: chapterLastSceneByQuestId,
|
||||||
SeasonIdByRouteId: seasonIdByRouteId,
|
SeasonIdByRouteId: seasonIdByRouteId,
|
||||||
OrderedSeasonRoutes: orderedSeasonRoutes,
|
|
||||||
QuestsWithDifficulty: questsWithDifficulty,
|
QuestsWithDifficulty: questsWithDifficulty,
|
||||||
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
BattleOnlyTargetSceneByQuestId: battleOnlyTargetSceneByQuestId,
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ const (
|
|||||||
QuestFlowTypeAnotherRouteReplayFlow QuestFlowType = 4
|
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 {
|
func (t QuestFlowType) String() string {
|
||||||
switch t {
|
switch t {
|
||||||
case QuestFlowTypeUnknown:
|
case QuestFlowTypeUnknown:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (h *QuestHandler) HandleBigHuntQuestFinish(user *store.UserState, questId i
|
|||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func (h *QuestHandler) HandleEventQuestFinish(user *store.UserState, eventQuestC
|
|||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(user, questId)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
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) {
|
func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||||
if !ok {
|
|
||||||
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
log.Printf("[HandleEventQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -84,8 +83,4 @@ func (h *QuestHandler) HandleEventQuestSceneProgress(user *store.UserState, ques
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
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)
|
outcome := h.evaluateFinishOutcome(user, questId)
|
||||||
if !isRetired {
|
if !isRetired {
|
||||||
h.applyQuestVictory(user, questId, &outcome, nowMillis)
|
h.applyQuestVictory(user, questId, &outcome, nowMillis, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isRetired && !isAnnihilated && quest.Stamina > 1 {
|
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) {
|
func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
scene, ok := h.SceneById[questSceneId]
|
if _, ok := h.SceneById[questSceneId]; !ok {
|
||||||
if !ok {
|
|
||||||
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
log.Printf("[HandleExtraQuestSceneProgress] unknown sceneId=%d, skipping", questSceneId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -79,8 +78,4 @@ func (h *QuestHandler) HandleExtraQuestSceneProgress(user *store.UserState, ques
|
|||||||
}
|
}
|
||||||
|
|
||||||
h.applySceneGrants(user, questSceneId, nowMillis)
|
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",
|
log.Printf("[HandleQuestStart] QuestMenuPick quest=%d isBattleOnly=%v scene=%d cleared=%v",
|
||||||
questId, isBattleOnly, sceneId, isCleared)
|
questId, isBattleOnly, sceneId, isCleared)
|
||||||
|
|
||||||
case isCleared && isReplayFlow:
|
case isReplayFlow:
|
||||||
snapshotMainQuestIfNeeded(user)
|
h.applyReplayStart(user, questId, isBattleOnly, nowMillis)
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeReplayFlow)
|
return
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = 0
|
|
||||||
user.MainQuest.ReplayFlowHeadQuestSceneId = 0
|
|
||||||
user.MainQuest.LatestVersion = nowMillis
|
|
||||||
log.Printf("[HandleQuestStart] MapPlay quest=%d isBattleOnly=%v", questId, isBattleOnly)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCleared {
|
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 {
|
func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
||||||
if isBattleOnly {
|
if isBattleOnly {
|
||||||
if v, ok := h.BattleOnlyTargetSceneIdFor(questId); ok {
|
if v, ok := h.BattleOnlyTargetSceneIdFor(questId); ok {
|
||||||
@@ -141,14 +158,24 @@ func (h *QuestHandler) menuPickSceneId(questId int32, isBattleOnly bool) int32 {
|
|||||||
return 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, wasReplay bool) {
|
||||||
questState := user.Quests[questId]
|
questState := user.Quests[questId]
|
||||||
if !questState.IsRewardGranted {
|
if !questState.IsRewardGranted {
|
||||||
h.applyQuestRewards(user, questId, nowMillis)
|
h.applyExpAndGoldRewards(user, questId, nowMillis)
|
||||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
if !wasReplay {
|
||||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeHalfResult, nowMillis)...)
|
h.applyFirstClearItemRewards(user, questId, nowMillis)
|
||||||
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
outcome.ChangedWeaponStoryIds = append(outcome.ChangedWeaponStoryIds,
|
||||||
h.grantWeaponStoryUnlocksForQuestScene(user, questId, model.QuestResultTypeFullResult, nowMillis)...)
|
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
|
questState.IsRewardGranted = true
|
||||||
}
|
}
|
||||||
for _, drop := range outcome.DropRewards {
|
for _, drop := range outcome.DropRewards {
|
||||||
@@ -197,13 +224,13 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
h.initQuestState(user, questId)
|
h.initQuestState(user, questId)
|
||||||
|
|
||||||
outcome := h.evaluateFinishOutcome(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
|
wasMenuReplay := user.MainQuest.SavedContext.Active
|
||||||
|
|
||||||
if !isRetired {
|
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)
|
lastSceneId := h.getLastMainFlowSceneId(questId)
|
||||||
h.advanceMainFlowScene(user, questId, lastSceneId)
|
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.ProgressQuestSceneId = 0
|
||||||
user.MainQuest.ProgressHeadQuestSceneId = 0
|
user.MainQuest.ProgressHeadQuestSceneId = 0
|
||||||
user.MainQuest.ProgressQuestFlowType = 0
|
if !wasReplay {
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeMainFlow)
|
// 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.QuestFlowTypeUnknown)
|
||||||
|
}
|
||||||
|
|
||||||
if wasMenuReplay {
|
if wasMenuReplay {
|
||||||
ctx := user.MainQuest.SavedContext
|
ctx := user.MainQuest.SavedContext
|
||||||
@@ -248,12 +279,6 @@ func (h *QuestHandler) HandleQuestFinish(user *store.UserState, questId int32, i
|
|||||||
ctx.CurrentQuestSceneId, ctx.HeadQuestSceneId, ctx.PortalCageInProgress)
|
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)
|
h.clearQuestMissions(user, questId, nowMillis)
|
||||||
|
|
||||||
return outcome
|
return outcome
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
panic(fmt.Sprintf("unknown questId=%d for evaluateFinishOutcome", questId))
|
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)
|
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||||
outcome.FirstClearRewards = append(outcome.FirstClearRewards, RewardGrant{
|
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] {
|
for _, reward := range h.ReplayFlowRewardsByGroupId[questDef.QuestReplayFlowRewardGroupId] {
|
||||||
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
outcome.ReplayFlowFirstClearRewards = append(outcome.ReplayFlowFirstClearRewards, RewardGrant{
|
||||||
PossessionType: model.PossessionType(reward.PossessionType),
|
PossessionType: model.PossessionType(reward.PossessionType),
|
||||||
@@ -72,48 +74,53 @@ func (h *QuestHandler) evaluateFinishOutcome(user *store.UserState, questId int3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pendingClearCount := 0
|
// Mission rewards / BigWin are first-clear concepts. Reference
|
||||||
regularMissionCount := 0
|
// IUserQuestMissionTable has no rows for replay-variant ids (30000+):
|
||||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
// the popup is empty on replay in the original game.
|
||||||
missionDef, ok := h.MissionById[questMissionId]
|
if !isReplay {
|
||||||
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) == model.QuestMissionConditionTypeComplete {
|
pendingClearCount := 0
|
||||||
continue
|
regularMissionCount := 0
|
||||||
}
|
|
||||||
regularMissionCount++
|
|
||||||
|
|
||||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
|
||||||
mission := user.QuestMissions[key]
|
|
||||||
|
|
||||||
if !mission.IsClear {
|
|
||||||
pendingClearCount++
|
|
||||||
outcome.MissionClearRewards = appendMissionRewards(
|
|
||||||
outcome.MissionClearRewards,
|
|
||||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
priorClearCount := regularMissionCount - pendingClearCount
|
|
||||||
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
|
|
||||||
// always equals regularMissionCount. The two-variable form is kept to mirror the
|
|
||||||
// original game's intent where individual missions could fail their conditions.
|
|
||||||
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
|
|
||||||
if allRegularWillClear {
|
|
||||||
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||||
missionDef, ok := h.MissionById[questMissionId]
|
missionDef, ok := h.MissionById[questMissionId]
|
||||||
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) != model.QuestMissionConditionTypeComplete {
|
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) == model.QuestMissionConditionTypeComplete {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
regularMissionCount++
|
||||||
|
|
||||||
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||||
if !user.QuestMissions[key].IsClear {
|
mission := user.QuestMissions[key]
|
||||||
outcome.MissionClearCompleteRewards = appendMissionRewards(
|
|
||||||
outcome.MissionClearCompleteRewards,
|
if !mission.IsClear {
|
||||||
|
pendingClearCount++
|
||||||
|
outcome.MissionClearRewards = appendMissionRewards(
|
||||||
|
outcome.MissionClearRewards,
|
||||||
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||||
)
|
)
|
||||||
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
|
||||||
|
priorClearCount := regularMissionCount - pendingClearCount
|
||||||
|
// On our server every mission auto-clears, so priorClearCount + pendingClearCount
|
||||||
|
// always equals regularMissionCount. The two-variable form is kept to mirror the
|
||||||
|
// original game's intent where individual missions could fail their conditions.
|
||||||
|
allRegularWillClear := regularMissionCount > 0 && (priorClearCount+pendingClearCount) == regularMissionCount
|
||||||
|
if allRegularWillClear {
|
||||||
|
for _, questMissionId := range h.MissionIdsByQuestId[questId] {
|
||||||
|
missionDef, ok := h.MissionById[questMissionId]
|
||||||
|
if !ok || model.QuestMissionConditionType(missionDef.QuestMissionConditionType) != model.QuestMissionConditionTypeComplete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := store.QuestMissionKey{QuestId: questId, QuestMissionId: questMissionId}
|
||||||
|
if !user.QuestMissions[key].IsClear {
|
||||||
|
outcome.MissionClearCompleteRewards = appendMissionRewards(
|
||||||
|
outcome.MissionClearCompleteRewards,
|
||||||
|
h.MissionRewardsByMissionId[missionDef.QuestMissionRewardId],
|
||||||
|
)
|
||||||
|
outcome.BigWinClearedQuestMissionIds = append(outcome.BigWinClearedQuestMissionIds, questMissionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outcome.IsBigWin = len(outcome.BigWinClearedQuestMissionIds) > 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outcome.DropRewards = h.computeDropRewards(questDef)
|
outcome.DropRewards = h.computeDropRewards(questDef)
|
||||||
@@ -240,7 +247,7 @@ func (h *QuestHandler) resolveDeckUnits(user *store.UserState, questId int32) (c
|
|||||||
return costumeUuids, characterIds
|
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]
|
questDef, ok := h.QuestById[questId]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -252,13 +259,24 @@ func (h *QuestHandler) applyQuestRewards(user *store.UserState, questId int32, n
|
|||||||
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
user.ConsumableItems[h.Config.ConsumableItemIdForGold] += questDef.Gold
|
||||||
log.Printf("[applyQuestRewards] questId=%d gold: +%d -> total=%d", questId, questDef.Gold, user.ConsumableItems[h.Config.ConsumableItemIdForGold])
|
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)
|
rewardGroupId := h.firstClearRewardGroupId(user, questDef)
|
||||||
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
for _, reward := range h.FirstClearRewardsByGroupId[rewardGroupId] {
|
||||||
h.applyRewardPossession(user, model.PossessionType(reward.PossessionType), reward.PossessionId, reward.Count, nowMillis)
|
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) {
|
func (h *QuestHandler) applyRewardPossession(user *store.UserState, possType model.PossessionType, possId, count int32, nowMillis int64) {
|
||||||
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
h.Granter.GrantFull(user, possType, possId, count, nowMillis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
package questflow
|
package questflow
|
||||||
|
|
||||||
|
// MainQuest scene-field families mirror three client entity tables:
|
||||||
|
//
|
||||||
|
// MainFlow* — EntityIUserMainQuestMainFlowStatus (#11443)
|
||||||
|
// Progress* — EntityIUserMainQuestProgressStatus (#11444)
|
||||||
|
// ReplayFlow* — EntityIUserMainQuestReplayFlowStatus (#11445)
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"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,
|
// Backs IUserMainQuestSeasonRoute: the client needs the history to load
|
||||||
// bumping LatestVersion on first insert. The history backs IUserMainQuestSeasonRoute,
|
// scene metadata when cage menu-replay jumps to older chapters.
|
||||||
// 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) {
|
func RecordSeasonRoute(user *store.UserState, seasonId, routeId int32, nowMillis int64) {
|
||||||
if seasonId <= 0 || routeId <= 0 {
|
if seasonId <= 0 || routeId <= 0 {
|
||||||
return
|
return
|
||||||
@@ -133,11 +137,48 @@ func (h *QuestHandler) getChapterLastSceneId(questId int32) int32 {
|
|||||||
|
|
||||||
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
func (h *QuestHandler) HandleReplayFlowSceneProgress(user *store.UserState, questSceneId int32, nowMillis int64) {
|
||||||
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
user.MainQuest.ReplayFlowCurrentQuestSceneId = questSceneId
|
||||||
if user.MainQuest.ReplayFlowHeadQuestSceneId == 0 || h.isSceneAhead(questSceneId, user.MainQuest.ReplayFlowHeadQuestSceneId) {
|
user.MainQuest.ReplayFlowHeadQuestSceneId = questSceneId
|
||||||
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
|
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) {
|
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 prevSceneId := user.MainQuest.ProgressQuestSceneId; prevSceneId != 0 {
|
||||||
if prevScene, ok := h.SceneById[prevSceneId]; ok && prevScene.QuestId != quest.QuestId {
|
if prevScene, ok := h.SceneById[prevSceneId]; ok && prevScene.QuestId != quest.QuestId {
|
||||||
h.finalizeChainPreviousQuest(user, prevScene.QuestId, gametime.NowMillis())
|
// 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) {
|
isReplay := model.IsReplayQuestFlowType(user.MainQuest.CurrentQuestFlowType)
|
||||||
if model.QuestResultType(scene.QuestResultType) == model.QuestResultTypeHalfResult {
|
|
||||||
nowMillis := gametime.NowMillis()
|
|
||||||
h.clearQuestMissions(user, quest.QuestId, nowMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if isMainQuestPlayable(quest) {
|
||||||
user.MainQuest.ProgressQuestSceneId = questSceneId
|
user.MainQuest.ProgressQuestSceneId = questSceneId
|
||||||
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
if h.isSceneAhead(questSceneId, user.MainQuest.ProgressHeadQuestSceneId) {
|
||||||
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
user.MainQuest.ProgressHeadQuestSceneId = questSceneId
|
||||||
}
|
}
|
||||||
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
if isReplay {
|
||||||
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
user.MainQuest.ProgressQuestFlowType = user.MainQuest.CurrentQuestFlowType
|
||||||
|
} else {
|
||||||
|
user.MainQuest.CurrentQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||||
|
user.MainQuest.ProgressQuestFlowType = int32(model.QuestFlowTypeSubFlow)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
user.MainQuest.CurrentQuestSceneId = questSceneId
|
user.MainQuest.CurrentQuestSceneId = questSceneId
|
||||||
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
if h.isSceneAhead(questSceneId, user.MainQuest.HeadQuestSceneId) {
|
||||||
@@ -177,4 +223,12 @@ func (h *QuestHandler) HandleMainQuestSceneProgress(user *store.UserState, quest
|
|||||||
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
lastSceneId := h.getChapterLastSceneId(quest.QuestId)
|
||||||
user.MainQuest.IsReachedLastQuestScene = questSceneId == lastSceneId
|
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"
|
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/store"
|
"lunar-tear/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +28,12 @@ func (s *PortalCageServiceServer) UpdatePortalCageSceneProgress(ctx context.Cont
|
|||||||
now := gametime.NowMillis()
|
now := gametime.NowMillis()
|
||||||
user.PortalCageStatus.IsCurrentProgress = true
|
user.PortalCageStatus.IsCurrentProgress = true
|
||||||
user.PortalCageStatus.LatestVersion = now
|
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
|
return &pb.UpdatePortalCageSceneProgressResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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/runtime"
|
||||||
"lunar-tear/server/internal/store"
|
"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)
|
userId := CurrentUserId(ctx, s.users, s.sessions)
|
||||||
s.users.UpdateUser(userId, func(user *store.UserState) {
|
s.users.UpdateUser(userId, func(user *store.UserState) {
|
||||||
now := gametime.NowMillis()
|
user.GameStartDatetime = 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
|
||||||
|
|||||||
@@ -537,7 +537,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.DokanConfirmed[id] = true
|
u.DokanConfirmed[id] = true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gifts
|
|
||||||
queryRows(db, `SELECT user_gift_uuid, is_received, possession_type, possession_id, count, grant_datetime,
|
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
|
description_gift_text_id, equipment_data, expiration_datetime, received_datetime
|
||||||
FROM user_gifts WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
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,
|
queryRows(db, `SELECT consumable_item_id, count FROM user_gacha_converted_medals WHERE user_id=? ORDER BY ordinal`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
var v store.ConsumableItemState
|
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)
|
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
|
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) {
|
FROM user_gacha_banners WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var v store.GachaBannerState
|
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,
|
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,
|
panel_release_bit4, latest_version FROM user_character_boards WHERE user_id=?`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
@@ -642,7 +638,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.ShopReplaceableLineup[v.SlotNumber] = v
|
u.ShopReplaceableLineup[v.SlotNumber] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
// Gimmick tables
|
|
||||||
queryRows(db, `SELECT gimmick_sequence_schedule_id, gimmick_sequence_id, gimmick_id,
|
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,
|
is_gimmick_cleared, start_datetime, latest_version FROM user_gimmick_progress WHERE user_id=?`, uid,
|
||||||
func(rows *sql.Rows) {
|
func(rows *sql.Rows) {
|
||||||
@@ -685,7 +680,6 @@ func loadMapTables(db *sql.DB, uid int64, u *store.UserState) {
|
|||||||
u.Gimmick.Unlocks[v.Key] = v
|
u.Gimmick.Unlocks[v.Key] = v
|
||||||
})
|
})
|
||||||
|
|
||||||
// Big hunt maps
|
|
||||||
queryRows(db, `SELECT big_hunt_boss_id, max_score, max_score_update_datetime, latest_version
|
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) {
|
FROM user_big_hunt_max_scores WHERE user_id=?`, uid, func(rows *sql.Rows) {
|
||||||
var id int32
|
var id int32
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ func boolToInt(b bool) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeUserState inserts all child table rows for a newly created user.
|
// Precondition: the users row must already exist.
|
||||||
// The users row must already exist.
|
|
||||||
func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
||||||
exec := func(query string, args ...any) error {
|
exec := func(query string, args ...any) error {
|
||||||
_, err := tx.Exec(query, args...)
|
_, err := tx.Exec(query, args...)
|
||||||
@@ -123,7 +122,6 @@ func writeUserState(tx *sql.Tx, uid int64, u *store.UserState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map tables
|
|
||||||
for _, v := range u.Characters {
|
for _, v := range u.Characters {
|
||||||
if err := exec(`INSERT INTO user_characters (user_id, character_id, level, exp, latest_version) VALUES (?,?,?,?,?)`,
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// diffAndSave compares before/after UserState and writes only changed rows.
|
// 1:1 tables update on field-change; maps INSERT OR REPLACE + DELETE; slice tables DELETE-all then INSERT-all.
|
||||||
// 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.
|
|
||||||
func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
func diffAndSave(tx *sql.Tx, uid int64, before, after *store.UserState) error {
|
||||||
exec := func(query string, args ...any) error {
|
exec := func(query string, args ...any) error {
|
||||||
_, err := tx.Exec(query, args...)
|
_, err := tx.Exec(query, args...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// users table
|
|
||||||
if before.PlayerId != after.PlayerId || before.OsType != after.OsType || before.PlatformType != after.PlatformType ||
|
if before.PlayerId != after.PlayerId || before.OsType != after.OsType || before.PlatformType != after.PlatformType ||
|
||||||
before.UserRestrictionType != after.UserRestrictionType || before.RegisterDatetime != after.RegisterDatetime ||
|
before.UserRestrictionType != after.UserRestrictionType || before.RegisterDatetime != after.RegisterDatetime ||
|
||||||
before.GameStartDatetime != after.GameStartDatetime || before.LatestVersion != after.LatestVersion ||
|
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 ||
|
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 {
|
before.Gacha.DailyMaxCount != after.Gacha.DailyMaxCount || before.Gacha.LastRewardDrawDate != after.Gacha.LastRewardDrawDate {
|
||||||
var obtainItemId, obtainCount sql.NullInt64
|
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",
|
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} },
|
func(v store.CharacterState) []any { return []any{v.CharacterId, v.Level, v.Exp, v.LatestVersion} },
|
||||||
"character_id, level, exp, latest_version")
|
"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}
|
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")
|
}, "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 {
|
for k, v := range after.Decks {
|
||||||
if old, ok := before.Decks[k]; !ok || old != v {
|
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 (?,?,?,?,?,?,?,?,?)`),
|
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) {
|
replaceSliceTable(tx, uid, "user_deck_sub_weapons", after.DeckSubWeapons, func(key string, uuids []string) {
|
||||||
for i, uuid := range uuids {
|
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)
|
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}
|
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_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 {
|
for k, v := range after.QuestMissions {
|
||||||
if old, ok := before.QuestMissions[k]; !ok || old != v {
|
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 (?,?,?,?,?,?,?)`,
|
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}
|
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_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)
|
exec(`DELETE FROM user_weapon_skills WHERE user_id=?`, uid)
|
||||||
for _, skills := range after.WeaponSkills {
|
for _, skills := range after.WeaponSkills {
|
||||||
for _, v := range skills {
|
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}
|
return []any{v.UserCostumeUuid, v.Level, v.AcquisitionDatetime, v.LatestVersion}
|
||||||
}, "user_costume_uuid, level, acquisition_datetime, latest_version")
|
}, "user_costume_uuid, level, acquisition_datetime, latest_version")
|
||||||
|
|
||||||
// Costume awaken status ups (composite key)
|
|
||||||
for k, v := range after.CostumeAwakenStatusUps {
|
for k, v := range after.CostumeAwakenStatusUps {
|
||||||
if old, ok := before.CostumeAwakenStatusUps[k]; !ok || old != v {
|
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 (?,?,?,?,?,?,?,?,?,?)`,
|
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 {
|
for k, v := range after.DeckTypeNotes {
|
||||||
if old, ok := before.DeckTypeNotes[k]; !ok || old != v {
|
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 (?,?,?,?)`,
|
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")
|
diffTimestampMap(tx, uid, before.DrawnOmikuji, after.DrawnOmikuji, "user_drawn_omikuji", "omikuji_id")
|
||||||
diffBoolMap(tx, uid, before.DokanConfirmed, after.DokanConfirmed, "user_dokan_confirmed", "dokan_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)
|
exec(`DELETE FROM user_gifts WHERE user_id=?`, uid)
|
||||||
for _, g := range after.Gifts.NotReceived {
|
for _, g := range after.Gifts.NotReceived {
|
||||||
var expDt sql.NullInt64
|
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)
|
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)
|
exec(`DELETE FROM user_gacha_converted_medals WHERE user_id=?`, uid)
|
||||||
for i, v := range after.Gacha.ConvertedGachaMedal.ConvertedMedalPossession {
|
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)
|
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 {
|
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 {
|
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 (?,?,?,?,?,?,?)`,
|
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)
|
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)
|
exec(`DELETE FROM user_gacha_banner_box_drew_counts WHERE user_id=? AND gacha_id=?`, uid, id)
|
||||||
for itemId, count := range v.BoxDrewCounts {
|
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)
|
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}
|
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_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 {
|
for k, v := range after.CharacterBoardAbilities {
|
||||||
if old, ok := before.CharacterBoardAbilities[k]; !ok || old != v {
|
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 (?,?,?,?,?)`,
|
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 {
|
for k, v := range after.CharacterBoardStatusUps {
|
||||||
if old, ok := before.CharacterBoardStatusUps[k]; !ok || old != v {
|
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 (?,?,?,?,?,?,?,?,?,?)`,
|
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")
|
"slot_number, shop_item_id, latest_version")
|
||||||
|
|
||||||
// Gimmick tables (composite keys)
|
|
||||||
for k, v := range after.Gimmick.Progress {
|
for k, v := range after.Gimmick.Progress {
|
||||||
if old, ok := before.Gimmick.Progress[k]; !ok || old != v {
|
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 (?,?,?,?,?,?,?)`,
|
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",
|
diffMapInt32(tx, uid, before.BigHuntMaxScores, after.BigHuntMaxScores, "user_big_hunt_max_scores", "big_hunt_boss_id",
|
||||||
func(v store.BigHuntMaxScore) []any {
|
func(v store.BigHuntMaxScore) []any {
|
||||||
return []any{0, v.MaxScore, v.MaxScoreUpdateDatetime, v.LatestVersion}
|
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
|
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) {
|
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 {
|
for k, v := range after {
|
||||||
if old, ok := before[k]; !ok || old != v {
|
if old, ok := before[k]; !ok || old != v {
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ func init() {
|
|||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
register("IUserMainQuestReplayFlowStatus", func(user store.UserState) string {
|
register("IUserMainQuestReplayFlowStatus", func(user store.UserState) string {
|
||||||
|
if user.MainQuest.ReplayFlowCurrentQuestSceneId == 0 && user.MainQuest.ReplayFlowHeadQuestSceneId == 0 {
|
||||||
|
return "[]"
|
||||||
|
}
|
||||||
s, _ := utils.EncodeJSONMaps(map[string]any{
|
s, _ := utils.EncodeJSONMaps(map[string]any{
|
||||||
"userId": user.UserId,
|
"userId": user.UserId,
|
||||||
"currentHeadQuestSceneId": user.MainQuest.ReplayFlowHeadQuestSceneId,
|
"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