snow/fortnite/external.go
Eccentric 250e85732d feat: update to latest;
new shop system
more config options
arena & hype
per season stats
battle pass
better variant system
complete vbuck & starter pack store
fix bugs related to deleting account
update launcher endpoints
fixed gift loot not deleting
2024-03-10 18:16:42 +00:00

504 lines
15 KiB
Go

package fortnite
import (
"encoding/json"
"fmt"
"io"
"net/http"
"slices"
"strings"
"github.com/ectrc/snow/aid"
"github.com/ectrc/snow/storage"
)
var (
DataClient *dataClient
)
type dataClient struct {
h *http.Client
FortniteSets map[string]*APISetDefinition `json:"sets"`
FortniteItems map[string]*APICosmeticDefinition `json:"items"`
FortniteItemsWithDisplayAssets map[string]*APICosmeticDefinition `json:"-"`
FortniteItemsWithFeaturedImage []*APICosmeticDefinition `json:"-"`
TypedFortniteItems map[string][]*APICosmeticDefinition `json:"-"`
TypedFortniteItemsWithDisplayAssets map[string][]*APICosmeticDefinition `json:"-"`
SnowVariantTokens map[string]*FortniteVariantToken `json:"variants"`
StorefrontCosmeticOfferPriceLookup map[string]map[string]int `json:"-"`
StorefrontDailyItemCountLookup []struct{Season int;Items int} `json:"-"`
StorefrontWeeklySetCountLookup []struct{Season int;Sets int} `json:"-"`
StorefrontCurrencyOfferPriceLookup map[string]map[int]int `json:"-"`
StorefrontCurrencyMultiplier map[string]float64 `json:"-"`
SnowSeason *SnowSeasonDefinition `json:"season"`
}
func NewDataClient() *dataClient {
return &dataClient{
h: &http.Client{},
FortniteItems: make(map[string]*APICosmeticDefinition),
FortniteSets: make(map[string]*APISetDefinition),
FortniteItemsWithDisplayAssets: make(map[string]*APICosmeticDefinition),
FortniteItemsWithFeaturedImage: []*APICosmeticDefinition{},
TypedFortniteItems: make(map[string][]*APICosmeticDefinition),
TypedFortniteItemsWithDisplayAssets: make(map[string][]*APICosmeticDefinition),
SnowVariantTokens: make(map[string]*FortniteVariantToken),
StorefrontDailyItemCountLookup: []struct{Season int;Items int}{
{2, 4},
{4, 6},
{13, 10},
},
StorefrontWeeklySetCountLookup: []struct{Season int;Sets int}{
{2, 2},
{4, 3},
{13, 5},
},
StorefrontCosmeticOfferPriceLookup: map[string]map[string]int{
"EFortRarity::Legendary": {
"AthenaCharacter": 2000,
"AthenaBackpack": 300,
"AthenaPickaxe": 1500,
"AthenaGlider": 1800,
"AthenaDance": 500,
"AthenaItemWrap": 800,
},
"EFortRarity::Epic": {
"AthenaCharacter": 1500,
"AthenaBackpack": 250,
"AthenaPickaxe": 1200,
"AthenaGlider": 1500,
"AthenaDance": 800,
"AthenaItemWrap": 800,
},
"EFortRarity::Rare": {
"AthenaCharacter": 1200,
"AthenaBackpack": 200,
"AthenaPickaxe": 800,
"AthenaGlider": 800,
"AthenaDance": 500,
"AthenaItemWrap": 600,
},
"EFortRarity::Uncommon": {
"AthenaCharacter": 800,
"AthenaBackpack": 200,
"AthenaPickaxe": 500,
"AthenaGlider": 500,
"AthenaDance": 200,
"AthenaItemWrap": 300,
},
"EFortRarity::Common": {
"AthenaCharacter": 500,
"AthenaBackpack": 200,
"AthenaPickaxe": 500,
"AthenaGlider": 500,
"AthenaDance": 200,
"AthenaItemWrap": 300,
},
},
StorefrontCurrencyOfferPriceLookup: map[string]map[int]int{
"USD": {
1000: 999,
2800: 2499,
5000: 3999,
7500: 5999,
13500: 9999,
},
"GBP": {
1000: 799,
2800: 1999,
5000: 3499,
7500: 4999,
13500: 7999,
},
},
StorefrontCurrencyMultiplier: map[string]float64{
"USD": 1.2503128911,
"GBP": 1.0,
},
}
}
func (c *dataClient) LoadExternalData() {
req, err := http.NewRequest("GET", "https://fortnite-api.com/v2/cosmetics/br", nil)
if err != nil {
return
}
resp, err := c.h.Do(req)
if err != nil {
return
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return
}
content := &APICosmeticsResponse{}
err = json.Unmarshal(bodyBytes, content)
if err != nil {
return
}
for _, item := range content.Data {
c.LoadItemDefinition(&item)
}
for _, item := range c.TypedFortniteItems["AthenaBackpack"] {
c.AddBackpackToItem(item)
}
displayAssets := storage.HttpAsset[[]string]("assets.snow.json")
if displayAssets == nil {
return
}
for _, displayAsset := range *displayAssets {
c.AddDisplayAssetToItem(displayAsset)
}
variantTokens := storage.HttpAsset[map[string]SnowCosmeticVariantDefinition]("variants.snow.json")
if variantTokens == nil {
return
}
for k, v := range *variantTokens {
item := c.FortniteItems[v.Item]
if item == nil {
continue
}
c.SnowVariantTokens[k] = &FortniteVariantToken{
Grants: v.Grants,
Item: item,
Name: v.Name,
Gift: v.Gift,
Equip: v.Equip,
Unseen: v.Unseen,
}
}
addNumericStylesToSets := []string{"Soccer", "Football", "ScaryBall"}
for _, setValue := range addNumericStylesToSets {
set, found := c.FortniteSets[setValue]
if !found {
continue
}
for _, item := range set.Items {
c.AddNumericStylesToItem(item)
}
}
athenaSeasonObj := storage.HttpAsset[[]UnrealEngineObject[UnrealSeasonProperties, UnrealNoRows]]("season.snow.json")
if athenaSeasonObj == nil {
return
}
levelProgressionObj := storage.HttpAsset[[]UnrealEngineObject[UnrealProgressionProperties, UnrealProgressionRows]]("progression.levels.snow.json")
if levelProgressionObj == nil {
return
}
bookProgressionObj := storage.HttpAsset[[]UnrealEngineObject[UnrealProgressionProperties, UnrealProgressionRows]]("progression.book.snow.json")
if bookProgressionObj == nil {
return
}
c.SnowSeason = NewSeasonDefinition()
for index, tier := range (*athenaSeasonObj)[0].Properties.BookXpSchedulePaid.Levels {
c.SnowSeason.TierRewardsPremium[index] = []*ItemGrant{}
for _, reward := range tier.Rewards {
templateId := aid.Ternary[string](reward.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(reward.ItemDefinition.AssetPathName), reward.TemplateID)
c.SnowSeason.TierRewardsPremium[index] = append(c.SnowSeason.TierRewardsPremium[index], NewItemGrant(templateId, reward.Quantity))
}
}
c.SnowSeason.TierRewardsPremium[0] = []*ItemGrant{}
for index, tier := range (*athenaSeasonObj)[0].Properties.BookXpScheduleFree.Levels {
c.SnowSeason.TierRewardsFree[index] = []*ItemGrant{}
for _, reward := range tier.Rewards {
templateId := aid.Ternary[string](reward.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(reward.ItemDefinition.AssetPathName), reward.TemplateID)
c.SnowSeason.TierRewardsFree[index] = append(c.SnowSeason.TierRewardsFree[index], NewItemGrant(templateId, reward.Quantity))
}
}
c.SnowSeason.TierRewardsFree[0] = []*ItemGrant{}
for index, level := range (*athenaSeasonObj)[0].Properties.SeasonXpScheduleFree.Levels {
c.SnowSeason.LevelRewards[index] = []*ItemGrant{}
for _, reward := range level.Rewards {
templateId := aid.Ternary[string](reward.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(reward.ItemDefinition.AssetPathName), reward.TemplateID)
c.SnowSeason.LevelRewards[index] = append(c.SnowSeason.LevelRewards[index], NewItemGrant(templateId, reward.Quantity))
}
}
c.SnowSeason.LevelRewards[0] = []*ItemGrant{}
for _, token := range (*athenaSeasonObj)[0].Properties.TokensToRemoveAtSeasonEnd {
c.SnowSeason.SeasonTokenRemoval = append(c.SnowSeason.SeasonTokenRemoval, NewItemGrant(convertAssetPathToTemplateId(token.AssetPathName), 1))
}
for _, reward := range (*athenaSeasonObj)[0].Properties.SeasonFirstWinRewards.Rewards {
templateId := aid.Ternary[string](reward.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(reward.ItemDefinition.AssetPathName), reward.TemplateID)
c.SnowSeason.VictoryRewards = append(c.SnowSeason.VictoryRewards, NewItemGrant(templateId, reward.Quantity))
}
for _, token := range (*athenaSeasonObj)[0].Properties.ExpiringRewardTypes {
c.SnowSeason.SeasonTokenRemoval = append(c.SnowSeason.SeasonTokenRemoval, NewItemGrant(convertAssetPathToTemplateId(token.AssetPathName), 1))
}
for _, reward := range (*athenaSeasonObj)[0].Properties.SeasonGrantsToEveryone.Rewards {
templateId := aid.Ternary[string](reward.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(reward.ItemDefinition.AssetPathName), reward.TemplateID)
c.SnowSeason.TierRewardsFree[0] = append(c.SnowSeason.TierRewardsFree[0], NewItemGrant(templateId, reward.Quantity))
}
for _, replacement := range (*athenaSeasonObj)[0].Properties.BattleStarSubstitutionReward.Rewards {
templateId := aid.Ternary[string](replacement.ItemDefinition.AssetPathName != "None", convertAssetPathToTemplateId(replacement.ItemDefinition.AssetPathName), replacement.TemplateID)
c.SnowSeason.BookXPReplacements = append(c.SnowSeason.BookXPReplacements, NewItemGrant(templateId, replacement.Quantity))
}
for indexString, row := range *(*levelProgressionObj)[0].Rows {
index := aid.ToInt(indexString)
c.SnowSeason.LevelProgression[index] = &row
}
c.SnowSeason.LevelProgression[0] = &UnrealProgressionRows{
Level: 0,
XpToNextLevel: 0,
XpTotal: 0,
}
c.SnowSeason.LevelProgression[100] = &UnrealProgressionRows{
Level: 100,
XpToNextLevel: 0,
XpTotal: c.SnowSeason.LevelProgression[99].XpTotal + c.SnowSeason.LevelProgression[99].XpToNextLevel,
}
for indexString, row := range *(*bookProgressionObj)[0].Rows {
index := aid.ToInt(indexString)
c.SnowSeason.BookProgression[index] = &row
}
c.SnowSeason.BookProgression[0] = &UnrealProgressionRows{
Level: 0,
XpToNextLevel: 0,
XpTotal: 0,
}
c.SnowSeason.BookProgression[100] = &UnrealProgressionRows{
Level: 100,
XpToNextLevel: 0,
XpTotal: c.SnowSeason.BookProgression[99].XpTotal + c.SnowSeason.BookProgression[99].XpToNextLevel,
}
c.SnowSeason.DefaultOfferID = (*athenaSeasonObj)[0].Properties.BattlePassOfferId
c.SnowSeason.BundleOfferID = (*athenaSeasonObj)[0].Properties.BattlePassBundleOfferId
c.SnowSeason.TierOfferID = (*athenaSeasonObj)[0].Properties.BattlePassLevelOfferID
}
func (c *dataClient) LoadItemDefinition(item *APICosmeticDefinition) {
if item.Introduction.BackendValue > aid.Config.Fortnite.Season || item.Introduction.BackendValue == 0 {
return
}
typeLookup := map[string]string{
"AthenaCharacter": "AthenaCharacter",
"AthenaBackpack": "AthenaBackpack",
"AthenaPickaxe": "AthenaPickaxe",
"AthenaGlider": "AthenaGlider",
"AthenaDance": "AthenaDance",
"AthenaToy": "AthenaDance",
"AthenaEmoji": "AthenaEmoji",
"AthenaItemWrap": "AthenaItemWrap",
"AthenaMusicPack": "AthenaMusicPack",
"AthenaPet": "AthenaBackpack",
"AthenaPetCarrier": "AthenaBackpack",
"AthenaLoadingScreen": "AthenaLoadingScreen",
"AthenaSkyDiveContrail": "AthenaSkyDiveContrail",
}
item.Type.BackendValue = aid.Ternary[string](typeLookup[item.Type.BackendValue] != "", typeLookup[item.Type.BackendValue], item.Type.BackendValue)
if c.FortniteSets[item.Set.BackendValue] == nil {
c.FortniteSets[item.Set.BackendValue] = &APISetDefinition{
BackendName: item.Set.Value,
DisplayName: item.Set.Text,
Items: []*APICosmeticDefinition{},
}
}
if c.TypedFortniteItems[item.Type.BackendValue] == nil {
c.TypedFortniteItems[item.Type.BackendValue] = []*APICosmeticDefinition{}
}
c.FortniteItems[item.ID] = item
c.FortniteSets[item.Set.BackendValue].Items = append(c.FortniteSets[item.Set.BackendValue].Items, item)
c.TypedFortniteItems[item.Type.BackendValue] = append(c.TypedFortniteItems[item.Type.BackendValue], item)
if item.Type.BackendValue != "AthenaCharacter" || item.Images.Featured == "" || slices.Contains[[]string]([]string{
"Soccer",
"Football",
"Waypoint",
}, item.Set.BackendValue) {
return
}
for _, tag := range item.GameplayTags {
if strings.Contains(tag, "StarterPack") {
return
}
}
c.FortniteItemsWithFeaturedImage = append(c.FortniteItemsWithFeaturedImage, item)
}
func (c *dataClient) AddBackpackToItem(backpack *APICosmeticDefinition) {
if backpack.ItemPreviewHeroPath == "" {
return
}
splitter := strings.Split(backpack.ItemPreviewHeroPath, "/")
character, found := c.FortniteItems[splitter[len(splitter) - 1]]
if !found {
return
}
character.BackpackDefinition = backpack
}
func (c *dataClient) AddDisplayAssetToItem(displayAsset string) {
split := strings.Split(displayAsset, "_")[1:]
found := c.FortniteItems[strings.Join(split[:], "_")]
if found == nil && split[0] == "CID" {
r := aid.Regex(strings.Join(split[:], "_"), `(?:CID_)(\d+|A_\d+)(?:_.+)`)
if r != nil {
found = GetItemByShallowID(*r)
}
}
if found == nil {
return
}
found.NewDisplayAssetPath = displayAsset
c.FortniteItemsWithDisplayAssets[found.ID] = found
c.TypedFortniteItemsWithDisplayAssets[found.Type.BackendValue] = append(c.TypedFortniteItemsWithDisplayAssets[found.Type.BackendValue], found)
}
func (c *dataClient) AddNumericStylesToItem(item *APICosmeticDefinition) {
ownedStyles := []APICosmeticDefinitionVariant{}
for i := 0; i < 100; i++ {
ownedStyles = append(ownedStyles, APICosmeticDefinitionVariant{
Tag: fmt.Sprint(i),
})
}
item.Variants = append(item.Variants, APICosmeticDefinitionVariantChannel{
Channel: "Numeric",
Type: "int",
Options: ownedStyles,
})
}
func (c *dataClient) GetStorefrontDailyItemCount(season int) int {
currentValue := 4
for _, item := range c.StorefrontDailyItemCountLookup {
if item.Season > season {
continue
}
currentValue = item.Items
}
return currentValue
}
func (c *dataClient) GetStorefrontWeeklySetCount(season int) int {
currentValue := 2
// for _, item := range c.StorefrontWeeklySetCountLookup {
// if item.Season > season {
// continue
// }
// currentValue = item.Sets
// }
return currentValue
}
func (c *dataClient) GetStorefrontCosmeticOfferPrice(rarity string, type_ string) int {
return c.StorefrontCosmeticOfferPriceLookup[rarity][type_]
}
func (c *dataClient) GetStorefrontCurrencyOfferPrice(currency string, amount int) int {
return c.StorefrontCurrencyOfferPriceLookup[currency][amount]
}
func (c *dataClient) GetStorefrontLocalizedOfferPrice(currency string, amount int) int {
return int(float64(amount) * c.StorefrontCurrencyMultiplier[currency])
}
func PreloadCosmetics() error {
DataClient = NewDataClient()
DataClient.LoadExternalData()
aid.Print("(snow) " + fmt.Sprint(len(DataClient.FortniteItems)) + " cosmetics loaded from fortnite-api.com")
return nil
}
func GetItemByShallowID(shallowID string) *APICosmeticDefinition {
for _, item := range DataClient.TypedFortniteItems["AthenaCharacter"] {
if strings.Contains(item.ID, shallowID) {
return item
}
}
return nil
}
func GetRandomItemWithDisplayAsset() *APICosmeticDefinition {
items := DataClient.FortniteItemsWithDisplayAssets
if len(items) == 0 {
return nil
}
flat := []APICosmeticDefinition{}
for _, item := range items {
flat = append(flat, *item)
}
slices.SortFunc[[]APICosmeticDefinition](flat, func(a, b APICosmeticDefinition) int {
return strings.Compare(a.ID, b.ID)
})
return &flat[aid.RandomInt(0, len(flat))]
}
func GetRandomItemWithDisplayAssetOfNotType(notType string) *APICosmeticDefinition {
flat := []APICosmeticDefinition{}
for t, items := range DataClient.TypedFortniteItemsWithDisplayAssets {
if t == notType {
continue
}
for _, item := range items {
flat = append(flat, *item)
}
}
slices.SortFunc[[]APICosmeticDefinition](flat, func(a, b APICosmeticDefinition) int {
return strings.Compare(a.ID, b.ID)
})
return &flat[aid.RandomInt(0, len(flat))]
}
func GetRandomSet() *APISetDefinition {
sets := []APISetDefinition{}
for _, set := range DataClient.FortniteSets {
if set.BackendName == "" {
continue
}
sets = append(sets, *set)
}
slices.SortFunc[[]APISetDefinition](sets, func(a, b APISetDefinition) int {
return strings.Compare(a.BackendName, b.BackendName)
})
return &sets[aid.RandomInt(0, len(sets))]
}