- Add random live handlers, schemas and persistence for lot/play/continue/gameover/reward flow - Update live status and schedule generation, including special rotation handling and CN timezone alignment - Add config support for unlocking all daily special rotation songs and document the option - Refresh live play, reward, precise score and session logic plus bundled main.db updates Signed-off-by: Sean Du <do4suki@gmail.com>
430 lines
12 KiB
Go
430 lines
12 KiB
Go
package live
|
|
|
|
import (
|
|
"fmt"
|
|
"honoka-chan/config"
|
|
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 cst = time.FixedZone("CST", 8*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"`
|
|
}
|
|
|
|
type specialLiveRotationSchedule struct {
|
|
LiveDifficultyID int
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
}
|
|
|
|
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 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: dayIndexInCST(baseDate),
|
|
})
|
|
}
|
|
|
|
currentDay := dayIndexInCST(now)
|
|
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 listCurrentAndNextSpecialRotationSchedules(ss *session.Session, now time.Time) ([]specialLiveRotationSchedule, error) {
|
|
if config.Conf.Settings.UnlockAllSpecialRotation {
|
|
return listAllSpecialRotationSchedules(ss)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type rotationEntry struct {
|
|
liveDifficultyID int
|
|
baseTime time.Time
|
|
dayModulo int64
|
|
}
|
|
|
|
grouped := map[int][]rotationEntry{}
|
|
groupIDs := []int{}
|
|
for _, row := range rows {
|
|
baseDate, err := parseRotationBaseDate(row.BaseDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := grouped[row.RotationGroupID]; !ok {
|
|
groupIDs = append(groupIDs, row.RotationGroupID)
|
|
}
|
|
grouped[row.RotationGroupID] = append(grouped[row.RotationGroupID], rotationEntry{
|
|
liveDifficultyID: row.LiveDifficultyID,
|
|
baseTime: baseDate,
|
|
dayModulo: dayIndexInCST(baseDate),
|
|
})
|
|
}
|
|
sort.Ints(groupIDs)
|
|
|
|
currentDay := dayIndexInCST(now)
|
|
result := make([]specialLiveRotationSchedule, 0, len(groupIDs)*2)
|
|
for _, groupID := range groupIDs {
|
|
liveList := grouped[groupID]
|
|
if len(liveList) == 0 {
|
|
continue
|
|
}
|
|
|
|
currentIndex := -1
|
|
currentDayModulo := currentDay % int64(len(liveList))
|
|
for i, live := range liveList {
|
|
if live.dayModulo%int64(len(liveList)) == currentDayModulo {
|
|
currentIndex = i
|
|
break
|
|
}
|
|
}
|
|
if currentIndex == -1 {
|
|
continue
|
|
}
|
|
|
|
periodDays := int64(1)
|
|
if len(liveList) >= 2 {
|
|
diff := int64(liveList[1].baseTime.Sub(liveList[0].baseTime) / (24 * time.Hour))
|
|
if diff > 0 {
|
|
periodDays = diff
|
|
}
|
|
}
|
|
|
|
currentEntry := liveList[currentIndex]
|
|
offsetDays := (currentDay - currentEntry.dayModulo) % periodDays
|
|
if offsetDays < 0 {
|
|
offsetDays += periodDays
|
|
}
|
|
currentStartTime := startOfDay(now.In(cst)).AddDate(0, 0, -int(offsetDays))
|
|
nextStartTime := currentStartTime.AddDate(0, 0, int(periodDays))
|
|
duration := time.Duration(periodDays)*24*time.Hour - time.Second
|
|
nextIndex := (currentIndex + 1) % len(liveList)
|
|
|
|
for i, live := range liveList {
|
|
switch i {
|
|
case currentIndex:
|
|
result = append(result, specialLiveRotationSchedule{
|
|
LiveDifficultyID: live.liveDifficultyID,
|
|
StartTime: currentStartTime,
|
|
EndTime: currentStartTime.Add(duration),
|
|
})
|
|
case nextIndex:
|
|
result = append(result, specialLiveRotationSchedule{
|
|
LiveDifficultyID: live.liveDifficultyID,
|
|
StartTime: nextStartTime,
|
|
EndTime: nextStartTime.Add(duration),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func listAllSpecialRotationSchedules(ss *session.Session) ([]specialLiveRotationSchedule, 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
|
|
}
|
|
|
|
type rotationEntry struct {
|
|
liveDifficultyID int
|
|
baseTime time.Time
|
|
}
|
|
|
|
grouped := map[int][]rotationEntry{}
|
|
groupIDs := []int{}
|
|
for _, row := range rows {
|
|
baseDate, err := parseRotationBaseDate(row.BaseDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := grouped[row.RotationGroupID]; !ok {
|
|
groupIDs = append(groupIDs, row.RotationGroupID)
|
|
}
|
|
grouped[row.RotationGroupID] = append(grouped[row.RotationGroupID], rotationEntry{
|
|
liveDifficultyID: row.LiveDifficultyID,
|
|
baseTime: baseDate,
|
|
})
|
|
}
|
|
sort.Ints(groupIDs)
|
|
|
|
result := make([]specialLiveRotationSchedule, 0, len(rows))
|
|
for _, groupID := range groupIDs {
|
|
liveList := grouped[groupID]
|
|
if len(liveList) == 0 {
|
|
continue
|
|
}
|
|
|
|
periodDays := int64(1)
|
|
if len(liveList) >= 2 {
|
|
diff := int64(liveList[1].baseTime.Sub(liveList[0].baseTime) / (24 * time.Hour))
|
|
if diff > 0 {
|
|
periodDays = diff
|
|
}
|
|
}
|
|
if periodDays != 1 {
|
|
continue
|
|
}
|
|
|
|
for _, live := range liveList {
|
|
result = append(result, specialLiveRotationSchedule{
|
|
LiveDifficultyID: live.liveDifficultyID,
|
|
StartTime: time.Unix(0, 0).In(cst),
|
|
EndTime: time.Unix(2147483647, 0).In(cst),
|
|
})
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func listAvailableTrainingLiveDifficultyIDs(ss *session.Session) ([]int, error) {
|
|
specialRows, err := listAvailableSpecialLiveRows(ss)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]int, 0)
|
|
for _, row := range specialRows {
|
|
if row.Difficulty <= 5 || row.AcFlag != 0 || row.ExcludeClearCountFlag != 0 {
|
|
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, cst)
|
|
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 dayIndexInCST(t time.Time) int64 {
|
|
return startOfDay(t.In(cst)).Unix() / 86400
|
|
}
|
|
|
|
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
|
|
}
|