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, onlyFirstFrame bool) 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 && !onlyFirstFrame { 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") }