Files
honoka-chan/internal/handler/api/live/common.go
T
YumeMichi f6466989c6 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>
2026-06-06 16:25:06 +08:00

318 lines
8.5 KiB
Go

package live
import (
"fmt"
liveapischema "honoka-chan/internal/schema/api/live"
"honoka-chan/internal/session"
"os"
"path/filepath"
"sort"
"time"
)
const (
randomLiveStartDate = "1970-01-01 00:00:00"
randomLiveEndDate = "2038-01-19 12:14:07"
)
var jst = time.FixedZone("JST", 9*3600)
type liveAvailabilityRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"`
LiveTrackID int `xorm:"live_track_id"`
LiveSettingID int `xorm:"live_setting_id"`
Difficulty int `xorm:"difficulty"`
AcFlag int `xorm:"ac_flag"`
NotesSettingAsset string `xorm:"notes_setting_asset"`
ExcludeClearCountFlag int `xorm:"exclude_clear_count_flag"`
}
type specialLiveRotationRow struct {
RotationGroupID int `xorm:"rotation_group_id"`
LiveDifficultyID int `xorm:"live_difficulty_id"`
BaseDate string `xorm:"base_date"`
}
func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
rows, err := listAvailableNormalLiveRows(ss)
if err != nil {
return nil, err
}
result := make([]int, 0, len(rows))
for _, row := range rows {
result = append(result, row.LiveDifficultyID)
}
return result, nil
}
func listAvailableNormalLiveTrackIDs(ss *session.Session) ([]int, error) {
rows, err := listAvailableNormalLiveRows(ss)
if err != nil {
return nil, err
}
trackSet := map[int]struct{}{}
for _, row := range rows {
trackSet[row.LiveTrackID] = struct{}{}
}
result := make([]int, 0, len(trackSet))
for id := range trackSet {
result = append(result, id)
}
sort.Ints(result)
return result, nil
}
func listAvailableNormalLiveRows(ss *session.Session) ([]liveAvailabilityRow, error) {
rows := []liveAvailabilityRow{}
err := ss.MainEng.Table("normal_live_m").Alias("live").
Join("LEFT", "live_setting_m setting", "live.live_setting_id = setting.live_setting_id").
Select(`
live.live_difficulty_id,
live.live_setting_id,
setting.live_track_id,
setting.difficulty,
setting.ac_flag,
setting.notes_setting_asset,
0 AS exclude_clear_count_flag
`).
OrderBy("live.live_difficulty_id ASC").
Find(&rows)
if err != nil {
return nil, err
}
result := make([]liveAvailabilityRow, 0, len(rows))
for _, row := range rows {
if !hasBeatmap(row.NotesSettingAsset) {
continue
}
result = append(result, row)
}
return result, nil
}
func listAvailableSpecialLiveRows(ss *session.Session) ([]liveAvailabilityRow, error) {
rows := []liveAvailabilityRow{}
err := ss.MainEng.Table("special_live_m").Alias("live").
Join("LEFT", "live_setting_m setting", "live.live_setting_id = setting.live_setting_id").
Select(`
live.live_difficulty_id,
live.live_setting_id,
setting.live_track_id,
setting.difficulty,
setting.ac_flag,
setting.notes_setting_asset,
COALESCE(live.exclude_clear_count_flag, 0) AS exclude_clear_count_flag
`).
OrderBy("live.live_difficulty_id ASC").
Find(&rows)
if err != nil {
return nil, err
}
result := make([]liveAvailabilityRow, 0, len(rows))
for _, row := range rows {
if !hasBeatmap(row.NotesSettingAsset) {
continue
}
result = append(result, row)
}
return result, nil
}
func hasBeatmap(notesSettingAsset string) bool {
if notesSettingAsset == "" {
return false
}
path := filepath.Join("assets/serverdata/beatmaps", notesSettingAsset)
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) ([]int, error) {
rows := []specialLiveRotationRow{}
err := ss.MainEng.Table("special_live_rotation_m").
OrderBy("rotation_group_id ASC, base_date ASC").
Find(&rows)
if err != nil {
return nil, err
}
grouped := map[int][]struct {
liveDifficultyID int
dayModulo int64
}{}
for _, row := range rows {
baseDate, err := parseRotationBaseDate(row.BaseDate)
if err != nil {
return nil, err
}
grouped[row.RotationGroupID] = append(grouped[row.RotationGroupID], struct {
liveDifficultyID int
dayModulo int64
}{
liveDifficultyID: row.LiveDifficultyID,
dayModulo: baseDate.Unix() / 86400,
})
}
currentDay := now.In(jst).Unix() / 86400
groupIDs := make([]int, 0, len(grouped))
for groupID := range grouped {
groupIDs = append(groupIDs, groupID)
}
sort.Ints(groupIDs)
result := make([]int, 0, len(groupIDs))
for _, groupID := range groupIDs {
liveList := grouped[groupID]
if len(liveList) == 0 {
continue
}
currentDayModulo := currentDay % int64(len(liveList))
for _, live := range liveList {
if live.dayModulo%int64(len(liveList)) == currentDayModulo {
result = append(result, live.liveDifficultyID)
break
}
}
}
return result, nil
}
func listAllSpecialRotationDifficultyIDs(ss *session.Session) (map[int]struct{}, error) {
ids := []int{}
err := ss.MainEng.Table("special_live_rotation_m").
Cols("live_difficulty_id").
Find(&ids)
if err != nil {
return nil, err
}
result := make(map[int]struct{}, len(ids))
for _, id := range ids {
result[id] = struct{}{}
}
return result, nil
}
func listAvailableTrainingLiveDifficultyIDs(ss *session.Session) ([]int, error) {
normalTrackIDs, err := listAvailableNormalLiveTrackIDs(ss)
if err != nil {
return nil, err
}
specialRows, err := listAvailableSpecialLiveRows(ss)
if err != nil {
return nil, err
}
rotationIDs, err := listAllSpecialRotationDifficultyIDs(ss)
if err != nil {
return nil, err
}
normalTrackIDSet := make(map[int]struct{}, len(normalTrackIDs))
for _, id := range normalTrackIDs {
normalTrackIDSet[id] = struct{}{}
}
result := make([]int, 0)
for _, row := range specialRows {
if _, ok := normalTrackIDSet[row.LiveTrackID]; !ok {
continue
}
if row.Difficulty <= 5 || row.AcFlag != 0 || row.ExcludeClearCountFlag != 0 {
continue
}
if _, ok := rotationIDs[row.LiveDifficultyID]; ok {
continue
}
result = append(result, row.LiveDifficultyID)
}
sort.Ints(result)
return result, nil
}
func parseRotationBaseDate(value string) (time.Time, error) {
layouts := []string{
"2006/01/02 15:04:05",
"2006/01/02 3:04:05",
}
for _, layout := range layouts {
t, err := time.ParseInLocation(layout, value, jst)
if err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("invalid special live rotation base_date: %s", value)
}
func startOfDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
}
func nextDayStart(t time.Time) time.Time {
return startOfDay(t).Add(24 * time.Hour)
}
func buildRandomLiveList() []liveapischema.RandomLiveList {
return []liveapischema.RandomLiveList{
{AttributeID: 1, StartDate: randomLiveStartDate, EndDate: randomLiveEndDate},
{AttributeID: 2, StartDate: randomLiveStartDate, EndDate: randomLiveEndDate},
{AttributeID: 3, StartDate: randomLiveStartDate, EndDate: randomLiveEndDate},
}
}
func buildNormalLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.NormalLiveStatusList {
result := make([]liveapischema.NormalLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.NormalLiveStatusList{
LiveDifficultyID: id,
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
}
func buildSpecialLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.SpecialLiveStatusList {
result := make([]liveapischema.SpecialLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.SpecialLiveStatusList{
LiveDifficultyID: id,
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
}
func buildTrainingLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.TrainingLiveStatusList {
result := make([]liveapischema.TrainingLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.TrainingLiveStatusList{
LiveDifficultyID: id,
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
}