Improve live and random live handling

- 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>
This commit is contained in:
2026-06-06 20:59:45 +08:00
parent 4e642cc7c1
commit 0aacf8a4e9
28 changed files with 1085 additions and 228 deletions
+163 -51
View File
@@ -2,6 +2,7 @@ package live
import (
"fmt"
"honoka-chan/config"
liveapischema "honoka-chan/internal/schema/api/live"
"honoka-chan/internal/session"
"os"
@@ -15,7 +16,7 @@ const (
randomLiveEndDate = "2038-01-19 12:14:07"
)
var jst = time.FixedZone("JST", 9*3600)
var cst = time.FixedZone("CST", 8*3600)
type liveAvailabilityRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"`
@@ -33,6 +34,12 @@ type specialLiveRotationRow struct {
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 {
@@ -46,25 +53,6 @@ func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
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").
@@ -155,11 +143,11 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
dayModulo int64
}{
liveDifficultyID: row.LiveDifficultyID,
dayModulo: baseDate.Unix() / 86400,
dayModulo: dayIndexInCST(baseDate),
})
}
currentDay := now.In(jst).Unix() / 86400
currentDay := dayIndexInCST(now)
groupIDs := make([]int, 0, len(grouped))
for groupID := range grouped {
groupIDs = append(groupIDs, groupID)
@@ -183,54 +171,174 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
return result, nil
}
func listAllSpecialRotationDifficultyIDs(ss *session.Session) (map[int]struct{}, error) {
ids := []int{}
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").
Cols("live_difficulty_id").
Find(&ids)
OrderBy("rotation_group_id ASC, base_date ASC").
Find(&rows)
if err != nil {
return nil, err
}
result := make(map[int]struct{}, len(ids))
for _, id := range ids {
result[id] = struct{}{}
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) {
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)
}
@@ -244,7 +352,7 @@ func parseRotationBaseDate(value string) (time.Time, error) {
"2006/01/02 3:04:05",
}
for _, layout := range layouts {
t, err := time.ParseInLocation(layout, value, jst)
t, err := time.ParseInLocation(layout, value, cst)
if err == nil {
return t, nil
}
@@ -256,6 +364,10 @@ 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)
}