Persist live status and migrate legacy live data
- Add user_live_status to store hi score, hi combo count, and clear count per chart - Expand user_live_goal to persist achieved live_goal_reward_id entries instead of only coarse goal types - Update live reward handling to refresh live status and sync newly achieved live goals on every clear - Return live status and achieved goal ids from stored user data in /api/live/status - Aggregate profile liveCnt from stored user_live_status instead of hardcoded values - Load complete-rank thresholds from live info for goal evaluation - Migrate legacy user_live_record data into user_live_status and backfill achievable live goals on startup Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
+44
-27
@@ -1,7 +1,6 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"honoka-chan/internal/constant"
|
||||
usermodel "honoka-chan/internal/model/user"
|
||||
liveschema "honoka-chan/internal/schema/live"
|
||||
liverecordschema "honoka-chan/internal/schema/liverecord"
|
||||
@@ -64,13 +63,34 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo
|
||||
BRankCombo int `xorm:"b_rank_combo"`
|
||||
CRankCombo int `xorm:"c_rank_combo"`
|
||||
SRankCombo int `xorm:"s_rank_combo"`
|
||||
ARankComplete int `xorm:"a_rank_complete"`
|
||||
BRankComplete int `xorm:"b_rank_complete"`
|
||||
CRankComplete int `xorm:"c_rank_complete"`
|
||||
SRankComplete int `xorm:"s_rank_complete"`
|
||||
AcFlag int `xorm:"ac_flag"`
|
||||
SwingFlag int `xorm:"swing_flag"`
|
||||
}
|
||||
has, err := ss.MainEng.Table("special_live_m").Alias("a").
|
||||
Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id").
|
||||
Where("live_difficulty_id = ?", LiveDifficultyID).
|
||||
Cols("a.live_difficulty_id,live_setting_m.*").Get(&info)
|
||||
Select(`
|
||||
a.live_difficulty_id,
|
||||
a.c_rank_complete,
|
||||
a.b_rank_complete,
|
||||
a.a_rank_complete,
|
||||
a.s_rank_complete,
|
||||
live_setting_m.notes_setting_asset,
|
||||
live_setting_m.a_rank_score,
|
||||
live_setting_m.b_rank_score,
|
||||
live_setting_m.c_rank_score,
|
||||
live_setting_m.s_rank_score,
|
||||
live_setting_m.a_rank_combo,
|
||||
live_setting_m.b_rank_combo,
|
||||
live_setting_m.c_rank_combo,
|
||||
live_setting_m.s_rank_combo,
|
||||
live_setting_m.ac_flag,
|
||||
live_setting_m.swing_flag
|
||||
`).Get(&info)
|
||||
if ss.CheckErr(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -79,7 +99,24 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo
|
||||
has, err = ss.MainEng.Table("normal_live_m").Alias("a").
|
||||
Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id").
|
||||
Where("live_difficulty_id = ?", LiveDifficultyID).
|
||||
Cols("a.live_difficulty_id,live_setting_m.*").Get(&info)
|
||||
Select(`
|
||||
a.live_difficulty_id,
|
||||
a.c_rank_complete,
|
||||
a.b_rank_complete,
|
||||
a.a_rank_complete,
|
||||
a.s_rank_complete,
|
||||
live_setting_m.notes_setting_asset,
|
||||
live_setting_m.a_rank_score,
|
||||
live_setting_m.b_rank_score,
|
||||
live_setting_m.c_rank_score,
|
||||
live_setting_m.s_rank_score,
|
||||
live_setting_m.a_rank_combo,
|
||||
live_setting_m.b_rank_combo,
|
||||
live_setting_m.c_rank_combo,
|
||||
live_setting_m.s_rank_combo,
|
||||
live_setting_m.ac_flag,
|
||||
live_setting_m.swing_flag
|
||||
`).Get(&info)
|
||||
if ss.CheckErr(err) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -105,6 +142,10 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo
|
||||
BRankCombo: info.BRankCombo,
|
||||
CRankCombo: info.CRankCombo,
|
||||
SRankCombo: info.SRankCombo,
|
||||
ARankComplete: info.ARankComplete,
|
||||
BRankComplete: info.BRankComplete,
|
||||
CRankComplete: info.CRankComplete,
|
||||
SRankComplete: info.SRankComplete,
|
||||
AcFlag: info.AcFlag,
|
||||
SwingFlag: info.SwingFlag,
|
||||
NotesList: []liveschema.NotesList{},
|
||||
@@ -188,27 +229,3 @@ func (ss *Session) GetDeckInfo(deckID int) (bool, *liverecordschema.DeckInfo) {
|
||||
UnitList: unitData,
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *Session) GetLiveGoalList(LiveDifficultyID int) []constant.LiveGoalType {
|
||||
var goal []constant.LiveGoalType
|
||||
err := ss.MainEng.Table("live_goal_reward_m").
|
||||
Where("live_difficulty_id = ?", LiveDifficultyID).Cols("live_goal_type").Find(&goal)
|
||||
if ss.CheckErr(err) {
|
||||
return []constant.LiveGoalType{}
|
||||
}
|
||||
return goal
|
||||
}
|
||||
|
||||
func (ss *Session) GetUserLiveGoalList(LiveDifficultyID int) []constant.LiveGoalType {
|
||||
var goal []constant.LiveGoalType
|
||||
var goalData []usermodel.UserLiveGoal
|
||||
err := ss.UserEng.Table(new(usermodel.UserLiveGoal)).
|
||||
Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, LiveDifficultyID).Find(&goalData)
|
||||
if ss.CheckErr(err) {
|
||||
return []constant.LiveGoalType{}
|
||||
}
|
||||
for _, g := range goalData {
|
||||
goal = append(goal, g.GoalType)
|
||||
}
|
||||
return goal
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"honoka-chan/internal/constant"
|
||||
usermodel "honoka-chan/internal/model/user"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type liveGoalRewardRow struct {
|
||||
LiveGoalRewardID int `xorm:"live_goal_reward_id"`
|
||||
LiveDifficultyID int `xorm:"live_difficulty_id"`
|
||||
LiveGoalType constant.LiveGoalType `xorm:"live_goal_type"`
|
||||
Rank int `xorm:"rank"`
|
||||
AddType int `xorm:"add_type"`
|
||||
ItemID int `xorm:"item_id"`
|
||||
ItemCategoryID int `xorm:"item_category_id"`
|
||||
Amount int `xorm:"amount"`
|
||||
}
|
||||
|
||||
type LiveStatusSnapshot struct {
|
||||
Status int
|
||||
HiScore int
|
||||
HiComboCount int
|
||||
ClearCnt int
|
||||
AchievedGoalIDList []int
|
||||
}
|
||||
|
||||
func liveRank(value int, cRank int, bRank int, aRank int, sRank int) int {
|
||||
switch {
|
||||
case value >= sRank:
|
||||
return 1
|
||||
case value >= aRank:
|
||||
return 2
|
||||
case value >= bRank:
|
||||
return 3
|
||||
case value >= cRank:
|
||||
return 4
|
||||
default:
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
func (ss *Session) GetUserLiveStatus(liveDifficultyID int) (bool, *usermodel.UserLiveStatus, error) {
|
||||
status := usermodel.UserLiveStatus{}
|
||||
has, err := ss.UserEng.Table(new(usermodel.UserLiveStatus)).
|
||||
Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID).
|
||||
Get(&status)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return has, &status, nil
|
||||
}
|
||||
|
||||
func (ss *Session) GetUserLiveStatusMap(liveDifficultyIDs []int) (map[int]usermodel.UserLiveStatus, error) {
|
||||
result := map[int]usermodel.UserLiveStatus{}
|
||||
if len(liveDifficultyIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rows := []usermodel.UserLiveStatus{}
|
||||
err := ss.UserEng.Table(new(usermodel.UserLiveStatus)).
|
||||
In("live_difficulty_id", liveDifficultyIDs).
|
||||
Where("user_id = ?", ss.UserID).
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
result[row.LiveDifficultyID] = row
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ss *Session) GetUserLiveGoalRewardIDMap(liveDifficultyIDs []int) (map[int][]int, error) {
|
||||
result := map[int][]int{}
|
||||
if len(liveDifficultyIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rows := []usermodel.UserLiveGoal{}
|
||||
err := ss.UserEng.Table(new(usermodel.UserLiveGoal)).
|
||||
In("live_difficulty_id", liveDifficultyIDs).
|
||||
Where("user_id = ?", ss.UserID).
|
||||
Where("live_goal_reward_id > 0").
|
||||
OrderBy("live_goal_reward_id ASC").
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
result[row.LiveDifficultyID] = append(result[row.LiveDifficultyID], row.LiveGoalRewardID)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ss *Session) BuildLiveStatusSnapshotMap(liveDifficultyIDs []int) (map[int]LiveStatusSnapshot, error) {
|
||||
statusMap, err := ss.GetUserLiveStatusMap(liveDifficultyIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
goalMap, err := ss.GetUserLiveGoalRewardIDMap(liveDifficultyIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[int]LiveStatusSnapshot, len(liveDifficultyIDs))
|
||||
for _, liveDifficultyID := range liveDifficultyIDs {
|
||||
status := LiveStatusSnapshot{
|
||||
Status: 1,
|
||||
HiScore: 0,
|
||||
HiComboCount: 0,
|
||||
ClearCnt: 0,
|
||||
AchievedGoalIDList: []int{},
|
||||
}
|
||||
|
||||
if row, ok := statusMap[liveDifficultyID]; ok {
|
||||
status.HiScore = row.HiScore
|
||||
status.HiComboCount = row.HiComboCount
|
||||
status.ClearCnt = row.ClearCnt
|
||||
if row.ClearCnt > 0 {
|
||||
status.Status = 2
|
||||
}
|
||||
}
|
||||
|
||||
if ids := goalMap[liveDifficultyID]; len(ids) > 0 {
|
||||
status.AchievedGoalIDList = ids
|
||||
}
|
||||
|
||||
result[liveDifficultyID] = status
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ss *Session) UpdateUserLiveStatus(liveDifficultyID int, totalScore int, maxCombo int) (before usermodel.UserLiveStatus, after usermodel.UserLiveStatus, err error) {
|
||||
now := time.Now().Unix()
|
||||
has, current, err := ss.GetUserLiveStatus(liveDifficultyID)
|
||||
if err != nil {
|
||||
return before, after, err
|
||||
}
|
||||
if current == nil {
|
||||
current = &usermodel.UserLiveStatus{}
|
||||
}
|
||||
|
||||
before = *current
|
||||
if !has {
|
||||
after = usermodel.UserLiveStatus{
|
||||
UserID: ss.UserID,
|
||||
LiveDifficultyID: liveDifficultyID,
|
||||
HiScore: totalScore,
|
||||
HiComboCount: maxCombo,
|
||||
ClearCnt: 1,
|
||||
InsertDate: now,
|
||||
UpdateDate: now,
|
||||
}
|
||||
_, err = ss.UserEng.Insert(&after)
|
||||
return before, after, err
|
||||
}
|
||||
|
||||
after = before
|
||||
if totalScore > after.HiScore {
|
||||
after.HiScore = totalScore
|
||||
}
|
||||
if maxCombo > after.HiComboCount {
|
||||
after.HiComboCount = maxCombo
|
||||
}
|
||||
after.ClearCnt++
|
||||
after.UpdateDate = now
|
||||
|
||||
_, err = ss.UserEng.Table(new(usermodel.UserLiveStatus)).
|
||||
Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID).
|
||||
Cols("hi_score", "hi_combo_count", "clear_cnt", "update_date").
|
||||
Update(&after)
|
||||
return before, after, err
|
||||
}
|
||||
|
||||
func (ss *Session) GetAchievedLiveGoalRewardRows(liveDifficultyID int, hiScore int, hiComboCount int, clearCnt int) ([]liveGoalRewardRow, error) {
|
||||
has, liveInfo := ss.GetLiveInfo(liveDifficultyID)
|
||||
if !has || liveInfo == nil {
|
||||
return []liveGoalRewardRow{}, nil
|
||||
}
|
||||
|
||||
rows := []liveGoalRewardRow{}
|
||||
err := ss.MainEng.Table("live_goal_reward_m").
|
||||
Where("live_difficulty_id = ?", liveDifficultyID).
|
||||
OrderBy("live_goal_type ASC, rank ASC, live_goal_reward_id ASC").
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
scoreRank := liveRank(hiScore, liveInfo.CRankScore, liveInfo.BRankScore, liveInfo.ARankScore, liveInfo.SRankScore)
|
||||
comboRank := liveRank(hiComboCount, liveInfo.CRankCombo, liveInfo.BRankCombo, liveInfo.ARankCombo, liveInfo.SRankCombo)
|
||||
clearRank := liveRank(clearCnt, liveInfo.CRankComplete, liveInfo.BRankComplete, liveInfo.ARankComplete, liveInfo.SRankComplete)
|
||||
|
||||
result := make([]liveGoalRewardRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
switch row.LiveGoalType {
|
||||
case constant.LiveGoalTypeScore:
|
||||
if scoreRank <= row.Rank {
|
||||
result = append(result, row)
|
||||
}
|
||||
case constant.LiveGoalTypeCombo:
|
||||
if comboRank <= row.Rank {
|
||||
result = append(result, row)
|
||||
}
|
||||
case constant.LiveGoalTypeClear:
|
||||
if clearRank <= row.Rank {
|
||||
result = append(result, row)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (ss *Session) SyncUserLiveGoals(liveDifficultyID int, hiScore int, hiComboCount int, clearCnt int) ([]int, error) {
|
||||
achievedRows, err := ss.GetAchievedLiveGoalRewardRows(liveDifficultyID, hiScore, hiComboCount, clearCnt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(achievedRows) == 0 {
|
||||
return []int{}, nil
|
||||
}
|
||||
|
||||
existingRows := []usermodel.UserLiveGoal{}
|
||||
err = ss.UserEng.Table(new(usermodel.UserLiveGoal)).
|
||||
Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID).
|
||||
Where("live_goal_reward_id > 0").
|
||||
Find(&existingRows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing := make(map[int]struct{}, len(existingRows))
|
||||
for _, row := range existingRows {
|
||||
existing[row.LiveGoalRewardID] = struct{}{}
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
newlyAchieved := make([]int, 0)
|
||||
for _, row := range achievedRows {
|
||||
if _, ok := existing[row.LiveGoalRewardID]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = ss.UserEng.Insert(&usermodel.UserLiveGoal{
|
||||
UserID: ss.UserID,
|
||||
LiveDifficultyID: liveDifficultyID,
|
||||
LiveGoalRewardID: row.LiveGoalRewardID,
|
||||
GoalType: row.LiveGoalType,
|
||||
Rank: row.Rank,
|
||||
CompletedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newlyAchieved = append(newlyAchieved, row.LiveGoalRewardID)
|
||||
}
|
||||
|
||||
sort.Ints(newlyAchieved)
|
||||
return newlyAchieved, nil
|
||||
}
|
||||
|
||||
func (ss *Session) GetLiveGoalRewardRowsByIDs(goalRewardIDs []int) ([]liveGoalRewardRow, error) {
|
||||
if len(goalRewardIDs) == 0 {
|
||||
return []liveGoalRewardRow{}, nil
|
||||
}
|
||||
|
||||
rows := []liveGoalRewardRow{}
|
||||
err := ss.MainEng.Table("live_goal_reward_m").
|
||||
In("live_goal_reward_id", goalRewardIDs).
|
||||
OrderBy("live_goal_reward_id ASC").
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
Reference in New Issue
Block a user