diff --git a/assets/main.db b/assets/main.db index 7046a9a..5dc965d 100644 Binary files a/assets/main.db and b/assets/main.db differ diff --git a/config/config.go b/config/config.go index 0722663..4985b59 100644 --- a/config/config.go +++ b/config/config.go @@ -23,8 +23,9 @@ type AppConfigs struct { } type Settings struct { - ListenPort string `json:"listen_port"` - CdnServer string `json:"cdn_server"` + ListenPort string `json:"listen_port"` + CdnServer string `json:"cdn_server"` + UnlockAllSpecialRotation bool `json:"unlock_all_special_rotation"` } func InitConfig() { @@ -35,8 +36,9 @@ func DefaultConfigs() *AppConfigs { return &AppConfigs{ AppName: "honoka-chan", Settings: Settings{ - ListenPort: "8080", - CdnServer: "http://127.0.0.1:8080/static", + ListenPort: "8080", + CdnServer: "http://127.0.0.1:8080/static", + UnlockAllSpecialRotation: false, }, } } diff --git a/docs/04-testing.md b/docs/04-testing.md index bcf56a5..03a8180 100644 --- a/docs/04-testing.md +++ b/docs/04-testing.md @@ -7,6 +7,7 @@ 第一次启动 `honoka-chan` 后会生成 `config.json`。需要确认: - `settings.cdn_server` 指向你的数据下载地址 +- `settings.unlock_all_special_rotation` 可选;设为 `true` 后会忽略日替时间限制,直接解锁全部日替特殊歌曲,不包含周替 `MASTER` - 如果数据直接放在本项目的 `static` 目录下,通常可以配置成类似 `http://192.168.1.123/static` 如果 `cdn_server/{系统}/archives/99_0_115.zip` 存在,服务端会自动把它追加到更新下载列表;如果不存在,则会直接跳过。 diff --git a/internal/handler/api/live/common.go b/internal/handler/api/live/common.go index 5ebb396..3d3989a 100644 --- a/internal/handler/api/live/common.go +++ b/internal/handler/api/live/common.go @@ -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) } diff --git a/internal/handler/api/live/schedule.go b/internal/handler/api/live/schedule.go index b278431..a9ae498 100644 --- a/internal/handler/api/live/schedule.go +++ b/internal/handler/api/live/schedule.go @@ -11,20 +11,17 @@ import ( func liveSchedule(ctx *gin.Context) (res any, err error) { ss := session.Get(ctx) - now := time.Now() - liveIDs, err := listTodaySpecialRotationDifficultyIDs(ss, now) + schedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now()) if err != nil { return nil, err } - 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 := make([]liveapischema.LiveList, 0, len(schedules)) + for _, schedule := range schedules { liveList = append(liveList, liveapischema.LiveList{ - LiveDifficultyID: id, - StartDate: startDate, - EndDate: endDate, + LiveDifficultyID: schedule.LiveDifficultyID, + StartDate: schedule.StartTime.Format("2006-01-02 15:04:05"), + EndDate: schedule.EndTime.Format("2006-01-02 15:04:05"), IsRandom: false, }) } diff --git a/internal/handler/api/live/status.go b/internal/handler/api/live/status.go index 20b6c38..e094687 100644 --- a/internal/handler/api/live/status.go +++ b/internal/handler/api/live/status.go @@ -16,10 +16,14 @@ func liveStatus(ctx *gin.Context) (res any, err error) { return nil, err } - specialIDs, err := listTodaySpecialRotationDifficultyIDs(ss, time.Now()) + specialSchedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now()) if err != nil { return nil, err } + specialIDs := make([]int, 0, len(specialSchedules)) + for _, schedule := range specialSchedules { + specialIDs = append(specialIDs, schedule.LiveDifficultyID) + } trainingIDs, err := listAvailableTrainingLiveDifficultyIDs(ss) if err != nil { return nil, err diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 74871cc..f703198 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -26,6 +26,7 @@ import ( _ "honoka-chan/internal/handler/profile" _ "honoka-chan/internal/handler/ranking" _ "honoka-chan/internal/handler/reward" + _ "honoka-chan/internal/handler/rlive" _ "honoka-chan/internal/handler/scenario" _ "honoka-chan/internal/handler/secretbox" _ "honoka-chan/internal/handler/subscenario" diff --git a/internal/handler/live/continue.go b/internal/handler/live/continue.go index 2d6ae9e..3a87534 100644 --- a/internal/handler/live/continue.go +++ b/internal/handler/live/continue.go @@ -14,7 +14,11 @@ func continuee(ctx *gin.Context) { ss := session.Get(ctx) defer ss.Finalize() - ss.Respond(liveschema.ContinueResp{ + ss.Respond(BuildContinueResp(ss)) +} + +func BuildContinueResp(ss *session.Session) liveschema.ContinueResp { + return liveschema.ContinueResp{ ResponseData: liveschema.ContinueData{ BeforeSnsCoin: ss.UserPref.SnsCoin, AfterSnsCoin: ss.UserPref.SnsCoin, @@ -22,7 +26,7 @@ func continuee(ctx *gin.Context) { }, ReleaseInfo: []any{}, StatusCode: 200, - }) + } } func init() { diff --git a/internal/handler/live/gameover.go b/internal/handler/live/gameover.go index 4b4a3b1..103227f 100644 --- a/internal/handler/live/gameover.go +++ b/internal/handler/live/gameover.go @@ -12,15 +12,21 @@ import ( func gameOver(ctx *gin.Context) { ss := session.Get(ctx) defer func() { - ss.ClearLiveInProgress() + if ss.UserEng != nil { + ss.ClearLiveInProgress() + } ss.Finalize() }() - ss.Respond(liveschema.GameOverResp{ + ss.Respond(BuildGameOverResp()) +} + +func BuildGameOverResp() liveschema.GameOverResp { + return liveschema.GameOverResp{ ResponseData: []any{}, ReleaseInfo: []any{}, StatusCode: 200, - }) + } } func init() { diff --git a/internal/handler/live/partylist.go b/internal/handler/live/partylist.go index 08d48b1..f7f5ad7 100644 --- a/internal/handler/live/partylist.go +++ b/internal/handler/live/partylist.go @@ -14,24 +14,37 @@ 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) + partyListData, err := BuildPartyListData(ss) if ss.CheckErr(err) { return } + ss.Respond(liveschema.PartyListResp{ + ResponseData: partyListData, + ReleaseInfo: []any{}, + StatusCode: 200, + }) +} + +func BuildPartyListData(ss *session.Session) (liveschema.PartyListData, error) { + userInfo := ss.GetUserInfo() + + pref := usermodel.UserPref{} + _, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref) + if err != nil { + return liveschema.PartyListData{}, err + } + supportRows, err := listLiveSupportRows(ss) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PartyListData{}, err } partyList := make([]liveschema.PartyList, 0, len(supportRows)) for _, row := range supportRows { party, err := buildLiveSupportParty(ss, row) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PartyListData{}, err } partyList = append(partyList, party) } @@ -44,22 +57,18 @@ func partyList(ctx *gin.Context) { } selfParty.AvailableSocialPoint = 10 selfParty.CenterUnitInfo, err = mustFindCenterUnitInfo(ss, ss.UserID, pref.UnitOwningUserID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PartyListData{}, err } partyList = append(partyList, selfParty) } - ss.Respond(liveschema.PartyListResp{ - ResponseData: liveschema.PartyListData{ - PartyList: partyList, - TrainingEnergy: userInfo.TrainingEnergy, - TrainingEnergyMax: userInfo.TrainingEnergyMax, - ServerTimestamp: time.Now().Unix(), - }, - ReleaseInfo: []any{}, - StatusCode: 200, - }) + return liveschema.PartyListData{ + PartyList: partyList, + TrainingEnergy: userInfo.TrainingEnergy, + TrainingEnergyMax: userInfo.TrainingEnergyMax, + ServerTimestamp: time.Now().Unix(), + }, nil } func init() { diff --git a/internal/handler/live/play.go b/internal/handler/live/play.go index ff93a3d..d8f3b56 100644 --- a/internal/handler/live/play.go +++ b/internal/handler/live/play.go @@ -25,93 +25,104 @@ func play(ctx *gin.Context) { } // fmt.Println(ctx.MustGet("request_data").(string)) + if err := ss.ResetRandomLiveInProgress(); ss.CheckErr(err) { + return + } + ss.RegisterLiveInProgress(playReq.UnitDeckID) - difficultyID, err := strconv.Atoi(playReq.LiveDifficultyID) + playResp, err := BuildPlayResp(ss, playReq, false) if ss.CheckErr(err) { return } + ss.Respond(playResp) +} + +func BuildPlayResp(ss *session.Session, playReq liveschema.PlayReq, isRandom bool) (liveschema.PlayResp, error) { + difficultyID, err := strconv.Atoi(playReq.LiveDifficultyID) + if err != nil { + return liveschema.PlayResp{}, err + } + liveSetting, err := loadLiveSetting(ss, difficultyID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } notes, err := loadLiveNotes(liveSetting.NotesSettingAsset) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } ranks := buildRankInfo(liveSetting) - owningIdList := []int{} + owningIDList := []int{} err = ss.UserEng.Table("user_deck_unit"). Join("LEFT", "user_deck", "user_deck_unit.user_deck_id = user_deck.id"). Where("user_deck.user_id = ? AND user_deck.deck_id = ?", ss.UserID, playReq.UnitDeckID). - Cols("unit_owning_user_id").OrderBy("user_deck_unit.position ASC").Find(&owningIdList) - if ss.CheckErr(err) { - return + Cols("unit_owning_user_id").OrderBy("user_deck_unit.position ASC").Find(&owningIDList) + if err != nil { + return liveschema.PlayResp{}, err } - if len(owningIdList) == 0 { - ss.CheckErr(errors.New("deck has no units")) - return + if len(owningIDList) == 0 { + return liveschema.PlayResp{}, errors.New("deck has no units") } museumBuff, err := getMuseumBuff(ss) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } myCenterUnitID, err := getDeckCenterUnitID(ss, playReq.UnitDeckID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } myLeaderSkill, err := getLeaderSkillData(ss, myCenterUnitID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } tomoUnitID, err := getSupportCenterUnitID(ss, int(playReq.PartyUserID), myCenterUnitID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } tomoLeaderSkill, err := getLeaderSkillData(ss, tomoUnitID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } memberTagCache := map[memberTagMatchKey]bool{} - unitDataMap, err := loadDeckUnitDataMap(ss, owningIdList) - if ss.CheckErr(err) { - return + unitDataMap, err := loadDeckUnitDataMap(ss, owningIDList) + if err != nil { + return liveschema.PlayResp{}, err } - accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIdList) - if ss.CheckErr(err) { - return + accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIDList) + if err != nil { + return liveschema.PlayResp{}, err } - removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIdList) - if ss.CheckErr(err) { - return + removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIDList) + if err != nil { + return liveschema.PlayResp{}, err } allRemovableSkillIDs := make([]int, 0) for _, skillIDs := range removableSkillMap { allRemovableSkillIDs = append(allRemovableSkillIDs, skillIDs...) } removableSkillMetaMap, err := loadRemovableSkillMetaMap(ss, allRemovableSkillIDs) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } - unitList := make([]liveschema.UnitList, 0, len(owningIdList)) + unitList := make([]liveschema.UnitList, 0, len(owningIDList)) var totalSmile, totalPure, totalCool float64 - var totalHp int - for _, owningId := range owningIdList { + var totalHP int + for _, owningID := range owningIDList { // 卡片基础属性 var baseSmile, basePure, baseCool, smileMax, pureMax, coolMax float64 - unitData, ok := unitDataMap[owningId] + unitData, ok := unitDataMap[owningID] if !ok { - ss.CheckErr(errors.New("unit data not found in deck")) - return + return liveschema.PlayResp{}, errors.New("unit data not found in deck") } baseSmile = float64(unitData.Smile) basePure = float64(unitData.Cute) @@ -120,7 +131,7 @@ func play(ctx *gin.Context) { // fmt.Println("基础属性:", baseSmile, basePure, baseCool) // 饰品属性加成(满级) - if bonus, ok := accessoryBonusMap[owningId]; ok { + if bonus, ok := accessoryBonusMap[owningID]; ok { // 饰品属性加成(该加成会影响个宝等百分比宝石属性加成的计算,故先计算。) baseSmile += bonus.Smile basePure += bonus.Pure @@ -153,14 +164,12 @@ func play(ctx *gin.Context) { var skillSmile, skillPure, skillCool float64 // 宝石加成(满级) - removableSkillIds := removableSkillMap[owningId] - - for _, sk := range removableSkillIds { + removableSkillIDs := removableSkillMap[owningID] + for _, skillID := range removableSkillIDs { // 判断宝石效果类型(效果范围、效果类型、效果值、是否固定数值) - meta, ok := removableSkillMetaMap[sk] + meta, ok := removableSkillMetaMap[skillID] if !ok { - ss.CheckErr(errors.New("removable skill meta not found")) - return + return liveschema.PlayResp{}, errors.New("removable skill meta not found") } if meta.FixedValueFlag == 1 { @@ -174,37 +183,28 @@ func play(ctx *gin.Context) { kissCool += meta.EffectValue } // fmt.Println("吻、眼神属性加成:", kissSmile, kissPure, kissCool) - } else { - // 仅效果类型为1、2、3的有属性加成 - if meta.EffectType == 1 || meta.EffectType == 2 || meta.EffectType == 3 { - // 加成范围:2:全员 1:非全员 - if meta.EffectRange == 2 { - switch meta.EffectType { - case 1: - skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100)) - case 2: - skillPure += math.Ceil(basePure * (meta.EffectValue / 100)) - case 3: - skillCool += math.Ceil(baseCool * (meta.EffectValue / 100)) - } - // fmt.Println("全员类宝石属性加成:", skillSmile, skillPure, skillCool) - } else { - // refType: 1 -> 年级类加成, target_type -> 指定年级(这里不需要使用,因为能装上宝石肯定是符合的) - // refType: 2 -> 个宝 - // refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石 - if meta.TargetReferenceType == 1 || meta.TargetReferenceType == 2 { // 年级类和个宝都是百分比加成 - switch meta.EffectType { - case 1: - skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100)) - case 2: - skillPure += math.Ceil(basePure * (meta.EffectValue / 100)) - case 3: - skillCool += math.Ceil(baseCool * (meta.EffectValue / 100)) - } - // fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool) - } - } + continue + } + + // 仅效果类型为1、2、3的有属性加成 + if meta.EffectType != 1 && meta.EffectType != 2 && meta.EffectType != 3 { + continue + } + + // 加成范围:2:全员 1:非全员 + if meta.EffectRange == 2 || meta.TargetReferenceType == 1 || meta.TargetReferenceType == 2 { + // refType: 1 -> 年级类加成, target_type -> 指定年级(这里不需要使用,因为能装上宝石肯定是符合的) + // refType: 2 -> 个宝 + // refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石 + switch meta.EffectType { + case 1: + skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100)) + case 2: + skillPure += math.Ceil(basePure * (meta.EffectValue / 100)) + case 3: + skillCool += math.Ceil(baseCool * (meta.EffectValue / 100)) } + // fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool) } } @@ -223,8 +223,8 @@ func play(ctx *gin.Context) { // 主唱技能加成:副C技能 matched, err := matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, myLeaderSkill.MemberTagID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } var mySubSmile, mySubPure, mySubCool float64 @@ -247,9 +247,10 @@ func play(ctx *gin.Context) { // 好友主唱技能加成:副C技能 matched, err = matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, tomoLeaderSkill.MemberTagID) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.PlayResp{}, err } + var tomoSubSmile, tomoSubPure, tomoSubCool float64 if matched { tomoSubSmile, tomoSubPure, tomoSubCool = applyAttributeBonus( @@ -264,7 +265,7 @@ func play(ctx *gin.Context) { totalSmile += smileMax + myCenterSmile + mySubSmile + tomoCenterSmile + tomoSubSmile totalPure += pureMax + myCenterPure + mySubPure + tomoCenterPure + tomoSubPure totalCool += coolMax + myCenterCool + mySubCool + tomoCenterCool + tomoSubCool - totalHp += unitData.MaxHp + totalHP += unitData.MaxHp // fmt.Println("smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, totalSmile", // smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, // smileMax+myCenterSmile+mySubSmile+tomoCenterSmile+tomoSubSmile) @@ -276,45 +277,38 @@ func play(ctx *gin.Context) { // coolMax+myCenterCool+mySubCool+tomoCenterCool+tomoSubCool) // 单卡属性计算结果取上取整 - fixedSmileMax := int(smileMax) - fixedPureMax := int(pureMax) - fixedCoolMax := int(coolMax) - // fmt.Println("单卡属性:", fixedSmileMax, fixedPureMax, fixedCoolMax) - unitList = append(unitList, liveschema.UnitList{ - Smile: fixedSmileMax, - Cute: fixedPureMax, - Cool: fixedCoolMax, + Smile: int(smileMax), + Cute: int(pureMax), + Cool: int(coolMax), }) + // fmt.Println("单卡属性:", int(smileMax), int(pureMax), int(coolMax)) } // 全部卡属性计算结果取上取整 - fixedTotalSmile := int(math.Ceil(totalSmile)) - fixedTotalPure := int(math.Ceil(totalPure)) - fixedTotalCool := int(math.Ceil(totalCool)) - // fmt.Println("全卡组属性:", fixedTotalSmile, fixedTotalPure, fixedTotalCool) - - lives := []liveschema.PlayLiveList{} - lives = append(lives, liveschema.PlayLiveList{ - LiveInfo: liveschema.LiveInfo{ - LiveDifficultyID: difficultyID, - IsRandom: false, - AcFlag: liveSetting.AcFlag, - SwingFlag: liveSetting.SwingFlag, - NotesList: notes, + lives := []liveschema.PlayLiveList{ + { + LiveInfo: liveschema.LiveInfo{ + LiveDifficultyID: difficultyID, + IsRandom: isRandom, + AcFlag: liveSetting.AcFlag, + SwingFlag: liveSetting.SwingFlag, + NotesList: notes, + }, + DeckInfo: liveschema.DeckInfo{ + UnitDeckID: playReq.UnitDeckID, + TotalSmile: int(math.Ceil(totalSmile)), + TotalCute: int(math.Ceil(totalPure)), + TotalCool: int(math.Ceil(totalCool)), + TotalHp: totalHP, + PreparedHpDamage: 0, + UnitList: unitList, + }, }, - DeckInfo: liveschema.DeckInfo{ - UnitDeckID: playReq.UnitDeckID, - TotalSmile: fixedTotalSmile, - TotalCute: fixedTotalPure, - TotalCool: fixedTotalCool, - TotalHp: totalHp, - PreparedHpDamage: 0, - UnitList: unitList, - }, - }) + } + // fmt.Println("全卡组属性:", int(math.Ceil(totalSmile)), int(math.Ceil(totalPure)), int(math.Ceil(totalCool))) - playResp := liveschema.PlayResp{ + return liveschema.PlayResp{ ResponseData: liveschema.PlayData{ RankInfo: ranks, EnergyFullTime: "2023-03-20 01:28:55", @@ -329,9 +323,7 @@ func play(ctx *gin.Context) { }, ReleaseInfo: []any{}, StatusCode: 200, - } - - ss.Respond(playResp) + }, nil } func init() { diff --git a/internal/handler/live/precisescore.go b/internal/handler/live/precisescore.go index 8254ed6..86ac327 100644 --- a/internal/handler/live/precisescore.go +++ b/internal/handler/live/precisescore.go @@ -10,7 +10,6 @@ import ( liverecordschema "honoka-chan/internal/schema/liverecord" "honoka-chan/internal/session" honokautils "honoka-chan/internal/utils" - "strconv" "time" "github.com/gin-gonic/gin" @@ -27,7 +26,7 @@ func preciseScore(ctx *gin.Context) { } // fmt.Println(ctx.MustGet("request_data").(string)) - difficultyID, _ := strconv.Atoi(playScoreReq.LiveDifficultyID) + difficultyID := int(playScoreReq.LiveDifficultyID) liveSetting, err := loadLiveSetting(ss, difficultyID) if ss.CheckErr(err) { @@ -43,6 +42,14 @@ func preciseScore(ctx *gin.Context) { // 检查是否正在进行 Live progress, _ := ss.GetLiveInProgress() + isRandom := false + if progress { + hasRandomLive, _, err := ss.GetActiveRandomLiveByDifficulty(difficultyID) + if ss.CheckErr(err) { + return + } + isRandom = hasRandomLive + } // 检查是否有 Live 记录 liveRecord := ss.GetUserLiveRecord(difficultyID) @@ -60,7 +67,7 @@ func preciseScore(ctx *gin.Context) { HasRecord: false, LiveInfo: liveschema.LiveInfo{ LiveDifficultyID: difficultyID, - IsRandom: false, + IsRandom: isRandom, AcFlag: liveSetting.AcFlag, SwingFlag: liveSetting.SwingFlag, NotesList: notesList, @@ -70,7 +77,7 @@ func preciseScore(ctx *gin.Context) { HasRecord: false, LiveInfo: liveschema.LiveInfo{ LiveDifficultyID: difficultyID, - IsRandom: false, + IsRandom: isRandom, AcFlag: liveSetting.AcFlag, SwingFlag: liveSetting.SwingFlag, NotesList: notesList, diff --git a/internal/handler/live/reward.go b/internal/handler/live/reward.go index de36c71..1b6c0ab 100644 --- a/internal/handler/live/reward.go +++ b/internal/handler/live/reward.go @@ -1,6 +1,7 @@ package live import ( + "errors" "honoka-chan/internal/middleware" usermodel "honoka-chan/internal/model/user" "honoka-chan/internal/router" @@ -16,7 +17,9 @@ import ( func reward(ctx *gin.Context) { ss := session.Get(ctx) defer func() { - ss.ClearLiveInProgress() + if ss.UserEng != nil { + ss.ClearLiveInProgress() + } ss.Finalize() }() @@ -26,37 +29,53 @@ func reward(ctx *gin.Context) { return } + playResp, err := BuildRewardResp(ss, playRewardReq, false) + if ss.CheckErr(err) { + return + } + + ss.Respond(playResp) +} + +func BuildRewardResp(ss *session.Session, playRewardReq liveschema.RewardReq, isRandom bool) (liveschema.RewardResp, error) { difficultyID := playRewardReq.LiveDifficultyID _, liveInfo := ss.GetLiveInfo(difficultyID) + if liveInfo == nil { + return liveschema.RewardResp{}, errors.New("live info not found") + } + liveInfo.IsRandom = isRandom _, progress := ss.GetLiveInProgress() + if progress == nil { + return liveschema.RewardResp{}, errors.New("live progress not found") + } _, deckInfo := ss.GetDeckInfo(progress.DeckID) deckInfo.LiveDifficultyID = difficultyID unitsList := []liveschema.PlayRewardUnitList{} - err = ss.UserEng.Table("user_deck_unit"). + err := ss.UserEng.Table("user_deck_unit"). Join("LEFT", "user_deck", "user_deck_unit.user_deck_id = user_deck.id"). Where("user_deck.user_id = ? AND user_deck.deck_id = ?", ss.UserID, progress.DeckID). Find(&unitsList) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.RewardResp{}, err } totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute beforeLiveStatus, afterLiveStatus, err := ss.UpdateUserLiveStatus(difficultyID, totalScore, playRewardReq.MaxCombo) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.RewardResp{}, err } newGoalRewardIDs, err := ss.SyncUserLiveGoals(difficultyID, afterLiveStatus.HiScore, afterLiveStatus.HiComboCount, afterLiveStatus.ClearCnt) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.RewardResp{}, err } goalAccomplishedIDs := make([]any, 0, len(newGoalRewardIDs)) goalRewards := make([]any, 0, len(newGoalRewardIDs)) if len(newGoalRewardIDs) > 0 { goalRewardRows, err := ss.GetLiveGoalRewardRowsByIDs(newGoalRewardIDs) - if ss.CheckErr(err) { - return + if err != nil { + return liveschema.RewardResp{}, err } for _, goalRewardID := range newGoalRewardIDs { goalAccomplishedIDs = append(goalAccomplishedIDs, goalRewardID) @@ -77,7 +96,7 @@ func reward(ctx *gin.Context) { LiveInfo: []liveschema.RewardLiveInfo{ { LiveDifficultyID: difficultyID, - IsRandom: false, + IsRandom: isRandom, AcFlag: liveInfo.AcFlag, SwingFlag: liveInfo.SwingFlag, }, @@ -205,7 +224,7 @@ func reward(ctx *gin.Context) { ss.UpdateUserLiveRecord(&newLiveRecord, liveSetting.PreciseScoreUpdateType) } - ss.Respond(playResp) + return playResp, nil } func init() { diff --git a/internal/handler/rlive/common.go b/internal/handler/rlive/common.go new file mode 100644 index 0000000..98decef --- /dev/null +++ b/internal/handler/rlive/common.go @@ -0,0 +1,39 @@ +package rlive + +import ( + "errors" + usermodel "honoka-chan/internal/model/user" + "honoka-chan/internal/session" +) + +func getRandomLiveByToken(ss *session.Session, token string) (usermodel.UserLiveRandom, error) { + if token == "" { + return usermodel.UserLiveRandom{}, errors.New("random live token is required") + } + + row := usermodel.UserLiveRandom{} + has, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ? AND token = ?", ss.UserID, token). + Get(&row) + if err != nil { + return usermodel.UserLiveRandom{}, err + } + if !has { + return usermodel.UserLiveRandom{}, errors.New("random live session not found") + } + + return row, nil +} + +func deleteRandomLiveByToken(ss *session.Session, token string) { + if token == "" || ss.UserEng == nil { + return + } + + _, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ? AND token = ?", ss.UserID, token). + Delete() + if ss.CheckErr(err) { + return + } +} diff --git a/internal/handler/rlive/continue.go b/internal/handler/rlive/continue.go new file mode 100644 index 0000000..cd1649c --- /dev/null +++ b/internal/handler/rlive/continue.go @@ -0,0 +1,41 @@ +package rlive + +import ( + "errors" + livehandler "honoka-chan/internal/handler/live" + "honoka-chan/internal/middleware" + "honoka-chan/internal/router" + rliveschema "honoka-chan/internal/schema/rlive" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +func continuee(ctx *gin.Context) { + ss := session.Get(ctx) + defer ss.Finalize() + + req := rliveschema.TokenReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + + _, err = getRandomLiveByToken(ss, req.Token) + if ss.CheckErr(err) { + return + } + + hasProgress, _ := ss.GetLiveInProgress() + if !hasProgress { + ss.CheckErr(errors.New("live progress not found")) + return + } + + ss.Respond(livehandler.BuildContinueResp(ss)) +} + +func init() { + router.AddHandler("main.php", "POST", "/rlive/continue", middleware.Common, continuee) +} diff --git a/internal/handler/rlive/gameover.go b/internal/handler/rlive/gameover.go new file mode 100644 index 0000000..34f4256 --- /dev/null +++ b/internal/handler/rlive/gameover.go @@ -0,0 +1,42 @@ +package rlive + +import ( + livehandler "honoka-chan/internal/handler/live" + "honoka-chan/internal/middleware" + "honoka-chan/internal/router" + rliveschema "honoka-chan/internal/schema/rlive" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +func gameOver(ctx *gin.Context) { + ss := session.Get(ctx) + token := "" + defer func() { + if ss.UserEng != nil { + deleteRandomLiveByToken(ss, token) + ss.ClearLiveInProgress() + } + ss.Finalize() + }() + + req := rliveschema.TokenReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + token = req.Token + + _, err = getRandomLiveByToken(ss, req.Token) + if ss.CheckErr(err) { + return + } + + ss.Respond(livehandler.BuildGameOverResp()) +} + +func init() { + router.AddHandler("main.php", "POST", "/rlive/gameover", middleware.Common, gameOver) +} diff --git a/internal/handler/rlive/lot.go b/internal/handler/rlive/lot.go new file mode 100644 index 0000000..d946a6d --- /dev/null +++ b/internal/handler/rlive/lot.go @@ -0,0 +1,251 @@ +package rlive + +import ( + "crypto/rand" + "errors" + livehandler "honoka-chan/internal/handler/live" + "honoka-chan/internal/middleware" + usermodel "honoka-chan/internal/model/user" + "honoka-chan/internal/router" + rliveschema "honoka-chan/internal/schema/rlive" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + pkgutils "honoka-chan/pkg/utils" + "math/big" + "os" + "path/filepath" + "time" + + "github.com/gin-gonic/gin" +) + +var rliveCST = time.FixedZone("CST", 8*3600) + +type randomLiveCandidateRow struct { + LiveDifficultyID int `xorm:"live_difficulty_id"` + NotesSettingAsset string `xorm:"notes_setting_asset"` + AcFlag int `xorm:"ac_flag"` + SwingFlag int `xorm:"swing_flag"` +} + +func lot(ctx *gin.Context) { + ss := session.Get(ctx) + defer ss.Finalize() + + req := rliveschema.LotReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + + if err := validateLotReq(req, time.Now()); ss.CheckErr(err) { + return + } + + randomLive, err := loadOrCreateRandomLive(ss, req) + if ss.CheckErr(err) { + return + } + + partyListData, err := livehandler.BuildPartyListData(ss) + if ss.CheckErr(err) { + return + } + + ss.Respond(rliveschema.LotResp{ + ResponseData: rliveschema.LotData{ + LiveInfo: rliveschema.LiveInfo{ + LiveDifficultyID: randomLive.LiveDifficultyID, + IsRandom: true, + AcFlag: randomLive.AcFlag, + SwingFlag: randomLive.SwingFlag, + }, + HasSlideNotes: randomLive.SwingFlag, + PartyList: partyListData.PartyList, + TrainingEnergy: partyListData.TrainingEnergy, + TrainingEnergyMax: partyListData.TrainingEnergyMax, + Token: randomLive.Token, + }, + ReleaseInfo: []any{}, + StatusCode: 200, + }) +} + +type randomLiveResult struct { + Token string + LiveDifficultyID int + AcFlag int + SwingFlag int +} + +func loadOrCreateRandomLive(ss *session.Session, req rliveschema.LotReq) (randomLiveResult, error) { + sessionRow := usermodel.UserLiveRandom{} + has, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ?", ss.UserID). + Where("attribute = ?", req.Attribute). + Where("difficulty = ?", req.Difficulty). + Where("member_category = ?", req.MemberCategory). + Get(&sessionRow) + if err != nil { + return randomLiveResult{}, err + } + + if has { + liveInfo, err := loadRandomLiveInfo(ss, sessionRow.LiveDifficultyID) + if err != nil { + return randomLiveResult{}, err + } + return randomLiveResult{ + Token: sessionRow.Token, + LiveDifficultyID: sessionRow.LiveDifficultyID, + AcFlag: liveInfo.AcFlag, + SwingFlag: liveInfo.SwingFlag, + }, nil + } + + candidates, err := listRandomLiveCandidates(ss, req.Difficulty, req.Attribute, req.MemberCategory) + if err != nil { + return randomLiveResult{}, err + } + if len(candidates) == 0 { + return randomLiveResult{}, errors.New("no random live candidates available") + } + + index, err := cryptoRandIndex(len(candidates)) + if err != nil { + return randomLiveResult{}, err + } + picked := candidates[index] + token := pkgutils.RandomStr(64) + if token == "" { + return randomLiveResult{}, errors.New("failed to generate random live token") + } + + _, err = ss.UserEng.Insert(&usermodel.UserLiveRandom{ + UserID: ss.UserID, + Attribute: req.Attribute, + Difficulty: req.Difficulty, + MemberCategory: req.MemberCategory, + Token: token, + LiveDifficultyID: picked.LiveDifficultyID, + InProgress: false, + }) + if err != nil { + return randomLiveResult{}, err + } + + return randomLiveResult{ + Token: token, + LiveDifficultyID: picked.LiveDifficultyID, + AcFlag: picked.AcFlag, + SwingFlag: picked.SwingFlag, + }, nil +} + +func validateLotReq(req rliveschema.LotReq, now time.Time) error { + if req.Difficulty < 1 || req.Difficulty > 4 { + return errors.New("invalid difficulty") + } + if req.Attribute < 1 || req.Attribute > 3 { + return errors.New("invalid attribute") + } + if req.MemberCategory < 0 { + return errors.New("invalid member category") + } + if int(now.In(rliveCST).Weekday())%3+1 != req.Attribute { + return errors.New("attribute does not match current random live rotation") + } + return nil +} + +func listRandomLiveCandidates(ss *session.Session, difficulty, attribute, memberCategory int) ([]randomLiveCandidateRow, error) { + rows := []randomLiveCandidateRow{} + + sql := ` +SELECT + difficulty.live_difficulty_id, + setting.notes_setting_asset, + setting.ac_flag, + setting.swing_flag +FROM live_setting_m AS setting +INNER JOIN ( + SELECT live_setting_id, live_difficulty_id FROM special_live_m + UNION + SELECT live_setting_id, live_difficulty_id FROM normal_live_m +) AS difficulty ON setting.live_setting_id = difficulty.live_setting_id +INNER JOIN live_track_m AS track ON track.live_track_id = setting.live_track_id +WHERE setting.difficulty = ? AND setting.attribute_icon_id = ? +` + args := []any{difficulty, attribute} + if memberCategory > 0 { + sql += ` AND track.member_category = ?` + args = append(args, memberCategory) + } + sql += ` +ORDER BY difficulty.live_difficulty_id ASC +` + + err := ss.MainEng.SQL(sql, args...).Find(&rows) + if err != nil { + return nil, err + } + + result := make([]randomLiveCandidateRow, 0, len(rows)) + for _, row := range rows { + if !hasRandomLiveBeatmap(row.NotesSettingAsset) { + continue + } + result = append(result, row) + } + return result, nil +} + +func loadRandomLiveInfo(ss *session.Session, liveDifficultyID int) (randomLiveCandidateRow, error) { + rows := []randomLiveCandidateRow{} + err := ss.MainEng.SQL(` +SELECT + difficulty.live_difficulty_id, + setting.notes_setting_asset, + setting.ac_flag, + setting.swing_flag +FROM live_setting_m AS setting +INNER JOIN ( + SELECT live_setting_id, live_difficulty_id FROM special_live_m + UNION + SELECT live_setting_id, live_difficulty_id FROM normal_live_m +) AS difficulty ON setting.live_setting_id = difficulty.live_setting_id +WHERE difficulty.live_difficulty_id = ? +LIMIT 1 +`, liveDifficultyID).Find(&rows) + if err != nil { + return randomLiveCandidateRow{}, err + } + if len(rows) == 0 { + return randomLiveCandidateRow{}, errors.New("random live difficulty not found") + } + return rows[0], nil +} + +func hasRandomLiveBeatmap(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 cryptoRandIndex(length int) (int, error) { + if length <= 0 { + return 0, errors.New("invalid random candidate length") + } + n, err := rand.Int(rand.Reader, big.NewInt(int64(length))) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + +func init() { + router.AddHandler("main.php", "POST", "/rlive/lot", middleware.Common, lot) +} diff --git a/internal/handler/rlive/play.go b/internal/handler/rlive/play.go new file mode 100644 index 0000000..bcd0996 --- /dev/null +++ b/internal/handler/rlive/play.go @@ -0,0 +1,55 @@ +package rlive + +import ( + livehandler "honoka-chan/internal/handler/live" + "honoka-chan/internal/middleware" + usermodel "honoka-chan/internal/model/user" + "honoka-chan/internal/router" + rliveschema "honoka-chan/internal/schema/rlive" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +func play(ctx *gin.Context) { + ss := session.Get(ctx) + defer ss.Finalize() + + req := rliveschema.PlayReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + + randomLive, err := getRandomLiveByToken(ss, req.Token) + if ss.CheckErr(err) { + return + } + + if err := ss.ResetRandomLiveInProgress(); ss.CheckErr(err) { + return + } + + ss.ClearLiveInProgress() + ss.RegisterLiveInProgress(req.UnitDeckID) + + _, err = ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ? AND token = ?", ss.UserID, req.Token). + Cols("in_progress"). + Update(&usermodel.UserLiveRandom{InProgress: true}) + if ss.CheckErr(err) { + return + } + + playResp, err := livehandler.BuildPlayResp(ss, req.ToLivePlayReq(randomLive.LiveDifficultyID), true) + if ss.CheckErr(err) { + return + } + + ss.Respond(playResp) +} + +func init() { + router.AddHandler("main.php", "POST", "/rlive/play", middleware.Common, play) +} diff --git a/internal/handler/rlive/reward.go b/internal/handler/rlive/reward.go new file mode 100644 index 0000000..d0ffe2d --- /dev/null +++ b/internal/handler/rlive/reward.go @@ -0,0 +1,59 @@ +package rlive + +import ( + "errors" + livehandler "honoka-chan/internal/handler/live" + "honoka-chan/internal/middleware" + "honoka-chan/internal/router" + rliveschema "honoka-chan/internal/schema/rlive" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +func reward(ctx *gin.Context) { + ss := session.Get(ctx) + token := "" + defer func() { + if ss.UserEng != nil { + deleteRandomLiveByToken(ss, token) + ss.ClearLiveInProgress() + } + ss.Finalize() + }() + + req := rliveschema.RewardReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + token = req.Token + + randomLive, err := getRandomLiveByToken(ss, req.Token) + if ss.CheckErr(err) { + return + } + + hasProgress, _ := ss.GetLiveInProgress() + if !hasProgress { + ss.CheckErr(errors.New("live progress not found")) + return + } + + if req.LiveDifficultyID != 0 && req.LiveDifficultyID != randomLive.LiveDifficultyID { + ss.CheckErr(errors.New("random live difficulty mismatch")) + return + } + + rewardResp, err := livehandler.BuildRewardResp(ss, req.ToLiveRewardReq(randomLive.LiveDifficultyID), true) + if ss.CheckErr(err) { + return + } + + ss.Respond(rewardResp) +} + +func init() { + router.AddHandler("main.php", "POST", "/rlive/reward", middleware.Common, reward) +} diff --git a/internal/model/user/userliverandom.go b/internal/model/user/userliverandom.go new file mode 100644 index 0000000..1e5f1dd --- /dev/null +++ b/internal/model/user/userliverandom.go @@ -0,0 +1,15 @@ +package usermodel + +type UserLiveRandom struct { + UserID int `xorm:"user_id pk"` + Attribute int `xorm:"attribute pk"` + Difficulty int `xorm:"difficulty pk"` + MemberCategory int `xorm:"member_category pk"` + Token string `xorm:"token"` + LiveDifficultyID int `xorm:"live_difficulty_id"` + InProgress bool `xorm:"in_progress"` +} + +func (UserLiveRandom) TableName() string { + return "user_live_random" +} diff --git a/internal/schema/live/precisescore.go b/internal/schema/live/precisescore.go index 0c271f9..69c6932 100644 --- a/internal/schema/live/precisescore.go +++ b/internal/schema/live/precisescore.go @@ -1,12 +1,39 @@ package liveschema +import ( + "encoding/json" + "strconv" +) + +type FlexibleLiveDifficultyID int + +func (id *FlexibleLiveDifficultyID) UnmarshalJSON(data []byte) error { + var intValue int + if err := json.Unmarshal(data, &intValue); err == nil { + *id = FlexibleLiveDifficultyID(intValue) + return nil + } + + var stringValue string + if err := json.Unmarshal(data, &stringValue); err != nil { + return err + } + + parsed, err := strconv.Atoi(stringValue) + if err != nil { + return err + } + *id = FlexibleLiveDifficultyID(parsed) + return nil +} + type PlayScoreReq struct { - Module string `json:"module"` - Action string `json:"action"` - TimeStamp int64 `json:"timeStamp"` - Mgd int `json:"mgd"` - LiveDifficultyID string `json:"live_difficulty_id"` - CommandNum string `json:"commandNum"` + Module string `json:"module"` + Action string `json:"action"` + TimeStamp int64 `json:"timeStamp"` + Mgd int `json:"mgd"` + LiveDifficultyID FlexibleLiveDifficultyID `json:"live_difficulty_id"` + CommandNum string `json:"commandNum"` } type On struct { diff --git a/internal/schema/rlive/common.go b/internal/schema/rlive/common.go new file mode 100644 index 0000000..dcbe73f --- /dev/null +++ b/internal/schema/rlive/common.go @@ -0,0 +1,10 @@ +package rliveschema + +type TokenReq struct { + Module string `json:"module"` + Action string `json:"action"` + Mgd int `json:"mgd"` + Token string `json:"token"` + TimeStamp int64 `json:"timeStamp"` + CommandNum string `json:"commandNum"` +} diff --git a/internal/schema/rlive/lot.go b/internal/schema/rlive/lot.go new file mode 100644 index 0000000..445904a --- /dev/null +++ b/internal/schema/rlive/lot.go @@ -0,0 +1,36 @@ +package rliveschema + +import liveschema "honoka-chan/internal/schema/live" + +type LotReq struct { + Module string `json:"module"` + Action string `json:"action"` + MemberCategory int `json:"member_category"` + Mgd int `json:"mgd"` + Difficulty int `json:"difficulty"` + Attribute int `json:"attribute"` + TimeStamp int64 `json:"timeStamp"` + CommandNum string `json:"commandNum"` +} + +type LiveInfo struct { + LiveDifficultyID int `json:"live_difficulty_id"` + IsRandom bool `json:"is_random"` + AcFlag int `json:"ac_flag"` + SwingFlag int `json:"swing_flag"` +} + +type LotData struct { + LiveInfo LiveInfo `json:"live_info"` + HasSlideNotes int `json:"has_slide_notes"` + PartyList []liveschema.PartyList `json:"party_list"` + TrainingEnergy int `json:"training_energy"` + TrainingEnergyMax int `json:"training_energy_max"` + Token string `json:"token"` +} + +type LotResp struct { + ResponseData LotData `json:"response_data"` + ReleaseInfo []any `json:"release_info"` + StatusCode int `json:"status_code"` +} diff --git a/internal/schema/rlive/play.go b/internal/schema/rlive/play.go new file mode 100644 index 0000000..fccd9e0 --- /dev/null +++ b/internal/schema/rlive/play.go @@ -0,0 +1,31 @@ +package rliveschema + +import liveschema "honoka-chan/internal/schema/live" + +type PlayReq struct { + Module string `json:"module"` + PartyUserID int64 `json:"party_user_id"` + Action string `json:"action"` + Mgd int `json:"mgd"` + IsTraining bool `json:"is_training"` + UnitDeckID int `json:"unit_deck_id"` + Token string `json:"token"` + TimeStamp int `json:"timeStamp"` + LpFactor int `json:"lp_factor"` + CommandNum string `json:"commandNum"` +} + +func (req PlayReq) ToLivePlayReq(liveDifficultyID int) liveschema.PlayReq { + return liveschema.PlayReq{ + Module: req.Module, + PartyUserID: req.PartyUserID, + Action: req.Action, + Mgd: req.Mgd, + IsTraining: req.IsTraining, + UnitDeckID: req.UnitDeckID, + LiveDifficultyID: itoa(liveDifficultyID), + TimeStamp: req.TimeStamp, + LpFactor: req.LpFactor, + CommandNum: req.CommandNum, + } +} diff --git a/internal/schema/rlive/reward.go b/internal/schema/rlive/reward.go new file mode 100644 index 0000000..6455eaf --- /dev/null +++ b/internal/schema/rlive/reward.go @@ -0,0 +1,54 @@ +package rliveschema + +import liveschema "honoka-chan/internal/schema/live" + +type RewardReq struct { + Module string `json:"module"` + Action string `json:"action"` + GoodCnt int `json:"good_cnt"` + MissCnt int `json:"miss_cnt"` + IsTraining bool `json:"is_training"` + GreatCnt int `json:"great_cnt"` + CommandNum string `json:"commandNum"` + LoveCnt int `json:"love_cnt"` + RemainHp int `json:"remain_hp"` + MaxCombo int `json:"max_combo"` + ScoreSmile int `json:"score_smile"` + PerfectCnt int `json:"perfect_cnt"` + BadCnt int `json:"bad_cnt"` + Mgd int `json:"mgd"` + EventPoint int `json:"event_point"` + LiveDifficultyID int `json:"live_difficulty_id"` + TimeStamp int `json:"timeStamp"` + PreciseScoreLog liveschema.PreciseScoreLog `json:"precise_score_log"` + ScoreCute int `json:"score_cute"` + EventID any `json:"event_id"` + ScoreCool int `json:"score_cool"` + Token string `json:"token"` +} + +func (req RewardReq) ToLiveRewardReq(liveDifficultyID int) liveschema.RewardReq { + return liveschema.RewardReq{ + Module: req.Module, + Action: req.Action, + GoodCnt: req.GoodCnt, + MissCnt: req.MissCnt, + IsTraining: req.IsTraining, + GreatCnt: req.GreatCnt, + CommandNum: req.CommandNum, + LoveCnt: req.LoveCnt, + RemainHp: req.RemainHp, + MaxCombo: req.MaxCombo, + ScoreSmile: req.ScoreSmile, + PerfectCnt: req.PerfectCnt, + BadCnt: req.BadCnt, + Mgd: req.Mgd, + EventPoint: req.EventPoint, + LiveDifficultyID: liveDifficultyID, + TimeStamp: req.TimeStamp, + PreciseScoreLog: req.PreciseScoreLog, + ScoreCute: req.ScoreCute, + EventID: req.EventID, + ScoreCool: req.ScoreCool, + } +} diff --git a/internal/schema/rlive/util.go b/internal/schema/rlive/util.go new file mode 100644 index 0000000..3325a02 --- /dev/null +++ b/internal/schema/rlive/util.go @@ -0,0 +1,7 @@ +package rliveschema + +import "strconv" + +func itoa(v int) string { + return strconv.Itoa(v) +} diff --git a/internal/session/live.go b/internal/session/live.go index 12d6286..2c26400 100644 --- a/internal/session/live.go +++ b/internal/session/live.go @@ -10,6 +10,10 @@ import ( // https://github.com/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L554 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L360 func (ss *Session) GetLiveInProgress() (bool, *usermodel.UserLiveInProgress) { + if ss.UserEng == nil { + return false, nil + } + progress := usermodel.UserLiveInProgress{} has, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)). Where("user_id = ?", ss.UserID).Get(&progress) @@ -24,6 +28,10 @@ func (ss *Session) GetLiveInProgress() (bool, *usermodel.UserLiveInProgress) { // https://github.com/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L45 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L372 func (ss *Session) RegisterLiveInProgress(deckID int) { + if ss.UserEng == nil { + return + } + var err error has, progress := ss.GetLiveInProgress() if has { @@ -45,12 +53,39 @@ func (ss *Session) RegisterLiveInProgress(deckID int) { // https://github.com/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L554 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L366 func (ss *Session) ClearLiveInProgress() { + if ss.UserEng == nil { + return + } + _, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)).Where("user_id = ?", ss.UserID).Delete() if ss.CheckErr(err) { return } } +func (ss *Session) GetActiveRandomLiveByDifficulty(liveDifficultyID int) (bool, *usermodel.UserLiveRandom, error) { + randomLive := usermodel.UserLiveRandom{} + has, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ? AND live_difficulty_id = ? AND in_progress = ?", ss.UserID, liveDifficultyID, true). + Get(&randomLive) + if err != nil { + return false, nil, err + } + return has, &randomLive, nil +} + +func (ss *Session) ResetRandomLiveInProgress() error { + if ss.UserEng == nil { + return nil + } + + _, err := ss.UserEng.Table(new(usermodel.UserLiveRandom)). + Where("user_id = ? AND in_progress = ?", ss.UserID, true). + Cols("in_progress"). + Update(&usermodel.UserLiveRandom{InProgress: false}) + return err +} + func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo) { var info struct { LiveDifficultyID int `xorm:"live_difficulty_id"` diff --git a/internal/startup/database.go b/internal/startup/database.go index d5fb8fe..06f84d5 100644 --- a/internal/startup/database.go +++ b/internal/startup/database.go @@ -29,6 +29,7 @@ func CreateTables() { db.UserEng.Sync2(new(usermodel.UserLiveGoal)) db.UserEng.Sync2(new(usermodel.UserLiveStatus)) db.UserEng.Sync2(new(usermodel.UserLiveInProgress)) + db.UserEng.Sync2(new(usermodel.UserLiveRandom)) db.UserEng.Sync2(new(usermodel.UserLiveRecord)) db.UserEng.Sync2(new(usermodel.UserFriend)) db.UserEng.Sync2(new(usermodel.UserGreet))