440 lines
17 KiB
Go
440 lines
17 KiB
Go
package scoresaber
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
"time"
|
||
|
||
"git.lxtend.com/lixiangwuxian/imagedd/sprite"
|
||
"git.lxtend.com/lixiangwuxian/imagedd/text2img"
|
||
"git.lxtend.com/qqbot/message"
|
||
"git.lxtend.com/qqbot/util"
|
||
)
|
||
|
||
// Command 表示从 WebSocket 收到的数据结构
|
||
type Command struct {
|
||
CommandName string `json:"commandName"`
|
||
CommandData CommandData `json:"commandData"`
|
||
}
|
||
|
||
// type Badge struct {
|
||
// Description string `json:"description"`
|
||
// Image string `json:"image"`
|
||
// }
|
||
|
||
// LeaderboardPlayerInfo 表示玩家的信息
|
||
type LeaderboardPlayerInfo struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
ProfilePicture string `json:"profilePicture"`
|
||
Country string `json:"country"`
|
||
Permissions int `json:"permissions"`
|
||
}
|
||
|
||
// Score 表示分数的信息
|
||
type Score struct {
|
||
ID int `json:"id"`
|
||
LeaderboardPlayerInfo LeaderboardPlayerInfo `json:"leaderboardPlayerInfo"`
|
||
Rank int `json:"rank"`
|
||
BaseScore int `json:"baseScore"`
|
||
ModifiedScore int `json:"modifiedScore"`
|
||
Pp float64 `json:"pp"`
|
||
Weight float64 `json:"weight"`
|
||
Modifiers string `json:"modifiers"`
|
||
Multiplier float64 `json:"multiplier"`
|
||
BadCuts int `json:"badCuts"`
|
||
MissedNotes int `json:"missedNotes"`
|
||
MaxCombo int `json:"maxCombo"`
|
||
FullCombo bool `json:"fullCombo"`
|
||
Hmd int `json:"hmd"`
|
||
TimeSet time.Time `json:"timeSet"`
|
||
HasReplay bool `json:"hasReplay"`
|
||
DeviceHmd *string `json:"deviceHmd"`
|
||
DeviceControllerLeft *string `json:"deviceControllerLeft"`
|
||
DeviceControllerRight *string `json:"deviceControllerRight"`
|
||
}
|
||
|
||
// Difficulty 表示关卡难度信息
|
||
type Difficulty struct {
|
||
LeaderboardID int `json:"leaderboardId"`
|
||
Difficulty int `json:"difficulty"`
|
||
GameMode string `json:"gameMode"`
|
||
DifficultyRaw string `json:"difficultyRaw"`
|
||
}
|
||
|
||
// Leaderboard 表示排行榜的信息
|
||
type Leaderboard struct {
|
||
ID int `json:"id"`
|
||
SongHash string `json:"songHash"`
|
||
SongName string `json:"songName"`
|
||
SongSubName string `json:"songSubName"`
|
||
SongAuthorName string `json:"songAuthorName"`
|
||
LevelAuthorName string `json:"levelAuthorName"`
|
||
Difficulty Difficulty `json:"difficulty"`
|
||
MaxScore int `json:"maxScore"`
|
||
CreatedDate time.Time `json:"createdDate"`
|
||
RankedDate *time.Time `json:"rankedDate"`
|
||
QualifiedDate *time.Time `json:"qualifiedDate"`
|
||
LovedDate *time.Time `json:"lovedDate"`
|
||
Ranked bool `json:"ranked"`
|
||
Qualified bool `json:"qualified"`
|
||
Loved bool `json:"loved"`
|
||
MaxPP float64 `json:"maxPP"`
|
||
Stars float64 `json:"stars"`
|
||
Plays int `json:"plays"`
|
||
DailyPlays int `json:"dailyPlays"`
|
||
PositiveModifiers bool `json:"positiveModifiers"`
|
||
PlayerScore *string `json:"playerScore"`
|
||
CoverImage string `json:"coverImage"`
|
||
Difficulties interface{} `json:"difficulties"`
|
||
}
|
||
|
||
// CommandData 表示命令的数据
|
||
type CommandData struct {
|
||
Score Score `json:"score"`
|
||
Leaderboard Leaderboard `json:"leaderboard"`
|
||
}
|
||
|
||
// 表示记录的数据,本地储存
|
||
type RecordDataLite struct {
|
||
ID int `json:"id" db:"id"`
|
||
ScoreID int `json:"scoreId" db:"score_id"`
|
||
SsID string `json:"ssId" db:"ss_id"`
|
||
Name string `json:"name" db:"name"`
|
||
Country string `json:"country" db:"country"`
|
||
SongName string `json:"songName" db:"song_name"`
|
||
SongSubName string `json:"songSubName" db:"song_sub_name"`
|
||
SongAuthorName string `json:"songAuthorName" db:"song_author_name"`
|
||
SongHash string `json:"songHash" db:"song_hash"`
|
||
SongId string `json:"songId" db:"song_id"`
|
||
CoverImage string `json:"coverImage" db:"cover_image"`
|
||
DifficultyRaw string `json:"difficultyRaw" db:"difficulty_raw"`
|
||
Stars float64 `json:"stars" db:"stars"`
|
||
PP float64 `json:"pp" db:"pp"`
|
||
Weight float64 `json:"weight" db:"weight"`
|
||
Modifiers string `json:"modifiers" db:"modifiers"`
|
||
Multiplier float64 `json:"multiplier" db:"multiplier"`
|
||
Rank int `json:"rank" db:"rank"`
|
||
BadCuts int `json:"badCuts" db:"bad_cuts"`
|
||
MissedNotes int `json:"missedNotes" db:"missed_notes"`
|
||
MaxCombo int `json:"maxCombo" db:"max_combo"`
|
||
Score int `json:"score" db:"score"`
|
||
MaxScore int `json:"maxScore" db:"max_score"`
|
||
FullCombo bool `json:"fullCombo" db:"full_combo"`
|
||
DeviceHmd string `json:"deviceHmd" db:"device_hmd"`
|
||
DeviceControllerLeft string `json:"deviceControllerLeft" db:"device_controller_left"`
|
||
DeviceControllerRight string `json:"deviceControllerRight" db:"device_controller_right"`
|
||
GeneratedTime string `json:"generatedTime" db:"generated_time"`
|
||
}
|
||
|
||
func (r RecordDataLite) ToString() string {
|
||
formatedStrRanked := "%s,%s 使用 %s 在 %s(%s) 的 %s 难度(%.1f🌟)中打到了全球排名第%d,pp 为 %.2f,准度为 %s。"
|
||
formatedStrUnranked := "%s, %s 使用 %s 在 %s(%s) 的 %s 难度中打到了全球排名第%d,准度为 %s。"
|
||
formatedStrWithoutDevice := "%s, %s 在 %s(%s) 的 %s 难度(%.1f🌟)中打到了全球排名第%d,pp 为 %.2f,准度为 %s。"
|
||
formatedStrWithoutDeviceAndRank := "%s, %s 在 %s(%s) 的 %s 难度中打到了全球排名第%d,准度为 %s。"
|
||
hardStr := strings.Split(r.DifficultyRaw, "_")[1]
|
||
layout := "2006-01-02 15:04:05.999999999-07:00"
|
||
parsedTime, _ := time.Parse(layout, r.GeneratedTime)
|
||
duration := time.Since(parsedTime)
|
||
timeStr := timeConvert(duration)
|
||
if r.Stars == 0 && r.DeviceHmd != "" {
|
||
return fmt.Sprintf(formatedStrUnranked, timeStr, r.Name, r.DeviceHmd, r.SongName, r.SongId, hardStr, r.Rank, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100))
|
||
} else if r.Stars != 0 && r.DeviceHmd != "" {
|
||
return fmt.Sprintf(formatedStrRanked, timeStr, r.Name, r.DeviceHmd, r.SongName, r.SongId, hardStr, r.Stars, r.Rank, r.PP, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100))
|
||
} else if r.Stars != 0 && r.DeviceHmd == "" {
|
||
return fmt.Sprintf(formatedStrWithoutDevice, timeStr, r.Name, r.SongName, r.SongId, hardStr, r.Stars, r.Rank, r.PP, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100))
|
||
} else {
|
||
return fmt.Sprintf(formatedStrWithoutDeviceAndRank, timeStr, r.Name, r.SongName, r.SongId, hardStr, r.Rank, fmt.Sprintf("%.2f%%", float64(r.Score)/float64(r.MaxScore)*100))
|
||
}
|
||
}
|
||
|
||
func timeConvert(duration time.Duration) string {
|
||
var result string
|
||
if duration.Hours() >= 24 {
|
||
days := int(duration.Hours() / 24)
|
||
result = fmt.Sprintf("%d天前", days)
|
||
} else if duration.Hours() >= 1 {
|
||
hours := int(duration.Hours())
|
||
result = fmt.Sprintf("%d小时前", hours)
|
||
} else if duration.Minutes() >= 1 {
|
||
minutes := int(duration.Minutes())
|
||
result = fmt.Sprintf("%d分钟前", minutes)
|
||
} else {
|
||
result = "刚刚"
|
||
}
|
||
return result
|
||
}
|
||
|
||
//用户信息
|
||
|
||
// ScoreStats 存储分数统计信息
|
||
type ScoreStats struct {
|
||
TotalScore int `json:"totalScore" db:"total_score"`
|
||
TotalRankedScore int `json:"totalRankedScore" db:"total_ranked_score"`
|
||
AverageRankedAccuracy float64 `json:"averageRankedAccuracy" db:"average_ranked_accuracy"`
|
||
TotalPlayCount int `json:"totalPlayCount" db:"total_play_count"`
|
||
RankedPlayCount int `json:"rankedPlayCount" db:"ranked_play_count"`
|
||
ReplaysWatched int `json:"replaysWatched" db:"replays_watched"`
|
||
}
|
||
|
||
// PlayerData 存储玩家的完整信息
|
||
type PlayerData struct {
|
||
ID string `json:"id" db:"id"`
|
||
Name string `json:"name" db:"name"`
|
||
ProfilePicture string `json:"profilePicture" db:"profile_picture"`
|
||
Bio *string `json:"bio" db:"bio"`
|
||
Country string `json:"country" db:"country"`
|
||
PP float64 `json:"pp" db:"pp"`
|
||
Rank int `json:"rank" db:"rank"`
|
||
CountryRank int `json:"countryRank" db:"country_rank"`
|
||
Role *string `json:"role" db:"role"`
|
||
// Badges []string `json:"badges" db:"badges"`
|
||
Histories string `json:"histories" db:"histories"`
|
||
Permissions int `json:"permissions" db:"permissions"`
|
||
Banned bool `json:"banned" db:"banned"`
|
||
Inactive bool `json:"inactive" db:"inactive"`
|
||
ScoreStats ScoreStats `json:"scoreStats" db:"score_stats"`
|
||
FirstSeen time.Time `json:"firstSeen" db:"first_seen"`
|
||
}
|
||
|
||
type PlayerDataLite struct {
|
||
ID string `json:"id" db:"id"`
|
||
Name string `json:"name" db:"name"`
|
||
ProfilePicture string `json:"profilePicture" db:"profile_picture"`
|
||
Country string `json:"country" db:"country"`
|
||
Device string `json:"device" db:"device"`
|
||
PP float64 `json:"pp" db:"pp"`
|
||
Rank int `json:"rank" db:"rank"`
|
||
CountryRank int `json:"countryRank" db:"country_rank"`
|
||
TotalScore int `json:"totalScore" db:"total_score"`
|
||
TotalRankedScore int `json:"totalRankedScore" db:"total_ranked_score"`
|
||
AverageRankedAccuracy float64 `json:"averageRankedAccuracy" db:"average_ranked_accuracy"`
|
||
TotalPlayCount int `json:"totalPlayCount" db:"total_play_count"`
|
||
RankedPlayCount int `json:"rankedPlayCount" db:"ranked_play_count"`
|
||
ReplaysWatched int `json:"replaysWatched" db:"replays_watched"`
|
||
GeneratedTime string `json:"generatedTime" db:"generated_time"`
|
||
}
|
||
|
||
func (p PlayerDataLite) IsDiffFrom(p2 PlayerDataLite) bool {
|
||
return p.TotalScore != p2.TotalScore ||
|
||
p.TotalRankedScore != p2.TotalRankedScore ||
|
||
p.AverageRankedAccuracy != p2.AverageRankedAccuracy ||
|
||
p.TotalPlayCount != p2.TotalPlayCount ||
|
||
p.RankedPlayCount != p2.RankedPlayCount ||
|
||
p.ReplaysWatched != p2.ReplaysWatched
|
||
}
|
||
|
||
func (p PlayerData) ToString() string {
|
||
filePath, err := util.DownloadFile(p.ProfilePicture, "/tmp/qqbot", false)
|
||
if err != nil {
|
||
log.Default().Printf("下载头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
defer os.Remove(filePath)
|
||
outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20)
|
||
if err != nil {
|
||
log.Default().Printf("缩放头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
picMsg := message.ImageMessage{
|
||
Type: message.TypeImage,
|
||
Data: message.ImageMessageData{
|
||
File: outFile,
|
||
},
|
||
}
|
||
formatedStr := "玩家 %s\n" +
|
||
picMsg.ToCQString() +
|
||
"区域 %s\n" +
|
||
"PP %.1f\n" +
|
||
"全球排名 %d\n" +
|
||
"区域排名 %d\n" +
|
||
"Ranked谱面均准 %.2f%%\n" +
|
||
"总游玩记数 %d\n" +
|
||
"Ranked谱面游玩记数 %d\n" +
|
||
"回放被观看次数 %d"
|
||
return fmt.Sprintf(formatedStr,
|
||
p.Name,
|
||
p.Country,
|
||
p.PP,
|
||
p.Rank,
|
||
p.CountryRank,
|
||
p.ScoreStats.AverageRankedAccuracy,
|
||
p.ScoreStats.TotalPlayCount,
|
||
p.ScoreStats.RankedPlayCount,
|
||
p.ScoreStats.ReplaysWatched)
|
||
}
|
||
|
||
func (p PlayerData) ToDataLite() PlayerDataLite {
|
||
return PlayerDataLite{
|
||
ID: p.ID,
|
||
Name: p.Name,
|
||
ProfilePicture: p.ProfilePicture,
|
||
Country: p.Country,
|
||
PP: p.PP,
|
||
Rank: p.Rank,
|
||
CountryRank: p.CountryRank,
|
||
TotalScore: p.ScoreStats.TotalScore,
|
||
TotalRankedScore: p.ScoreStats.TotalRankedScore,
|
||
AverageRankedAccuracy: p.ScoreStats.AverageRankedAccuracy,
|
||
TotalPlayCount: p.ScoreStats.TotalPlayCount,
|
||
RankedPlayCount: p.ScoreStats.RankedPlayCount,
|
||
ReplaysWatched: p.ScoreStats.ReplaysWatched,
|
||
GeneratedTime: p.FirstSeen.Format(time.RFC3339),
|
||
}
|
||
}
|
||
|
||
func (p PlayerData) LastDiffToString(lastDayQueryData PlayerDataLite) string {
|
||
filePath, err := util.DownloadFile(p.ProfilePicture, "/tmp/qqbot", false)
|
||
if err != nil {
|
||
log.Default().Printf("下载头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
defer os.Remove(filePath)
|
||
outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20)
|
||
if err != nil {
|
||
log.Default().Printf("缩放头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
picMsg := message.ImageMessage{
|
||
Type: message.TypeImage,
|
||
Data: message.ImageMessageData{
|
||
File: outFile,
|
||
},
|
||
}
|
||
formatedStr := "玩家 %s\n" +
|
||
picMsg.ToCQString() +
|
||
"区域 %s\n" +
|
||
"PP %.1f(%+.1f)\n" +
|
||
"全球排名 %d(%+d)\n" +
|
||
"区域排名 %d(%+d)\n" +
|
||
"Ranked谱面均准 %.2f%%(%+.2f%%)\n" +
|
||
"总游玩记数 %d(%+d)\n" +
|
||
"Ranked谱面游玩记数 %d(%+d)\n" +
|
||
"回放被观看次数 %d"
|
||
return fmt.Sprintf(formatedStr,
|
||
p.Name,
|
||
p.Country,
|
||
p.PP, p.PP-lastDayQueryData.PP,
|
||
p.Rank, lastDayQueryData.Rank-p.Rank,
|
||
p.CountryRank, lastDayQueryData.CountryRank-p.CountryRank,
|
||
p.ScoreStats.AverageRankedAccuracy, p.ScoreStats.AverageRankedAccuracy-lastDayQueryData.AverageRankedAccuracy,
|
||
p.ScoreStats.TotalPlayCount, p.ScoreStats.TotalPlayCount-lastDayQueryData.TotalPlayCount,
|
||
p.ScoreStats.RankedPlayCount, p.ScoreStats.RankedPlayCount-lastDayQueryData.RankedPlayCount,
|
||
p.ScoreStats.ReplaysWatched)
|
||
}
|
||
|
||
func (p PlayerDataLite) LastDiffToImage(lastQueryData PlayerDataLite) string {
|
||
filePath, err := util.DownloadFile(p.ProfilePicture, "/tmp/qqbot", false)
|
||
if err != nil {
|
||
log.Default().Printf("下载头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
defer os.Remove(filePath)
|
||
|
||
baseboard := sprite.NewNamedSpriteBoard()
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("玩家 %s\n", p.Name))
|
||
sb.WriteString(fmt.Sprintf("区域 %s\n", p.Country))
|
||
{
|
||
// PP值
|
||
ppDiff := p.PP - lastQueryData.PP
|
||
if ppDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("PP %.1f\n", p.PP))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("PP %.1f(%+.1f)\n", p.PP, ppDiff))
|
||
} // 全球排名
|
||
rankDiff := lastQueryData.Rank - p.Rank
|
||
if rankDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("全球排名 %d\n", p.Rank))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("全球排名 %d(%+d)\n", p.Rank, rankDiff))
|
||
}
|
||
|
||
// 区域排名
|
||
countryRankDiff := lastQueryData.CountryRank - p.CountryRank
|
||
if countryRankDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("区域排名 %d\n", p.CountryRank))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("区域排名 %d(%+d)\n", p.CountryRank, countryRankDiff))
|
||
}
|
||
|
||
// Ranked谱面均准
|
||
accDiff := (p.AverageRankedAccuracy - lastQueryData.AverageRankedAccuracy) * 100
|
||
if accDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%\n", p.AverageRankedAccuracy))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%(%+.2f%%)\n", p.AverageRankedAccuracy, accDiff))
|
||
}
|
||
// 总游玩记数
|
||
totalPlayDiff := p.TotalPlayCount - lastQueryData.TotalPlayCount
|
||
if totalPlayDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("总游玩记数 %d\n", p.TotalPlayCount))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("总游玩记数 %d(%+d)\n", p.TotalPlayCount, totalPlayDiff))
|
||
}
|
||
|
||
// Ranked谱面游玩记数
|
||
rankedPlayDiff := p.RankedPlayCount - lastQueryData.RankedPlayCount
|
||
if rankedPlayDiff == 0 {
|
||
sb.WriteString(fmt.Sprintf("Ranked谱面游玩记数 %d\n", p.RankedPlayCount))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("Ranked谱面游玩记数 %d(%+d)\n", p.RankedPlayCount, rankedPlayDiff))
|
||
}
|
||
// 回放被观看次数
|
||
sb.WriteString(fmt.Sprintf("回放被观看次数 %d", p.ReplaysWatched))
|
||
}
|
||
text := sb.String()
|
||
textImg, err := text2img.RenderTextToTrimmedImage(nil, text, 24, color.Black, 0, 0)
|
||
if err != nil {
|
||
log.Default().Printf("渲染文字失败,err:%v", err)
|
||
}
|
||
|
||
avatar, delay, err := util.ResizeImageByMaxHeight2Image(filePath, uint(textImg.Bounds().Dy()))
|
||
if err != nil {
|
||
log.Default().Printf("缩放头像失败,url:%s,err:%v", p.ProfilePicture, err)
|
||
}
|
||
avatarSpirit := sprite.Sprite{
|
||
Name: "avatar",
|
||
Images: avatar,
|
||
Delay: delay,
|
||
Index: 1,
|
||
}
|
||
if avatar == nil {
|
||
avatarSpirit.Images = []image.Image{image.NewRGBA(image.Rect(0, 0, 0, 0))}
|
||
}
|
||
baseboard.AddSprite(&avatarSpirit)
|
||
|
||
textSpirit := sprite.Sprite{
|
||
Name: "text",
|
||
Images: []image.Image{textImg},
|
||
Index: 2,
|
||
Position: image.Point{X: avatarSpirit.Position.X + avatarSpirit.Images[0].Bounds().Dx() + 3, Y: 0},
|
||
}
|
||
baseboard.AddSprite(&textSpirit)
|
||
|
||
minX, minY, maxX, maxY := baseboard.GetRenderBounds()
|
||
totalWidth := maxX - minX
|
||
totalHeight := maxY - minY
|
||
|
||
background := image.NewRGBA(image.Rect(0, 0, int(totalWidth+10), int(totalHeight+10)))
|
||
draw.Draw(background, background.Bounds(), image.White, image.Point{}, draw.Src)
|
||
backgroundSpirit := sprite.Sprite{
|
||
Name: "background",
|
||
Images: []image.Image{background},
|
||
Index: 0,
|
||
Position: image.Point{X: minX - 5, Y: minY - 5},
|
||
}
|
||
baseboard.AddSprite(&backgroundSpirit)
|
||
// if len(avatarSpirit.Images) > 1 {
|
||
// if err := baseboard.SaveToGif(util.GenTempFilePath("css.gif")); err != nil {
|
||
// log.Default().Printf("保存图片失败,err:%v", err)
|
||
// }
|
||
// return util.GenTempFilePath("css.gif")
|
||
// }
|
||
if err := baseboard.SaveToApng(util.GenTempFilePath("css.png")); err != nil {
|
||
log.Default().Printf("保存图片失败,err:%v", err)
|
||
}
|
||
return util.GenTempFilePath("css.png")
|
||
}
|