package beatleader 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" ) type ScoreData struct { ContextExtensions []ContextExtension `json:"contextExtensions"` MyScore *string `json:"myScore"` ValidContexts int `json:"validContexts"` Leaderboard Leaderboard `json:"leaderboard"` AccLeft float64 `json:"accLeft"` AccRight float64 `json:"accRight"` ID int `json:"id"` BaseScore int `json:"baseScore"` ModifiedScore int `json:"modifiedScore"` Accuracy float64 `json:"accuracy"` PlayerID string `json:"playerId"` Pp float64 `json:"pp"` BonusPp float64 `json:"bonusPp"` PassPp float64 `json:"passPP"` AccPp float64 `json:"accPP"` TechPp float64 `json:"techPP"` Rank int `json:"rank"` Country string `json:"country"` FcAccuracy float64 `json:"fcAccuracy"` FcPp float64 `json:"fcPp"` Weight float64 `json:"weight"` Replay string `json:"replay"` Modifiers string `json:"modifiers"` BadCuts int `json:"badCuts"` MissedNotes int `json:"missedNotes"` BombCuts int `json:"bombCuts"` WallsHit int `json:"wallsHit"` Pauses int `json:"pauses"` FullCombo bool `json:"fullCombo"` Platform string `json:"platform"` MaxCombo int `json:"maxCombo"` MaxStreak *int `json:"maxStreak"` HMD int `json:"hmd"` Controller int `json:"controller"` LeaderboardID string `json:"leaderboardId"` Timeset string `json:"timeset"` Timepost int64 `json:"timepost"` ReplaysWatched int `json:"replaysWatched"` PlayCount int `json:"playCount"` LastTryTime int64 `json:"lastTryTime"` Priority int `json:"priority"` Player Player `json:"player"` ScoreImprovement ScoreImprovement `json:"scoreImprovement"` // RankVoting *string `json:"rankVoting"` // Metadata *string `json:"metadata"` Offsets Offsets `json:"offsets"` } type ContextExtension struct { ID int `json:"id"` PlayerID string `json:"playerId"` Weight float64 `json:"weight"` Rank int `json:"rank"` BaseScore int `json:"baseScore"` ModifiedScore int `json:"modifiedScore"` Accuracy float64 `json:"accuracy"` Pp float64 `json:"pp"` PassPp float64 `json:"passPP"` AccPp float64 `json:"accPP"` TechPp float64 `json:"techPP"` BonusPp float64 `json:"bonusPp"` Modifiers string `json:"modifiers"` Context int `json:"context"` ScoreImprovement ScoreImprovement `json:"scoreImprovement"` } type ScoreImprovement struct { ID int `json:"id"` Timeset string `json:"timeset"` Score int `json:"score"` Accuracy float64 `json:"accuracy"` Pp float64 `json:"pp"` BonusPp float64 `json:"bonusPp"` Rank int `json:"rank"` AccRight float64 `json:"accRight"` AccLeft float64 `json:"accLeft"` AverageRankedAccuracy float64 `json:"averageRankedAccuracy"` TotalPp float64 `json:"totalPp"` TotalRank int `json:"totalRank"` BadCuts int `json:"badCuts"` MissedNotes int `json:"missedNotes"` BombCuts int `json:"bombCuts"` WallsHit int `json:"wallsHit"` Pauses int `json:"pauses"` } type Leaderboard struct { ID string `json:"id"` Song Song `json:"song"` Difficulty Difficulty `json:"difficulty"` } type Song struct { ID string `json:"id"` Hash string `json:"hash"` Name string `json:"name"` SubName string `json:"subName"` Author string `json:"author"` Mapper string `json:"mapper"` MapperID int `json:"mapperId"` CollaboratorIds *string `json:"collaboratorIds"` CoverImage string `json:"coverImage"` Bpm float64 `json:"bpm"` Duration float64 `json:"duration"` FullCoverImage *string `json:"fullCoverImage"` } type Difficulty struct { ID int `json:"id"` Value int `json:"value"` Mode int `json:"mode"` DifficultyName string `json:"difficultyName"` ModeName string `json:"modeName"` Status int `json:"status"` ModifierValues ModifierValues `json:"modifierValues"` // ModifiersRating *string `json:"modifiersRating"` NominatedTime int64 `json:"nominatedTime"` QualifiedTime int64 `json:"qualifiedTime"` RankedTime int64 `json:"rankedTime"` SpeedTags int `json:"speedTags"` StyleTags int `json:"styleTags"` FeatureTags int `json:"featureTags"` Stars *float64 `json:"stars"` PredictedAcc float64 `json:"predictedAcc"` } type ModifierValues struct { ModifierID int `json:"modifierId"` Da float64 `json:"da"` Fs float64 `json:"fs"` Sf float64 `json:"sf"` // 其他修改器字段... } type Player struct { ID string `json:"id"` Name string `json:"name"` Platform string `json:"platform"` Avatar string `json:"avatar"` Country string `json:"country"` Alias *string `json:"alias"` Bot bool `json:"bot"` Pp float64 `json:"pp"` Rank int `json:"rank"` CountryRank int `json:"countryRank"` Role string `json:"role"` // Socials *string `json:"socials"` ContextExtensions *string `json:"contextExtensions"` } type Offsets struct { ID int `json:"id"` Frames int `json:"frames"` Notes int `json:"notes"` Walls int `json:"walls"` Heights int `json:"heights"` Pauses int `json:"pauses"` } // 表示记录的数据,本地储存 type RecordDataLite struct { ID int `json:"id" db:"id"` ScoreID int `json:"scoreId" db:"score_id"` BlID string `json:"blId" db:"bl_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 := r.DifficultyRaw 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 PlayerData struct { MapperID int `json:"mapperId"` Banned bool `json:"banned"` Inactive bool `json:"inactive"` // BanDescription *string `json:"banDescription"` ExternalProfileURL string `json:"externalProfileUrl"` RichBioTimeset int64 `json:"richBioTimeset"` SpeedrunStart int64 `json:"speedrunStart"` LinkedIDs LinkedIDs `json:"linkedIds"` // History *string `json:"history"` // Badges []string `json:"badges"` // PinnedScores *string `json:"pinnedScores"` // Changes []Change `json:"changes"` AccPp float64 `json:"accPp"` PassPp float64 `json:"passPp"` TechPp float64 `json:"techPp"` ScoreStats ScoreStats `json:"scoreStats"` LastWeekPp float64 `json:"lastWeekPp"` LastWeekRank int `json:"lastWeekRank"` LastWeekCountryRank int `json:"lastWeekCountryRank"` ExtensionID int `json:"extensionId"` ID string `json:"id"` Name string `json:"name"` Platform string `json:"platform"` Avatar string `json:"avatar"` Country string `json:"country"` // Alias *string `json:"alias"` Bot bool `json:"bot"` PP float64 `json:"pp"` Rank int `json:"rank"` CountryRank int `json:"countryRank"` Role string `json:"role"` // Socials []string `json:"socials"` // ContextExtensions *string `json:"contextExtensions"` // PatreonFeatures *string `json:"patreonFeatures"` // ProfileSettings ProfileSettings `json:"profileSettings"` // ClanOrder string `json:"clanOrder"` // Clans []string `json:"clans"` } func (p PlayerData) ToDataLite() PlayerDataLite { dataLite := PlayerDataLite{ ID: p.ID, Name: p.Name, Country: p.Country, Avatar: p.Avatar, PP: p.AccPp + p.PassPp + p.TechPp, 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.WatchedReplays, GeneratedTime: time.Now().Format("2006-01-02 15:04:05.999999999-07:00"), } return dataLite } type LinkedIDs struct { QuestID int `json:"questId"` SteamID string `json:"steamId"` OculusPCID string `json:"oculusPCId"` } type Change struct { ID int64 `json:"id"` Timestamp int64 `json:"timestamp"` PlayerID string `json:"playerId"` OldName string `json:"oldName"` NewName string `json:"newName"` OldCountry string `json:"oldCountry"` NewCountry string `json:"newCountry"` Changer *string `json:"changer"` } type ScoreStats struct { ID int64 `json:"id"` TotalScore int64 `json:"totalScore"` TotalUnrankedScore int64 `json:"totalUnrankedScore"` TotalRankedScore int64 `json:"totalRankedScore"` LastScoreTime int64 `json:"lastScoreTime"` LastUnrankedScoreTime int64 `json:"lastUnrankedScoreTime"` LastRankedScoreTime int64 `json:"lastRankedScoreTime"` AverageRankedAccuracy float64 `json:"averageRankedAccuracy"` AverageWeightedRankedAccuracy float64 `json:"averageWeightedRankedAccuracy"` AverageUnrankedAccuracy float64 `json:"averageUnrankedAccuracy"` AverageAccuracy float64 `json:"averageAccuracy"` MedianRankedAccuracy float64 `json:"medianRankedAccuracy"` MedianAccuracy float64 `json:"medianAccuracy"` TopRankedAccuracy float64 `json:"topRankedAccuracy"` TopUnrankedAccuracy float64 `json:"topUnrankedAccuracy"` TopAccuracy float64 `json:"topAccuracy"` TopPp float64 `json:"topPp"` TopBonusPP float64 `json:"topBonusPP"` TopPassPP float64 `json:"topPassPP"` TopAccPP float64 `json:"topAccPP"` TopTechPP float64 `json:"topTechPP"` PeakRank int `json:"peakRank"` RankedMaxStreak int `json:"rankedMaxStreak"` UnrankedMaxStreak int `json:"unrankedMaxStreak"` MaxStreak int `json:"maxStreak"` AverageLeftTiming float64 `json:"averageLeftTiming"` AverageRightTiming float64 `json:"averageRightTiming"` RankedPlayCount int `json:"rankedPlayCount"` UnrankedPlayCount int `json:"unrankedPlayCount"` TotalPlayCount int `json:"totalPlayCount"` RankedImprovementsCount int `json:"rankedImprovementsCount"` UnrankedImprovementsCount int `json:"unrankedImprovementsCount"` TotalImprovementsCount int `json:"totalImprovementsCount"` RankedTop1Count int `json:"rankedTop1Count"` UnrankedTop1Count int `json:"unrankedTop1Count"` Top1Count int `json:"top1Count"` RankedTop1Score int64 `json:"rankedTop1Score"` UnrankedTop1Score int64 `json:"unrankedTop1Score"` Top1Score int64 `json:"top1Score"` AverageRankedRank float64 `json:"averageRankedRank"` AverageWeightedRankedRank float64 `json:"averageWeightedRankedRank"` AverageUnrankedRank float64 `json:"averageUnrankedRank"` AverageRank float64 `json:"averageRank"` SspPlays int `json:"sspPlays"` SsPlays int `json:"ssPlays"` SpPlays int `json:"spPlays"` SPlays int `json:"sPlays"` APlays int `json:"aPlays"` TopPlatform string `json:"topPlatform"` TopHMD int `json:"topHMD"` AllHMDs string `json:"allHMDs"` TopPercentile float64 `json:"topPercentile"` CountryTopPercentile float64 `json:"countryTopPercentile"` DailyImprovements int `json:"dailyImprovements"` AuthorizedReplayWatched int `json:"authorizedReplayWatched"` AnonimusReplayWatched int `json:"anonimusReplayWatched"` WatchedReplays int `json:"watchedReplays"` } type ProfileSettings struct { ID int64 `json:"id"` Bio *string `json:"bio"` Message *string `json:"message"` EffectName string `json:"effectName"` ProfileAppearance string `json:"profileAppearance"` Hue int `json:"hue"` Saturation float64 `json:"saturation"` LeftSaberColor *string `json:"leftSaberColor"` RightSaberColor *string `json:"rightSaberColor"` ProfileCover *string `json:"profileCover"` StarredFriends string `json:"starredFriends"` HorizontalRichBio bool `json:"horizontalRichBio"` RankedMapperSort *string `json:"rankedMapperSort"` ShowBots bool `json:"showBots"` ShowAllRatings bool `json:"showAllRatings"` ShowStatsPublic bool `json:"showStatsPublic"` ShowStatsPublicPinned bool `json:"showStatsPublicPinned"` } type PlayerDataLite struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Country string `json:"country" db:"country"` Avatar string `json:"avatar" db:"avatar"` 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 int64 `json:"totalScore" db:"total_score"` TotalRankedScore int64 `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 PlayerDataLite) LastDiffToString(lastQueryData PlayerDataLite) string { filePath, err := util.DownloadFile(p.Avatar, "/tmp/qqbot", false) if err != nil { log.Default().Printf("下载头像失败,url:%s,err:%v", p.Avatar, err) } defer os.Remove(filePath) outFile, err := util.ResizeImageByMaxHeight2File(filePath, 20) if err != nil { log.Default().Printf("缩放头像失败,url:%s,err:%v", p.Avatar, err) } picMsg := message.ImageMessage{ Type: message.TypeImage, Data: message.ImageMessageData{ File: outFile, }, } var sb strings.Builder sb.WriteString(fmt.Sprintf("玩家 %s\n", p.Name)) sb.WriteString(picMsg.ToCQString()) 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*100)) } else { sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%(%+.2f%%)\n", p.AverageRankedAccuracy*100, 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)) return sb.String() } func (p PlayerDataLite) LastDiffToImage(lastQueryData PlayerDataLite) string { filePath, err := util.DownloadFile(p.Avatar, "/tmp/qqbot", false) if err != nil { log.Default().Printf("下载头像失败,url:%s,err:%v", p.Avatar, 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*100)) } else { sb.WriteString(fmt.Sprintf("Ranked谱面均准 %.2f%%(%+.2f%%)\n", p.AverageRankedAccuracy*100, 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.Avatar, 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("cbl.gif")); err != nil { log.Default().Printf("保存图片失败,err:%v", err) } return util.GenTempFilePath("cbl.gif") } if err := baseboard.SaveToPng(util.GenTempFilePath("cbl.png")); err != nil { log.Default().Printf("保存图片失败,err:%v", err) } return util.GenTempFilePath("cbl.png") } func GetControllerStr(controller int) string { switch controller { case 1: return "Oculus Touch 手柄" case 16: return "Oculus Touch 2 手柄" case 79: return "Meta Quest 3 手柄" case 256: return "Quest 2 手柄" case 2: return "VIVE wands" case 4: return "VIVE 2 wands" case 128: return "VIVE cosmos controllers" case 8: return "WMR 手柄" case 33: return "Pico 手柄" case 34: return "Pico 手柄" case 35: return "VIVE Pro dudads" case 37: return "Miramar 手柄" case 44: return "disco 手柄" case 61: return "Quest Pro 手柄" case 62: return "VIVE tracker" case 63: return "VIVE tracker 2" case 64: return "Knuckles" case 65: return "nolo手柄" case 66: return "Pico phoenix" case 67: return "双手🙌" case 68: return "VIVE tracker 3" case 75: return "游戏手柄🎮" case 76: return "Joy-Con" case 77: return "Steam Deck" case 78: return "Etee" } return "神秘手柄" } func GetHMDStr(hmd int) string { switch hmd { case 256: return "Quest 2" case 512: return "Quest 3" case 64: return "Valve Index" case 513: return "Quest 3S" case 1: return "Rift CV1" case 2: return "Vive" case 60: return "Pico 4" case 61: return "Quest Pro" case 70: return "PS VR2" case 8: return "WMR" case 16: return "Rift S" case 65: return "Controllable" case 32: return "Quest" case 4: return "Vive Pro" case 35: return "Vive Pro 2" case 128: return "Vive Cosmos" case 36: return "Vive Elite" case 47: return "Vive Focus" case 38: return "Pimax 8K" case 39: return "Pimax 5K" case 40: return "Pimax Artisan" case 33: return "Pico Neo 3" case 34: return "Pico Neo 2" case 41: return "HP Reverb" case 42: return "Samsung WMR" case 43: return "Qiyu Dream" case 45: return "Lenovo Explorer" case 46: return "Acer WMR" case 66: return "Bigscreen Beyond" case 67: return "NOLO Sonic" case 68: return "Hypereal" case 48: return "Arpara" case 49: return "Dell Visor" case 71: return "MeganeX VG1" case 55: return "Huawei VR" case 56: return "Asus WMR" case 51: return "Vive DVT" case 52: return "glasses20" case 53: return "Varjo" case 69: return "Varjo Aero" case 54: return "Vaporeon" case 57: return "Cloud XR" case 58: return "VRidge" case 50: return "e3" case 59: return "Medion Eraser" case 37: return "Miramar" case 0: return "Unknown headset" case 44: return "Disco" default: return "神秘头显" } }