250e85732d
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
504 lines
15 KiB
Go
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))]
|
|
} |