595 lines
11 KiB
Go
595 lines
11 KiB
Go
package fortnite
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/ectrc/snow/aid"
|
|
"github.com/ectrc/snow/storage"
|
|
)
|
|
|
|
type FortniteAPI struct {
|
|
URL string
|
|
C *http.Client
|
|
}
|
|
|
|
type FAPI_Response struct {
|
|
Status int `json:"status"`
|
|
Data []FAPI_Cosmetic `json:"data"`
|
|
}
|
|
|
|
type FAPI_Error struct {
|
|
Status int `json:"status"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
type FAPI_Cosmetic_Variant struct {
|
|
Channel string `json:"channel"`
|
|
Type string `json:"type"`
|
|
Options []FAPI_Cosmetic_VariantChannel `json:"options"`
|
|
}
|
|
|
|
type FAPI_Cosmetic_VariantChannel struct {
|
|
Tag string `json:"tag"`
|
|
Name string `json:"name"`
|
|
Image string `json:"image"`
|
|
}
|
|
|
|
type FAPI_Cosmetic struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Type struct {
|
|
Value string `json:"value"`
|
|
DisplayValue string `json:"displayValue"`
|
|
BackendValue string `json:"backendValue"`
|
|
} `json:"type"`
|
|
Rarity struct {
|
|
Value string `json:"value"`
|
|
DisplayValue string `json:"displayValue"`
|
|
BackendValue string `json:"backendValue"`
|
|
} `json:"rarity"`
|
|
Series struct {
|
|
Value string `json:"value"`
|
|
Image string `json:"image"`
|
|
BackendValue string `json:"backendValue"`
|
|
} `json:"series"`
|
|
Set struct {
|
|
Value string `json:"value"`
|
|
Text string `json:"text"`
|
|
BackendValue string `json:"backendValue"`
|
|
} `json:"set"`
|
|
Introduction struct {
|
|
Chapter string `json:"chapter"`
|
|
Season string `json:"season"`
|
|
Text string `json:"text"`
|
|
BackendValue int `json:"backendValue"`
|
|
} `json:"introduction"`
|
|
Images struct {
|
|
Icon string `json:"icon"`
|
|
Featured string `json:"featured"`
|
|
SmallIcon string `json:"smallIcon"`
|
|
Other map[string]string `json:"other"`
|
|
} `json:"images"`
|
|
Variants []FAPI_Cosmetic_Variant `json:"variants"`
|
|
GameplayTags []string `json:"gameplayTags"`
|
|
SearchTags []string `json:"searchTags"`
|
|
MetaTags []string `json:"metaTags"`
|
|
ShowcaseVideo string `json:"showcaseVideo"`
|
|
DynamicPakID string `json:"dynamicPakId"`
|
|
DisplayAssetPath string `json:"displayAssetPath"`
|
|
DisplayAssetPath2 string
|
|
ItemPreviewHeroPath string `json:"itemPreviewHeroPath"`
|
|
Backpack string `json:"backpack"`
|
|
Path string `json:"path"`
|
|
Added string `json:"added"`
|
|
ShopHistory []string `json:"shopHistory"`
|
|
BattlePass bool `json:"battlePass"`
|
|
}
|
|
|
|
type SnowVariantGrant struct {
|
|
Channel string `json:"channel"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type SnowVariant struct {
|
|
Grants []SnowVariantGrant `json:"grants"`
|
|
Name string `json:"name"`
|
|
Gift bool `json:"gift"`
|
|
Equip bool `json:"equip"`
|
|
Unseen bool `json:"unseen"`
|
|
}
|
|
|
|
type Set struct {
|
|
Items map[string]FAPI_Cosmetic `json:"items"`
|
|
Name string `json:"name"`
|
|
BackendName string `json:"backendName"`
|
|
}
|
|
|
|
type CosmeticData struct {
|
|
Items map[string]FAPI_Cosmetic `json:"items"`
|
|
Sets map[string]Set `json:"sets"`
|
|
VariantTokens map[string]SnowVariant `json:"variantTokens"`
|
|
}
|
|
|
|
func (c *CosmeticData) GetRandomItem() FAPI_Cosmetic {
|
|
randomInt := rand.Intn(len(c.Items))
|
|
|
|
i := 0
|
|
for _, item := range c.Items {
|
|
if strings.Contains(item.Description, "TBD") {
|
|
continue
|
|
}
|
|
|
|
if i == randomInt {
|
|
return item
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
return c.GetRandomItem()
|
|
}
|
|
|
|
func (c *CosmeticData) GetRandomItemByType(itemType string) FAPI_Cosmetic {
|
|
randomInt := rand.Intn(len(c.Items))
|
|
|
|
i := 0
|
|
for _, item := range c.Items {
|
|
if item.Type.BackendValue != itemType {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(item.Description, "TBD") {
|
|
continue
|
|
}
|
|
|
|
if i == randomInt {
|
|
return item
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
return c.GetRandomItemByType(itemType)
|
|
}
|
|
|
|
func (c *CosmeticData) GetRandomItemByNotType(itemType string) FAPI_Cosmetic {
|
|
randomInt := rand.Intn(len(c.Items))
|
|
|
|
i := 0
|
|
for _, item := range c.Items {
|
|
if item.Type.BackendValue == itemType {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(item.Description, "TBD") {
|
|
continue
|
|
}
|
|
|
|
if i == randomInt {
|
|
return item
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
return c.GetRandomItemByNotType(itemType)
|
|
}
|
|
|
|
func (c *CosmeticData) GetRandomSet() Set {
|
|
randomInt := rand.Intn(len(c.Sets))
|
|
|
|
i := 0
|
|
for _, set := range c.Sets {
|
|
if i == randomInt {
|
|
return set
|
|
}
|
|
|
|
i++
|
|
}
|
|
|
|
return c.GetRandomSet()
|
|
}
|
|
|
|
var EXTRA_NUMERIC_STYLES = []string{"Soccer", "Football", "ScaryBall"}
|
|
|
|
func (c *CosmeticData) AddItem(item FAPI_Cosmetic) {
|
|
if slices.Contains(EXTRA_NUMERIC_STYLES, item.Set.BackendValue) {
|
|
item = c.AddNumericVariantChannelToItem(item)
|
|
}
|
|
|
|
c.Items[item.ID] = item
|
|
|
|
if item.Set.BackendValue != "" {
|
|
if _, ok := Cosmetics.Sets[item.Set.BackendValue]; !ok {
|
|
Cosmetics.Sets[item.Set.BackendValue] = Set{
|
|
Items: make(map[string]FAPI_Cosmetic),
|
|
Name: item.Set.Value,
|
|
BackendName: item.Set.BackendValue,
|
|
}
|
|
}
|
|
|
|
Cosmetics.Sets[item.Set.BackendValue].Items[item.ID] = item
|
|
}
|
|
}
|
|
|
|
func (c *CosmeticData) AddNumericVariantChannelToItem(item FAPI_Cosmetic) FAPI_Cosmetic {
|
|
owned := []FAPI_Cosmetic_VariantChannel{}
|
|
for i := 0; i < 100; i++ {
|
|
owned = append(owned, FAPI_Cosmetic_VariantChannel{
|
|
Tag: fmt.Sprint(i),
|
|
})
|
|
}
|
|
|
|
item.Variants = append(item.Variants, FAPI_Cosmetic_Variant{
|
|
Channel: "Numeric",
|
|
Type: "int",
|
|
Options: owned,
|
|
})
|
|
|
|
return item
|
|
}
|
|
|
|
var (
|
|
StaticAPI = NewFortniteAPI()
|
|
Cosmetics = CosmeticData{
|
|
Items: make(map[string]FAPI_Cosmetic),
|
|
Sets: make(map[string]Set),
|
|
VariantTokens: make(map[string]SnowVariant),
|
|
}
|
|
)
|
|
|
|
func NewFortniteAPI() *FortniteAPI {
|
|
return &FortniteAPI{
|
|
URL: "https://fortnite-api.com",
|
|
C: &http.Client{},
|
|
}
|
|
}
|
|
|
|
func (f *FortniteAPI) Get(path string) (*FAPI_Response, error) {
|
|
req, err := http.NewRequest("GET", f.URL + path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := f.C.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data FAPI_Response
|
|
err = json.Unmarshal(bodyBytes, &data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &data, nil
|
|
}
|
|
|
|
func (f *FortniteAPI) GetAllCosmetics() ([]FAPI_Cosmetic, error) {
|
|
resp, err := f.Get("/v2/cosmetics/br")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.Data, nil
|
|
}
|
|
|
|
func (f *FortniteAPI) GetPlaylistImage(playlist string) (any, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func PreloadCosmetics(max int) error {
|
|
aid.Print("(external) assets from", StaticAPI.URL)
|
|
|
|
list, err := StaticAPI.GetAllCosmetics()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, item := range list {
|
|
if item.Introduction.BackendValue == 0 || item.Introduction.BackendValue > max {
|
|
continue
|
|
}
|
|
|
|
if len(item.ShopHistory) == 0 && item.Type.Value == "outfit" {
|
|
item.BattlePass = true
|
|
}
|
|
|
|
Cosmetics.AddItem(item)
|
|
}
|
|
|
|
for id, item := range Cosmetics.Items {
|
|
if item.Type.Value != "backpack" {
|
|
continue
|
|
}
|
|
|
|
if item.ItemPreviewHeroPath == "" {
|
|
continue
|
|
}
|
|
|
|
previewHeroPath := strings.Split(item.ItemPreviewHeroPath, "/")
|
|
characterId := previewHeroPath[len(previewHeroPath)-1]
|
|
|
|
character, ok := Cosmetics.Items[characterId]
|
|
if !ok {
|
|
continue
|
|
}
|
|
character.Backpack = id
|
|
Cosmetics.AddItem(character)
|
|
}
|
|
|
|
assets := storage.HttpAsset("QKnwROGzQjYm1W9xu9uL3VrbSA0tnVj6NJJtEChUdAb3DF8uN.json")
|
|
if assets == nil {
|
|
panic("Failed to load assets")
|
|
}
|
|
|
|
var assetData []string
|
|
err = json.Unmarshal(*assets, &assetData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
withDisplayAssets := 0
|
|
|
|
for _, asset := range assetData {
|
|
asset := strings.ReplaceAll(asset, "DAv2_", "")
|
|
parts := strings.Split(asset, "_")
|
|
|
|
if strings.Contains(asset, "Bundle") {
|
|
withDisplayAssets++
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case parts[0] == "CID":
|
|
addCharacterAsset(parts)
|
|
case parts[0] == "Character":
|
|
addCharacterAsset(parts)
|
|
case parts[0] == "BID":
|
|
addBackpackAsset(parts)
|
|
case parts[0] == "EID":
|
|
addEmoteAsset(parts)
|
|
case parts[0] == "Emote":
|
|
addEmoteAsset(parts)
|
|
case parts[0] == "Pickaxe":
|
|
addPickaxeAsset(parts)
|
|
case parts[0] == "Wrap":
|
|
addWrapAsset(parts)
|
|
case parts[0] == "Glider":
|
|
addGliderAsset(parts)
|
|
case parts[0] == "MusicPack":
|
|
addMusicAsset(parts)
|
|
}
|
|
}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.DisplayAssetPath2 == "" {
|
|
continue
|
|
}
|
|
|
|
withDisplayAssets++
|
|
}
|
|
|
|
variants := storage.HttpAsset("QF3nHCFt1vhELoU4q1VKTmpxnk20c2iAiBEBzlbzQAY.json")
|
|
if variants == nil {
|
|
panic("Failed to load variants")
|
|
}
|
|
|
|
err = json.Unmarshal(*variants, &Cosmetics.VariantTokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
aid.Print("(snow) preloaded", len(Cosmetics.Items), "cosmetics")
|
|
return nil
|
|
}
|
|
|
|
func addCharacterAsset(parts []string) {
|
|
character := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "outfit" {
|
|
continue
|
|
}
|
|
|
|
if parts[0] == "CID" {
|
|
cid := ""
|
|
if parts[1] != "A" {
|
|
cid = parts[0] + "_" + parts[1]
|
|
}
|
|
|
|
if parts[1] == "A" {
|
|
cid = parts[0] + "_A_" + parts[2]
|
|
}
|
|
|
|
if strings.Contains(item.ID, cid) {
|
|
character = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if parts[0] == "Character" {
|
|
if strings.Contains(item.ID, parts[1]) {
|
|
character = item
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if character.ID == "" {
|
|
return
|
|
}
|
|
|
|
character.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(character)
|
|
}
|
|
|
|
func addBackpackAsset(parts []string) {
|
|
backpack := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "backpack" {
|
|
continue
|
|
}
|
|
|
|
bid := ""
|
|
if parts[1] != "A" {
|
|
bid = parts[0] + "_" + parts[1]
|
|
}
|
|
|
|
if parts[1] == "A" {
|
|
bid = parts[0] + "_A_" + parts[2]
|
|
}
|
|
|
|
if strings.Contains(item.ID, bid) {
|
|
backpack = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if backpack.ID == "" {
|
|
return
|
|
}
|
|
|
|
backpack.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(backpack)
|
|
}
|
|
|
|
func addEmoteAsset(parts []string) {
|
|
emote := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "emote" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(item.ID, parts[1]) {
|
|
emote = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if emote.ID == "" {
|
|
return
|
|
}
|
|
|
|
emote.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(emote)
|
|
}
|
|
|
|
func addPickaxeAsset(parts []string) {
|
|
pickaxe := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "pickaxe" {
|
|
continue
|
|
}
|
|
|
|
pickaxeId := ""
|
|
if parts[1] != "ID" {
|
|
pickaxeId = parts[0] + "_" + parts[1]
|
|
}
|
|
|
|
if parts[1] == "ID" {
|
|
pickaxeId = parts[0] + "_ID_" + parts[2]
|
|
}
|
|
|
|
if strings.Contains(item.ID, pickaxeId) {
|
|
pickaxe = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if pickaxe.ID == "" {
|
|
return
|
|
}
|
|
|
|
pickaxe.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(pickaxe)
|
|
}
|
|
|
|
func addGliderAsset(parts []string) {
|
|
glider := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "glider" {
|
|
continue
|
|
}
|
|
|
|
gliderId := ""
|
|
if parts[1] != "ID" {
|
|
gliderId = parts[0] + "_" + parts[1]
|
|
}
|
|
|
|
if parts[1] == "ID" {
|
|
gliderId = parts[0] + "_ID_" + parts[2]
|
|
}
|
|
|
|
if strings.Contains(item.ID, gliderId) {
|
|
glider = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if glider.ID == "" {
|
|
return
|
|
}
|
|
|
|
glider.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(glider)
|
|
}
|
|
|
|
func addWrapAsset(parts []string) {
|
|
wrap := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "wrap" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(item.ID, parts[1]) {
|
|
wrap = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if wrap.ID == "" {
|
|
return
|
|
}
|
|
|
|
wrap.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(wrap)
|
|
}
|
|
|
|
func addMusicAsset(parts []string) {
|
|
music := FAPI_Cosmetic{}
|
|
|
|
for _, item := range Cosmetics.Items {
|
|
if item.Type.Value != "music" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(item.ID, parts[1]) {
|
|
music = item
|
|
break
|
|
}
|
|
}
|
|
|
|
if music.ID == "" {
|
|
return
|
|
}
|
|
|
|
music.DisplayAssetPath2 = "DAv2_" + strings.Join(parts, "_")
|
|
Cosmetics.AddItem(music)
|
|
} |