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
BIN
View File
Binary file not shown.
+2
View File
@@ -25,6 +25,7 @@ type AppConfigs struct {
type Settings struct { type Settings struct {
ListenPort string `json:"listen_port"` ListenPort string `json:"listen_port"`
CdnServer string `json:"cdn_server"` CdnServer string `json:"cdn_server"`
UnlockAllSpecialRotation bool `json:"unlock_all_special_rotation"`
} }
func InitConfig() { func InitConfig() {
@@ -37,6 +38,7 @@ func DefaultConfigs() *AppConfigs {
Settings: Settings{ Settings: Settings{
ListenPort: "8080", ListenPort: "8080",
CdnServer: "http://127.0.0.1:8080/static", CdnServer: "http://127.0.0.1:8080/static",
UnlockAllSpecialRotation: false,
}, },
} }
} }
+1
View File
@@ -7,6 +7,7 @@
第一次启动 `honoka-chan` 后会生成 `config.json`。需要确认: 第一次启动 `honoka-chan` 后会生成 `config.json`。需要确认:
- `settings.cdn_server` 指向你的数据下载地址 - `settings.cdn_server` 指向你的数据下载地址
- `settings.unlock_all_special_rotation` 可选;设为 `true` 后会忽略日替时间限制,直接解锁全部日替特殊歌曲,不包含周替 `MASTER`
- 如果数据直接放在本项目的 `static` 目录下,通常可以配置成类似 `http://192.168.1.123/static` - 如果数据直接放在本项目的 `static` 目录下,通常可以配置成类似 `http://192.168.1.123/static`
如果 `cdn_server/{系统}/archives/99_0_115.zip` 存在,服务端会自动把它追加到更新下载列表;如果不存在,则会直接跳过。 如果 `cdn_server/{系统}/archives/99_0_115.zip` 存在,服务端会自动把它追加到更新下载列表;如果不存在,则会直接跳过。
+163 -51
View File
@@ -2,6 +2,7 @@ package live
import ( import (
"fmt" "fmt"
"honoka-chan/config"
liveapischema "honoka-chan/internal/schema/api/live" liveapischema "honoka-chan/internal/schema/api/live"
"honoka-chan/internal/session" "honoka-chan/internal/session"
"os" "os"
@@ -15,7 +16,7 @@ const (
randomLiveEndDate = "2038-01-19 12:14:07" randomLiveEndDate = "2038-01-19 12:14:07"
) )
var jst = time.FixedZone("JST", 9*3600) var cst = time.FixedZone("CST", 8*3600)
type liveAvailabilityRow struct { type liveAvailabilityRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"` LiveDifficultyID int `xorm:"live_difficulty_id"`
@@ -33,6 +34,12 @@ type specialLiveRotationRow struct {
BaseDate string `xorm:"base_date"` BaseDate string `xorm:"base_date"`
} }
type specialLiveRotationSchedule struct {
LiveDifficultyID int
StartTime time.Time
EndTime time.Time
}
func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) { func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
rows, err := listAvailableNormalLiveRows(ss) rows, err := listAvailableNormalLiveRows(ss)
if err != nil { if err != nil {
@@ -46,25 +53,6 @@ func listAvailableNormalLiveDifficultyIDs(ss *session.Session) ([]int, error) {
return result, nil 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) { func listAvailableNormalLiveRows(ss *session.Session) ([]liveAvailabilityRow, error) {
rows := []liveAvailabilityRow{} rows := []liveAvailabilityRow{}
err := ss.MainEng.Table("normal_live_m").Alias("live"). err := ss.MainEng.Table("normal_live_m").Alias("live").
@@ -155,11 +143,11 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
dayModulo int64 dayModulo int64
}{ }{
liveDifficultyID: row.LiveDifficultyID, 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)) groupIDs := make([]int, 0, len(grouped))
for groupID := range grouped { for groupID := range grouped {
groupIDs = append(groupIDs, groupID) groupIDs = append(groupIDs, groupID)
@@ -183,54 +171,174 @@ func listTodaySpecialRotationDifficultyIDs(ss *session.Session, now time.Time) (
return result, nil return result, nil
} }
func listAllSpecialRotationDifficultyIDs(ss *session.Session) (map[int]struct{}, error) { func listCurrentAndNextSpecialRotationSchedules(ss *session.Session, now time.Time) ([]specialLiveRotationSchedule, error) {
ids := []int{} if config.Conf.Settings.UnlockAllSpecialRotation {
return listAllSpecialRotationSchedules(ss)
}
rows := []specialLiveRotationRow{}
err := ss.MainEng.Table("special_live_rotation_m"). err := ss.MainEng.Table("special_live_rotation_m").
Cols("live_difficulty_id"). OrderBy("rotation_group_id ASC, base_date ASC").
Find(&ids) Find(&rows)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := make(map[int]struct{}, len(ids)) type rotationEntry struct {
for _, id := range ids { liveDifficultyID int
result[id] = struct{}{} 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 return result, nil
} }
func listAvailableTrainingLiveDifficultyIDs(ss *session.Session) ([]int, error) { func listAvailableTrainingLiveDifficultyIDs(ss *session.Session) ([]int, error) {
normalTrackIDs, err := listAvailableNormalLiveTrackIDs(ss)
if err != nil {
return nil, err
}
specialRows, err := listAvailableSpecialLiveRows(ss) specialRows, err := listAvailableSpecialLiveRows(ss)
if err != nil { if err != nil {
return nil, err 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) result := make([]int, 0)
for _, row := range specialRows { for _, row := range specialRows {
if _, ok := normalTrackIDSet[row.LiveTrackID]; !ok {
continue
}
if row.Difficulty <= 5 || row.AcFlag != 0 || row.ExcludeClearCountFlag != 0 { if row.Difficulty <= 5 || row.AcFlag != 0 || row.ExcludeClearCountFlag != 0 {
continue continue
} }
if _, ok := rotationIDs[row.LiveDifficultyID]; ok {
continue
}
result = append(result, row.LiveDifficultyID) result = append(result, row.LiveDifficultyID)
} }
@@ -244,7 +352,7 @@ func parseRotationBaseDate(value string) (time.Time, error) {
"2006/01/02 3:04:05", "2006/01/02 3:04:05",
} }
for _, layout := range layouts { for _, layout := range layouts {
t, err := time.ParseInLocation(layout, value, jst) t, err := time.ParseInLocation(layout, value, cst)
if err == nil { if err == nil {
return t, 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()) 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 { func nextDayStart(t time.Time) time.Time {
return startOfDay(t).Add(24 * time.Hour) return startOfDay(t).Add(24 * time.Hour)
} }
+6 -9
View File
@@ -11,20 +11,17 @@ import (
func liveSchedule(ctx *gin.Context) (res any, err error) { func liveSchedule(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx) ss := session.Get(ctx)
now := time.Now() schedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now())
liveIDs, err := listTodaySpecialRotationDifficultyIDs(ss, now)
if err != nil { if err != nil {
return nil, err return nil, err
} }
liveList := make([]liveapischema.LiveList, 0, len(liveIDs)) liveList := make([]liveapischema.LiveList, 0, len(schedules))
startDate := startOfDay(now.In(jst)).Format("2006-01-02 15:04:05") for _, schedule := range schedules {
endDate := nextDayStart(now.In(jst)).Format("2006-01-02 15:04:05")
for _, id := range liveIDs {
liveList = append(liveList, liveapischema.LiveList{ liveList = append(liveList, liveapischema.LiveList{
LiveDifficultyID: id, LiveDifficultyID: schedule.LiveDifficultyID,
StartDate: startDate, StartDate: schedule.StartTime.Format("2006-01-02 15:04:05"),
EndDate: endDate, EndDate: schedule.EndTime.Format("2006-01-02 15:04:05"),
IsRandom: false, IsRandom: false,
}) })
} }
+5 -1
View File
@@ -16,10 +16,14 @@ func liveStatus(ctx *gin.Context) (res any, err error) {
return nil, err return nil, err
} }
specialIDs, err := listTodaySpecialRotationDifficultyIDs(ss, time.Now()) specialSchedules, err := listCurrentAndNextSpecialRotationSchedules(ss, time.Now())
if err != nil { if err != nil {
return nil, err return nil, err
} }
specialIDs := make([]int, 0, len(specialSchedules))
for _, schedule := range specialSchedules {
specialIDs = append(specialIDs, schedule.LiveDifficultyID)
}
trainingIDs, err := listAvailableTrainingLiveDifficultyIDs(ss) trainingIDs, err := listAvailableTrainingLiveDifficultyIDs(ss)
if err != nil { if err != nil {
return nil, err return nil, err
+1
View File
@@ -26,6 +26,7 @@ import (
_ "honoka-chan/internal/handler/profile" _ "honoka-chan/internal/handler/profile"
_ "honoka-chan/internal/handler/ranking" _ "honoka-chan/internal/handler/ranking"
_ "honoka-chan/internal/handler/reward" _ "honoka-chan/internal/handler/reward"
_ "honoka-chan/internal/handler/rlive"
_ "honoka-chan/internal/handler/scenario" _ "honoka-chan/internal/handler/scenario"
_ "honoka-chan/internal/handler/secretbox" _ "honoka-chan/internal/handler/secretbox"
_ "honoka-chan/internal/handler/subscenario" _ "honoka-chan/internal/handler/subscenario"
+6 -2
View File
@@ -14,7 +14,11 @@ func continuee(ctx *gin.Context) {
ss := session.Get(ctx) ss := session.Get(ctx)
defer ss.Finalize() defer ss.Finalize()
ss.Respond(liveschema.ContinueResp{ ss.Respond(BuildContinueResp(ss))
}
func BuildContinueResp(ss *session.Session) liveschema.ContinueResp {
return liveschema.ContinueResp{
ResponseData: liveschema.ContinueData{ ResponseData: liveschema.ContinueData{
BeforeSnsCoin: ss.UserPref.SnsCoin, BeforeSnsCoin: ss.UserPref.SnsCoin,
AfterSnsCoin: ss.UserPref.SnsCoin, AfterSnsCoin: ss.UserPref.SnsCoin,
@@ -22,7 +26,7 @@ func continuee(ctx *gin.Context) {
}, },
ReleaseInfo: []any{}, ReleaseInfo: []any{},
StatusCode: 200, StatusCode: 200,
}) }
} }
func init() { func init() {
+8 -2
View File
@@ -12,15 +12,21 @@ import (
func gameOver(ctx *gin.Context) { func gameOver(ctx *gin.Context) {
ss := session.Get(ctx) ss := session.Get(ctx)
defer func() { defer func() {
if ss.UserEng != nil {
ss.ClearLiveInProgress() ss.ClearLiveInProgress()
}
ss.Finalize() ss.Finalize()
}() }()
ss.Respond(liveschema.GameOverResp{ ss.Respond(BuildGameOverResp())
}
func BuildGameOverResp() liveschema.GameOverResp {
return liveschema.GameOverResp{
ResponseData: []any{}, ResponseData: []any{},
ReleaseInfo: []any{}, ReleaseInfo: []any{},
StatusCode: 200, StatusCode: 200,
}) }
} }
func init() { func init() {
+25 -16
View File
@@ -14,24 +14,37 @@ import (
func partyList(ctx *gin.Context) { func partyList(ctx *gin.Context) {
ss := session.Get(ctx) ss := session.Get(ctx)
defer ss.Finalize() defer ss.Finalize()
userInfo := ss.GetUserInfo() partyListData, err := BuildPartyListData(ss)
pref := usermodel.UserPref{}
_, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if ss.CheckErr(err) { if ss.CheckErr(err) {
return 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) supportRows, err := listLiveSupportRows(ss)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PartyListData{}, err
} }
partyList := make([]liveschema.PartyList, 0, len(supportRows)) partyList := make([]liveschema.PartyList, 0, len(supportRows))
for _, row := range supportRows { for _, row := range supportRows {
party, err := buildLiveSupportParty(ss, row) party, err := buildLiveSupportParty(ss, row)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PartyListData{}, err
} }
partyList = append(partyList, party) partyList = append(partyList, party)
} }
@@ -44,22 +57,18 @@ func partyList(ctx *gin.Context) {
} }
selfParty.AvailableSocialPoint = 10 selfParty.AvailableSocialPoint = 10
selfParty.CenterUnitInfo, err = mustFindCenterUnitInfo(ss, ss.UserID, pref.UnitOwningUserID) selfParty.CenterUnitInfo, err = mustFindCenterUnitInfo(ss, ss.UserID, pref.UnitOwningUserID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PartyListData{}, err
} }
partyList = append(partyList, selfParty) partyList = append(partyList, selfParty)
} }
ss.Respond(liveschema.PartyListResp{ return liveschema.PartyListData{
ResponseData: liveschema.PartyListData{
PartyList: partyList, PartyList: partyList,
TrainingEnergy: userInfo.TrainingEnergy, TrainingEnergy: userInfo.TrainingEnergy,
TrainingEnergyMax: userInfo.TrainingEnergyMax, TrainingEnergyMax: userInfo.TrainingEnergyMax,
ServerTimestamp: time.Now().Unix(), ServerTimestamp: time.Now().Unix(),
}, }, nil
ReleaseInfo: []any{},
StatusCode: 200,
})
} }
func init() { func init() {
+86 -94
View File
@@ -25,93 +25,104 @@ func play(ctx *gin.Context) {
} }
// fmt.Println(ctx.MustGet("request_data").(string)) // fmt.Println(ctx.MustGet("request_data").(string))
if err := ss.ResetRandomLiveInProgress(); ss.CheckErr(err) {
return
}
ss.RegisterLiveInProgress(playReq.UnitDeckID) ss.RegisterLiveInProgress(playReq.UnitDeckID)
difficultyID, err := strconv.Atoi(playReq.LiveDifficultyID) playResp, err := BuildPlayResp(ss, playReq, false)
if ss.CheckErr(err) { if ss.CheckErr(err) {
return 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) liveSetting, err := loadLiveSetting(ss, difficultyID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
notes, err := loadLiveNotes(liveSetting.NotesSettingAsset) notes, err := loadLiveNotes(liveSetting.NotesSettingAsset)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
ranks := buildRankInfo(liveSetting) ranks := buildRankInfo(liveSetting)
owningIdList := []int{} owningIDList := []int{}
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"). 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). 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) Cols("unit_owning_user_id").OrderBy("user_deck_unit.position ASC").Find(&owningIDList)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
if len(owningIdList) == 0 { if len(owningIDList) == 0 {
ss.CheckErr(errors.New("deck has no units")) return liveschema.PlayResp{}, errors.New("deck has no units")
return
} }
museumBuff, err := getMuseumBuff(ss) museumBuff, err := getMuseumBuff(ss)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
myCenterUnitID, err := getDeckCenterUnitID(ss, playReq.UnitDeckID) myCenterUnitID, err := getDeckCenterUnitID(ss, playReq.UnitDeckID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
myLeaderSkill, err := getLeaderSkillData(ss, myCenterUnitID) myLeaderSkill, err := getLeaderSkillData(ss, myCenterUnitID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
tomoUnitID, err := getSupportCenterUnitID(ss, int(playReq.PartyUserID), myCenterUnitID) tomoUnitID, err := getSupportCenterUnitID(ss, int(playReq.PartyUserID), myCenterUnitID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
tomoLeaderSkill, err := getLeaderSkillData(ss, tomoUnitID) tomoLeaderSkill, err := getLeaderSkillData(ss, tomoUnitID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
memberTagCache := map[memberTagMatchKey]bool{} memberTagCache := map[memberTagMatchKey]bool{}
unitDataMap, err := loadDeckUnitDataMap(ss, owningIdList) unitDataMap, err := loadDeckUnitDataMap(ss, owningIDList)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIdList) accessoryBonusMap, err := loadAccessoryBonusMap(ss, ss.UserID, owningIDList)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIdList) removableSkillMap, err := loadDeckRemovableSkillMap(ss, ss.UserID, owningIDList)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
allRemovableSkillIDs := make([]int, 0) allRemovableSkillIDs := make([]int, 0)
for _, skillIDs := range removableSkillMap { for _, skillIDs := range removableSkillMap {
allRemovableSkillIDs = append(allRemovableSkillIDs, skillIDs...) allRemovableSkillIDs = append(allRemovableSkillIDs, skillIDs...)
} }
removableSkillMetaMap, err := loadRemovableSkillMetaMap(ss, allRemovableSkillIDs) removableSkillMetaMap, err := loadRemovableSkillMetaMap(ss, allRemovableSkillIDs)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
unitList := make([]liveschema.UnitList, 0, len(owningIdList)) unitList := make([]liveschema.UnitList, 0, len(owningIDList))
var totalSmile, totalPure, totalCool float64 var totalSmile, totalPure, totalCool float64
var totalHp int var totalHP int
for _, owningId := range owningIdList { for _, owningID := range owningIDList {
// 卡片基础属性 // 卡片基础属性
var baseSmile, basePure, baseCool, smileMax, pureMax, coolMax float64 var baseSmile, basePure, baseCool, smileMax, pureMax, coolMax float64
unitData, ok := unitDataMap[owningId] unitData, ok := unitDataMap[owningID]
if !ok { if !ok {
ss.CheckErr(errors.New("unit data not found in deck")) return liveschema.PlayResp{}, errors.New("unit data not found in deck")
return
} }
baseSmile = float64(unitData.Smile) baseSmile = float64(unitData.Smile)
basePure = float64(unitData.Cute) basePure = float64(unitData.Cute)
@@ -120,7 +131,7 @@ func play(ctx *gin.Context) {
// fmt.Println("基础属性:", baseSmile, basePure, baseCool) // fmt.Println("基础属性:", baseSmile, basePure, baseCool)
// 饰品属性加成(满级) // 饰品属性加成(满级)
if bonus, ok := accessoryBonusMap[owningId]; ok { if bonus, ok := accessoryBonusMap[owningID]; ok {
// 饰品属性加成(该加成会影响个宝等百分比宝石属性加成的计算,故先计算。) // 饰品属性加成(该加成会影响个宝等百分比宝石属性加成的计算,故先计算。)
baseSmile += bonus.Smile baseSmile += bonus.Smile
basePure += bonus.Pure basePure += bonus.Pure
@@ -153,14 +164,12 @@ func play(ctx *gin.Context) {
var skillSmile, skillPure, skillCool float64 var skillSmile, skillPure, skillCool float64
// 宝石加成(满级) // 宝石加成(满级)
removableSkillIds := removableSkillMap[owningId] removableSkillIDs := removableSkillMap[owningID]
for _, skillID := range removableSkillIDs {
for _, sk := range removableSkillIds {
// 判断宝石效果类型(效果范围、效果类型、效果值、是否固定数值) // 判断宝石效果类型(效果范围、效果类型、效果值、是否固定数值)
meta, ok := removableSkillMetaMap[sk] meta, ok := removableSkillMetaMap[skillID]
if !ok { if !ok {
ss.CheckErr(errors.New("removable skill meta not found")) return liveschema.PlayResp{}, errors.New("removable skill meta not found")
return
} }
if meta.FixedValueFlag == 1 { if meta.FixedValueFlag == 1 {
@@ -174,25 +183,19 @@ func play(ctx *gin.Context) {
kissCool += meta.EffectValue kissCool += meta.EffectValue
} }
// fmt.Println("吻、眼神属性加成:", kissSmile, kissPure, kissCool) // fmt.Println("吻、眼神属性加成:", kissSmile, kissPure, kissCool)
} else { continue
// 仅效果类型为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 { // 仅效果类型为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: 1 -> 年级类加成, target_type -> 指定年级(这里不需要使用,因为能装上宝石肯定是符合的)
// refType: 2 -> 个宝 // refType: 2 -> 个宝
// refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石 // refType: 3 -> 爆分、奶、判宝石, 0 -> 竞技场宝石
if meta.TargetReferenceType == 1 || meta.TargetReferenceType == 2 { // 年级类和个宝都是百分比加成
switch meta.EffectType { switch meta.EffectType {
case 1: case 1:
skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100)) skillSmile += math.Ceil(baseSmile * (meta.EffectValue / 100))
@@ -204,9 +207,6 @@ func play(ctx *gin.Context) {
// fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool) // fmt.Println("年级类宝石、个宝属性加成:", skillSmile, skillPure, skillCool)
} }
} }
}
}
}
// 单卡属性 // 单卡属性
smileMax = baseSmile + kissSmile + skillSmile smileMax = baseSmile + kissSmile + skillSmile
@@ -223,8 +223,8 @@ func play(ctx *gin.Context) {
// 主唱技能加成:副C技能 // 主唱技能加成:副C技能
matched, err := matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, myLeaderSkill.MemberTagID) matched, err := matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, myLeaderSkill.MemberTagID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
var mySubSmile, mySubPure, mySubCool float64 var mySubSmile, mySubPure, mySubCool float64
@@ -247,9 +247,10 @@ func play(ctx *gin.Context) {
// 好友主唱技能加成:副C技能 // 好友主唱技能加成:副C技能
matched, err = matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, tomoLeaderSkill.MemberTagID) matched, err = matchMemberTag(ss, memberTagCache, unitData.UnitTypeID, tomoLeaderSkill.MemberTagID)
if ss.CheckErr(err) { if err != nil {
return return liveschema.PlayResp{}, err
} }
var tomoSubSmile, tomoSubPure, tomoSubCool float64 var tomoSubSmile, tomoSubPure, tomoSubCool float64
if matched { if matched {
tomoSubSmile, tomoSubPure, tomoSubCool = applyAttributeBonus( tomoSubSmile, tomoSubPure, tomoSubCool = applyAttributeBonus(
@@ -264,7 +265,7 @@ func play(ctx *gin.Context) {
totalSmile += smileMax + myCenterSmile + mySubSmile + tomoCenterSmile + tomoSubSmile totalSmile += smileMax + myCenterSmile + mySubSmile + tomoCenterSmile + tomoSubSmile
totalPure += pureMax + myCenterPure + mySubPure + tomoCenterPure + tomoSubPure totalPure += pureMax + myCenterPure + mySubPure + tomoCenterPure + tomoSubPure
totalCool += coolMax + myCenterCool + mySubCool + tomoCenterCool + tomoSubCool totalCool += coolMax + myCenterCool + mySubCool + tomoCenterCool + tomoSubCool
totalHp += unitData.MaxHp totalHP += unitData.MaxHp
// fmt.Println("smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, totalSmile", // fmt.Println("smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, totalSmile",
// smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile, // smileMax, myCenterSmile, mySubSmile, tomoCenterSmile, tomoSubSmile,
// smileMax+myCenterSmile+mySubSmile+tomoCenterSmile+tomoSubSmile) // smileMax+myCenterSmile+mySubSmile+tomoCenterSmile+tomoSubSmile)
@@ -276,45 +277,38 @@ func play(ctx *gin.Context) {
// coolMax+myCenterCool+mySubCool+tomoCenterCool+tomoSubCool) // coolMax+myCenterCool+mySubCool+tomoCenterCool+tomoSubCool)
// 单卡属性计算结果取上取整 // 单卡属性计算结果取上取整
fixedSmileMax := int(smileMax)
fixedPureMax := int(pureMax)
fixedCoolMax := int(coolMax)
// fmt.Println("单卡属性:", fixedSmileMax, fixedPureMax, fixedCoolMax)
unitList = append(unitList, liveschema.UnitList{ unitList = append(unitList, liveschema.UnitList{
Smile: fixedSmileMax, Smile: int(smileMax),
Cute: fixedPureMax, Cute: int(pureMax),
Cool: fixedCoolMax, Cool: int(coolMax),
}) })
// fmt.Println("单卡属性:", int(smileMax), int(pureMax), int(coolMax))
} }
// 全部卡属性计算结果取上取整 // 全部卡属性计算结果取上取整
fixedTotalSmile := int(math.Ceil(totalSmile)) lives := []liveschema.PlayLiveList{
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{ LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID, LiveDifficultyID: difficultyID,
IsRandom: false, IsRandom: isRandom,
AcFlag: liveSetting.AcFlag, AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag, SwingFlag: liveSetting.SwingFlag,
NotesList: notes, NotesList: notes,
}, },
DeckInfo: liveschema.DeckInfo{ DeckInfo: liveschema.DeckInfo{
UnitDeckID: playReq.UnitDeckID, UnitDeckID: playReq.UnitDeckID,
TotalSmile: fixedTotalSmile, TotalSmile: int(math.Ceil(totalSmile)),
TotalCute: fixedTotalPure, TotalCute: int(math.Ceil(totalPure)),
TotalCool: fixedTotalCool, TotalCool: int(math.Ceil(totalCool)),
TotalHp: totalHp, TotalHp: totalHP,
PreparedHpDamage: 0, PreparedHpDamage: 0,
UnitList: unitList, 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{ ResponseData: liveschema.PlayData{
RankInfo: ranks, RankInfo: ranks,
EnergyFullTime: "2023-03-20 01:28:55", EnergyFullTime: "2023-03-20 01:28:55",
@@ -329,9 +323,7 @@ func play(ctx *gin.Context) {
}, },
ReleaseInfo: []any{}, ReleaseInfo: []any{},
StatusCode: 200, StatusCode: 200,
} }, nil
ss.Respond(playResp)
} }
func init() { func init() {
+11 -4
View File
@@ -10,7 +10,6 @@ import (
liverecordschema "honoka-chan/internal/schema/liverecord" liverecordschema "honoka-chan/internal/schema/liverecord"
"honoka-chan/internal/session" "honoka-chan/internal/session"
honokautils "honoka-chan/internal/utils" honokautils "honoka-chan/internal/utils"
"strconv"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -27,7 +26,7 @@ func preciseScore(ctx *gin.Context) {
} }
// fmt.Println(ctx.MustGet("request_data").(string)) // fmt.Println(ctx.MustGet("request_data").(string))
difficultyID, _ := strconv.Atoi(playScoreReq.LiveDifficultyID) difficultyID := int(playScoreReq.LiveDifficultyID)
liveSetting, err := loadLiveSetting(ss, difficultyID) liveSetting, err := loadLiveSetting(ss, difficultyID)
if ss.CheckErr(err) { if ss.CheckErr(err) {
@@ -43,6 +42,14 @@ func preciseScore(ctx *gin.Context) {
// 检查是否正在进行 Live // 检查是否正在进行 Live
progress, _ := ss.GetLiveInProgress() progress, _ := ss.GetLiveInProgress()
isRandom := false
if progress {
hasRandomLive, _, err := ss.GetActiveRandomLiveByDifficulty(difficultyID)
if ss.CheckErr(err) {
return
}
isRandom = hasRandomLive
}
// 检查是否有 Live 记录 // 检查是否有 Live 记录
liveRecord := ss.GetUserLiveRecord(difficultyID) liveRecord := ss.GetUserLiveRecord(difficultyID)
@@ -60,7 +67,7 @@ func preciseScore(ctx *gin.Context) {
HasRecord: false, HasRecord: false,
LiveInfo: liveschema.LiveInfo{ LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID, LiveDifficultyID: difficultyID,
IsRandom: false, IsRandom: isRandom,
AcFlag: liveSetting.AcFlag, AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag, SwingFlag: liveSetting.SwingFlag,
NotesList: notesList, NotesList: notesList,
@@ -70,7 +77,7 @@ func preciseScore(ctx *gin.Context) {
HasRecord: false, HasRecord: false,
LiveInfo: liveschema.LiveInfo{ LiveInfo: liveschema.LiveInfo{
LiveDifficultyID: difficultyID, LiveDifficultyID: difficultyID,
IsRandom: false, IsRandom: isRandom,
AcFlag: liveSetting.AcFlag, AcFlag: liveSetting.AcFlag,
SwingFlag: liveSetting.SwingFlag, SwingFlag: liveSetting.SwingFlag,
NotesList: notesList, NotesList: notesList,
+30 -11
View File
@@ -1,6 +1,7 @@
package live package live
import ( import (
"errors"
"honoka-chan/internal/middleware" "honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user" usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router" "honoka-chan/internal/router"
@@ -16,7 +17,9 @@ import (
func reward(ctx *gin.Context) { func reward(ctx *gin.Context) {
ss := session.Get(ctx) ss := session.Get(ctx)
defer func() { defer func() {
if ss.UserEng != nil {
ss.ClearLiveInProgress() ss.ClearLiveInProgress()
}
ss.Finalize() ss.Finalize()
}() }()
@@ -26,37 +29,53 @@ func reward(ctx *gin.Context) {
return 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 difficultyID := playRewardReq.LiveDifficultyID
_, liveInfo := ss.GetLiveInfo(difficultyID) _, liveInfo := ss.GetLiveInfo(difficultyID)
if liveInfo == nil {
return liveschema.RewardResp{}, errors.New("live info not found")
}
liveInfo.IsRandom = isRandom
_, progress := ss.GetLiveInProgress() _, progress := ss.GetLiveInProgress()
if progress == nil {
return liveschema.RewardResp{}, errors.New("live progress not found")
}
_, deckInfo := ss.GetDeckInfo(progress.DeckID) _, deckInfo := ss.GetDeckInfo(progress.DeckID)
deckInfo.LiveDifficultyID = difficultyID deckInfo.LiveDifficultyID = difficultyID
unitsList := []liveschema.PlayRewardUnitList{} 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"). 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). Where("user_deck.user_id = ? AND user_deck.deck_id = ?", ss.UserID, progress.DeckID).
Find(&unitsList) Find(&unitsList)
if ss.CheckErr(err) { if err != nil {
return return liveschema.RewardResp{}, err
} }
totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute
beforeLiveStatus, afterLiveStatus, err := ss.UpdateUserLiveStatus(difficultyID, totalScore, playRewardReq.MaxCombo) beforeLiveStatus, afterLiveStatus, err := ss.UpdateUserLiveStatus(difficultyID, totalScore, playRewardReq.MaxCombo)
if ss.CheckErr(err) { if err != nil {
return return liveschema.RewardResp{}, err
} }
newGoalRewardIDs, err := ss.SyncUserLiveGoals(difficultyID, afterLiveStatus.HiScore, afterLiveStatus.HiComboCount, afterLiveStatus.ClearCnt) newGoalRewardIDs, err := ss.SyncUserLiveGoals(difficultyID, afterLiveStatus.HiScore, afterLiveStatus.HiComboCount, afterLiveStatus.ClearCnt)
if ss.CheckErr(err) { if err != nil {
return return liveschema.RewardResp{}, err
} }
goalAccomplishedIDs := make([]any, 0, len(newGoalRewardIDs)) goalAccomplishedIDs := make([]any, 0, len(newGoalRewardIDs))
goalRewards := make([]any, 0, len(newGoalRewardIDs)) goalRewards := make([]any, 0, len(newGoalRewardIDs))
if len(newGoalRewardIDs) > 0 { if len(newGoalRewardIDs) > 0 {
goalRewardRows, err := ss.GetLiveGoalRewardRowsByIDs(newGoalRewardIDs) goalRewardRows, err := ss.GetLiveGoalRewardRowsByIDs(newGoalRewardIDs)
if ss.CheckErr(err) { if err != nil {
return return liveschema.RewardResp{}, err
} }
for _, goalRewardID := range newGoalRewardIDs { for _, goalRewardID := range newGoalRewardIDs {
goalAccomplishedIDs = append(goalAccomplishedIDs, goalRewardID) goalAccomplishedIDs = append(goalAccomplishedIDs, goalRewardID)
@@ -77,7 +96,7 @@ func reward(ctx *gin.Context) {
LiveInfo: []liveschema.RewardLiveInfo{ LiveInfo: []liveschema.RewardLiveInfo{
{ {
LiveDifficultyID: difficultyID, LiveDifficultyID: difficultyID,
IsRandom: false, IsRandom: isRandom,
AcFlag: liveInfo.AcFlag, AcFlag: liveInfo.AcFlag,
SwingFlag: liveInfo.SwingFlag, SwingFlag: liveInfo.SwingFlag,
}, },
@@ -205,7 +224,7 @@ func reward(ctx *gin.Context) {
ss.UpdateUserLiveRecord(&newLiveRecord, liveSetting.PreciseScoreUpdateType) ss.UpdateUserLiveRecord(&newLiveRecord, liveSetting.PreciseScoreUpdateType)
} }
ss.Respond(playResp) return playResp, nil
} }
func init() { func init() {
+39
View File
@@ -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
}
}
+41
View File
@@ -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)
}
+42
View File
@@ -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)
}
+251
View File
@@ -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)
}
+55
View File
@@ -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)
}
+59
View File
@@ -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)
}
+15
View File
@@ -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"
}
+28 -1
View File
@@ -1,11 +1,38 @@
package liveschema 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 { type PlayScoreReq struct {
Module string `json:"module"` Module string `json:"module"`
Action string `json:"action"` Action string `json:"action"`
TimeStamp int64 `json:"timeStamp"` TimeStamp int64 `json:"timeStamp"`
Mgd int `json:"mgd"` Mgd int `json:"mgd"`
LiveDifficultyID string `json:"live_difficulty_id"` LiveDifficultyID FlexibleLiveDifficultyID `json:"live_difficulty_id"`
CommandNum string `json:"commandNum"` CommandNum string `json:"commandNum"`
} }
+10
View File
@@ -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"`
}
+36
View File
@@ -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"`
}
+31
View File
@@ -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,
}
}
+54
View File
@@ -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,
}
}
+7
View File
@@ -0,0 +1,7 @@
package rliveschema
import "strconv"
func itoa(v int) string {
return strconv.Itoa(v)
}
+35
View File
@@ -10,6 +10,10 @@ import (
// https://github.com/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L554 // https://github.com/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L554
// https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L360 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L360
func (ss *Session) GetLiveInProgress() (bool, *usermodel.UserLiveInProgress) { func (ss *Session) GetLiveInProgress() (bool, *usermodel.UserLiveInProgress) {
if ss.UserEng == nil {
return false, nil
}
progress := usermodel.UserLiveInProgress{} progress := usermodel.UserLiveInProgress{}
has, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)). has, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)).
Where("user_id = ?", ss.UserID).Get(&progress) 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/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L45
// https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L372 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L372
func (ss *Session) RegisterLiveInProgress(deckID int) { func (ss *Session) RegisterLiveInProgress(deckID int) {
if ss.UserEng == nil {
return
}
var err error var err error
has, progress := ss.GetLiveInProgress() has, progress := ss.GetLiveInProgress()
if has { 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/YumeMichi/honoka-chan/blob/a749efad9fd0789668dedcb59948248da369d32c/handler/live.go#L554
// https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L366 // https://github.com/DarkEnergyProcessor/NPPS4/blob/29aaba6e7a3b4b80d414e58f4b511a9627cb0e24/npps4/system/live.py#L366
func (ss *Session) ClearLiveInProgress() { func (ss *Session) ClearLiveInProgress() {
if ss.UserEng == nil {
return
}
_, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)).Where("user_id = ?", ss.UserID).Delete() _, err := ss.UserEng.Table(new(usermodel.UserLiveInProgress)).Where("user_id = ?", ss.UserID).Delete()
if ss.CheckErr(err) { if ss.CheckErr(err) {
return 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) { func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo) {
var info struct { var info struct {
LiveDifficultyID int `xorm:"live_difficulty_id"` LiveDifficultyID int `xorm:"live_difficulty_id"`
+1
View File
@@ -29,6 +29,7 @@ func CreateTables() {
db.UserEng.Sync2(new(usermodel.UserLiveGoal)) db.UserEng.Sync2(new(usermodel.UserLiveGoal))
db.UserEng.Sync2(new(usermodel.UserLiveStatus)) db.UserEng.Sync2(new(usermodel.UserLiveStatus))
db.UserEng.Sync2(new(usermodel.UserLiveInProgress)) db.UserEng.Sync2(new(usermodel.UserLiveInProgress))
db.UserEng.Sync2(new(usermodel.UserLiveRandom))
db.UserEng.Sync2(new(usermodel.UserLiveRecord)) db.UserEng.Sync2(new(usermodel.UserLiveRecord))
db.UserEng.Sync2(new(usermodel.UserFriend)) db.UserEng.Sync2(new(usermodel.UserFriend))
db.UserEng.Sync2(new(usermodel.UserGreet)) db.UserEng.Sync2(new(usermodel.UserGreet))