diff --git a/assets/main.db b/assets/main.db index 64752b3..7046a9a 100644 Binary files a/assets/main.db and b/assets/main.db differ diff --git a/internal/handler/api/live/common.go b/internal/handler/api/live/common.go new file mode 100644 index 0000000..e2dfba1 --- /dev/null +++ b/internal/handler/api/live/common.go @@ -0,0 +1,314 @@ +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) []liveapischema.NormalLiveStatusList { + result := make([]liveapischema.NormalLiveStatusList, 0, len(ids)) + for _, id := range ids { + result = append(result, liveapischema.NormalLiveStatusList{ + LiveDifficultyID: id, + Status: 1, + HiScore: 0, + HiComboCount: 0, + ClearCnt: 0, + AchievedGoalIDList: []int{}, + }) + } + return result +} + +func buildSpecialLiveStatusList(ids []int) []liveapischema.SpecialLiveStatusList { + result := make([]liveapischema.SpecialLiveStatusList, 0, len(ids)) + for _, id := range ids { + result = append(result, liveapischema.SpecialLiveStatusList{ + LiveDifficultyID: id, + Status: 1, + HiScore: 0, + HiComboCount: 0, + ClearCnt: 0, + AchievedGoalIDList: []int{}, + }) + } + return result +} + +func buildTrainingLiveStatusList(ids []int) []liveapischema.TrainingLiveStatusList { + result := make([]liveapischema.TrainingLiveStatusList, 0, len(ids)) + for _, id := range ids { + result = append(result, liveapischema.TrainingLiveStatusList{ + LiveDifficultyID: id, + Status: 1, + HiScore: 0, + HiComboCount: 0, + ClearCnt: 0, + AchievedGoalIDList: []int{}, + }) + } + return result +} diff --git a/internal/handler/api/live/schedule.go b/internal/handler/api/live/schedule.go index df5f293..b278431 100644 --- a/internal/handler/api/live/schedule.go +++ b/internal/handler/api/live/schedule.go @@ -11,19 +11,20 @@ import ( func liveSchedule(ctx *gin.Context) (res any, err error) { ss := session.Get(ctx) - var liveID []int - err = ss.MainEng.Table("special_live_m"). - Cols("live_difficulty_id").OrderBy("live_difficulty_id ASC").Find(&liveID) + now := time.Now() + liveIDs, err := listTodaySpecialRotationDifficultyIDs(ss, now) if err != nil { return nil, err } - liveList := []liveapischema.LiveList{} - for _, id := range liveID { + liveList := make([]liveapischema.LiveList, 0, len(liveIDs)) + startDate := startOfDay(now.In(jst)).Format("2006-01-02 15:04:05") + endDate := nextDayStart(now.In(jst)).Format("2006-01-02 15:04:05") + for _, id := range liveIDs { liveList = append(liveList, liveapischema.LiveList{ LiveDifficultyID: id, - StartDate: "2023-01-01 00:00:00", - EndDate: "2037-01-01 00:00:00", + StartDate: startDate, + EndDate: endDate, IsRandom: false, }) } @@ -34,7 +35,7 @@ func liveSchedule(ctx *gin.Context) (res any, err error) { LiveList: liveList, LimitedBonusList: []any{}, LimitedBonusCommonList: []liveapischema.LimitedBonusCommonList{}, - RandomLiveList: []liveapischema.RandomLiveList{}, + RandomLiveList: buildRandomLiveList(), FreeLiveList: []any{}, TrainingLiveList: []liveapischema.TrainingLiveList{}, }, diff --git a/internal/handler/api/live/status.go b/internal/handler/api/live/status.go index c176e83..a831837 100644 --- a/internal/handler/api/live/status.go +++ b/internal/handler/api/live/status.go @@ -11,49 +11,28 @@ import ( func liveStatus(ctx *gin.Context) (res any, err error) { ss := session.Get(ctx) - var liveDifficultyID []int - normalLives := []liveapischema.NormalLiveStatusList{} - err = ss.MainEng.Table("normal_live_m").Cols("live_difficulty_id").OrderBy("live_difficulty_id ASC").Find(&liveDifficultyID) + normalIDs, err := listAvailableNormalLiveDifficultyIDs(ss) if err != nil { return nil, err } - for _, id := range liveDifficultyID { - normalLive := liveapischema.NormalLiveStatusList{ - LiveDifficultyID: id, - Status: 1, - HiScore: 0, - HiComboCount: 0, - ClearCnt: 0, - AchievedGoalIDList: []int{}, - } - normalLives = append(normalLives, normalLive) - } - specialLives := []liveapischema.SpecialLiveStatusList{} - err = ss.MainEng.Table("special_live_m").Cols("live_difficulty_id").OrderBy("live_difficulty_id ASC").Find(&liveDifficultyID) + specialIDs, err := listTodaySpecialRotationDifficultyIDs(ss, time.Now()) if err != nil { return nil, err } - for _, id := range liveDifficultyID { - specialLive := liveapischema.SpecialLiveStatusList{ - LiveDifficultyID: id, - Status: 1, - HiScore: 0, - HiComboCount: 0, - ClearCnt: 0, - AchievedGoalIDList: []int{}, - } - specialLives = append(specialLives, specialLive) + trainingIDs, err := listAvailableTrainingLiveDifficultyIDs(ss) + if err != nil { + return nil, err } res = liveapischema.StatusResp{ Result: liveapischema.StatusData{ - NormalLiveStatusList: normalLives, - SpecialLiveStatusList: specialLives, - TrainingLiveStatusList: []liveapischema.TrainingLiveStatusList{}, + NormalLiveStatusList: buildNormalLiveStatusList(normalIDs), + SpecialLiveStatusList: buildSpecialLiveStatusList(specialIDs), + TrainingLiveStatusList: buildTrainingLiveStatusList(trainingIDs), MarathonLiveStatusList: []any{}, FreeLiveStatusList: []any{}, - CanResumeLive: false, + CanResumeLive: true, }, Status: 200, CommandNum: false, diff --git a/internal/handler/api/login/topinfoonce.go b/internal/handler/api/login/topinfoonce.go index d0af3ec..6c47d20 100644 --- a/internal/handler/api/login/topinfoonce.go +++ b/internal/handler/api/login/topinfoonce.go @@ -10,14 +10,15 @@ import ( func loginTopInfoOnce(ctx *gin.Context) (res any, err error) { ss := session.Get(ctx) + userInfo := ss.GetUserInfo() res = loginapischema.TopInfoOnceResp{ Result: loginapischema.TopInfoOnceData{ NewAchievementCnt: 0, UnaccomplishedAchievementCnt: 0, LiveDailyRewardExist: false, - TrainingEnergy: 10, - TrainingEnergyMax: 10, + TrainingEnergy: userInfo.TrainingEnergy, + TrainingEnergyMax: userInfo.TrainingEnergyMax, Notification: loginapischema.Notification{ Push: false, Lp: false, diff --git a/internal/handler/live/partylist.go b/internal/handler/live/partylist.go index f1f8ee4..08d48b1 100644 --- a/internal/handler/live/partylist.go +++ b/internal/handler/live/partylist.go @@ -14,6 +14,7 @@ import ( func partyList(ctx *gin.Context) { ss := session.Get(ctx) defer ss.Finalize() + userInfo := ss.GetUserInfo() pref := usermodel.UserPref{} _, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref) @@ -37,7 +38,7 @@ func partyList(ctx *gin.Context) { if len(partyList) == 0 { selfParty := liveschema.PartyList{ - UserInfo: ss.GetUserInfo(), + UserInfo: userInfo, SettingAwardID: pref.AwardID, FriendStatus: usermodel.ClientFriendStatusNone, } @@ -52,8 +53,8 @@ func partyList(ctx *gin.Context) { ss.Respond(liveschema.PartyListResp{ ResponseData: liveschema.PartyListData{ PartyList: partyList, - TrainingEnergy: 10, - TrainingEnergyMax: 10, + TrainingEnergy: userInfo.TrainingEnergy, + TrainingEnergyMax: userInfo.TrainingEnergyMax, ServerTimestamp: time.Now().Unix(), }, ReleaseInfo: []any{}, diff --git a/internal/session/userdata.go b/internal/session/userdata.go index c85a9f3..b18cbb4 100644 --- a/internal/session/userdata.go +++ b/internal/session/userdata.go @@ -16,6 +16,8 @@ var defaultLpRecoveryItemIDs = []int{ 777015, 777016, 777017, 777018, 777019, 777020, } +const defaultTrainingEnergy = 999 + func (ss *Session) GetUserPref(userID string) usermodel.UserPref { pref := usermodel.UserPref{} has, err := ss.UserEng.Table(new(usermodel.UserPref)). @@ -62,8 +64,8 @@ func (ss *Session) GetUserInfo() userschema.UserInfo { LicenseLiveEnergyRecoverlyTime: 60, EnergyFullNeedTime: 0, OverMaxEnergy: ss.UserPref.EffectiveCurrentEnergy(), - TrainingEnergy: 10, - TrainingEnergyMax: 10, + TrainingEnergy: defaultTrainingEnergy, + TrainingEnergyMax: defaultTrainingEnergy, FriendMax: 99, InviteCode: ss.UserPref.InviteCode, InsertDate: "2015-08-10 18:58:30",