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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user