440 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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🌟)中打到了全球排名第%dpp 为 %.2f,准度为 %s。"
formatedStrUnranked := "%s, %s 使用 %s 在 %s(%s) 的 %s 难度中打到了全球排名第%d准度为 %s。"
formatedStrWithoutDevice := "%s, %s 在 %s(%s) 的 %s 难度(%.1f🌟)中打到了全球排名第%dpp 为 %.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")
}