Continue to research how fortnite storefront works.

This commit is contained in:
eccentric 2023-11-26 22:39:05 +00:00
parent 731705c0f4
commit 5348295631
10 changed files with 423 additions and 7187 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@
.vscode
tmp
config.ini
hide_*

231
fortnite/interact.go Normal file
View File

@ -0,0 +1,231 @@
package fortnite
import (
"encoding/json"
"io"
"math/rand"
"net/http"
"strings"
"github.com/ectrc/snow/aid"
)
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 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 []struct {
Channel string `json:"channel"`
Type string `json:"type"`
Options []struct {
Tag string `json:"tag"`
Name string `json:"name"`
Image string `json:"image"`
} `json:"options"`
} `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"`
ItemPreviewHeroPath string `json:"itemPreviewHeroPath"`
Backpack string `json:"backpack"`
Path string `json:"path"`
Added string `json:"added"`
ShopHistory []string `json:"shopHistory"`
}
type Set struct {
Items map[string]FAPI_Cosmetic `json:"items"`
Name string `json:"name"`
}
type CosmeticData struct {
Items map[string]FAPI_Cosmetic `json:"items"`
Sets map[string]Set `json:"sets"`
}
func (c *CosmeticData) GetRandomItem() FAPI_Cosmetic {
randomInt := rand.Intn(len(c.Items))
i := 0
for _, item := range c.Items {
if i == randomInt {
return item
}
i++
}
return c.GetRandomItem()
}
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 (
StaticAPI = NewFortniteAPI()
Cosmetics = CosmeticData{
Items: make(map[string]FAPI_Cosmetic),
Sets: make(map[string]Set),
}
)
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 PreloadCosmetics(max int) error {
list, err := StaticAPI.GetAllCosmetics()
if err != nil {
return err
}
for _, item := range list {
if item.Introduction.BackendValue > max {
continue
}
Cosmetics.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,
}
}
Cosmetics.Sets[item.Set.BackendValue].Items[item.ID] = item
}
}
aid.Print("Preloaded", len(Cosmetics.Items), "cosmetics")
aid.Print("Preloaded", len(Cosmetics.Sets), "sets")
notFound := make([]string, 0)
for id, item := range Cosmetics.Items {
if item.ItemPreviewHeroPath == "" {
continue
}
if item.Type.Value != "AthenaBackpack" {
continue
}
previewHeroPath := strings.Split(item.ItemPreviewHeroPath, "/")
characterId := previewHeroPath[len(previewHeroPath)-1]
character, ok := Cosmetics.Items[characterId]
if !ok {
notFound = append(notFound, characterId)
continue
}
character.Backpack = id
Cosmetics.Items[characterId] = character
Cosmetics.Sets[character.Set.BackendValue].Items[characterId] = character
}
aid.Print("Could not find", len(notFound), "items with backpacks")
return nil
}

View File

@ -1,12 +1,11 @@
package fortnite
import (
"encoding/json"
"strconv"
"strings"
"github.com/ectrc/snow/aid"
p "github.com/ectrc/snow/person"
"github.com/ectrc/snow/storage"
)
var (
@ -35,21 +34,27 @@ func NewFortnitePerson(displayName string, key string) *p.Person {
for _, item := range defaultCommonCoreItems {
if item == "HomebaseBannerIcon:StandardBanner" {
for i := 1; i < 32; i++ {
person.CommonCoreProfile.Items.AddItem(p.NewItem(item+strconv.Itoa(i), 1)).Save()
item := p.NewItem(item+strconv.Itoa(i), 1)
item.HasSeen = true
person.CommonCoreProfile.Items.AddItem(item).Save()
}
continue
}
if item == "HomebaseBannerColor:DefaultColor" {
for i := 1; i < 22; i++ {
person.CommonCoreProfile.Items.AddItem(p.NewItem(item+strconv.Itoa(i), 1)).Save()
item := p.NewItem(item+strconv.Itoa(i), 1)
item.HasSeen = true
person.CommonCoreProfile.Items.AddItem(item).Save()
}
continue
}
if item == "Currency:MtxPurchased" {
person.CommonCoreProfile.Items.AddItem(p.NewItem(item, 0)).Save()
person.Profile0Profile.Items.AddItem(p.NewItem(item, 0)).Save()
item := p.NewItem(item, 0)
item.HasSeen = true
person.CommonCoreProfile.Items.AddItem(item).Save()
person.Profile0Profile.Items.AddItem(item).Save()
continue
}
@ -106,19 +111,19 @@ func NewFortnitePerson(displayName string, key string) *p.Person {
loadout := p.NewLoadout("sandbox_loadout", person.AthenaProfile)
person.AthenaProfile.Loadouts.AddLoadout(loadout).Save()
person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("loadouts", []string{
loadout.ID,
})).Save()
person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("loadouts", []string{loadout.ID})).Save()
person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("last_applied_loadout", loadout.ID)).Save()
person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("active_loadout_index", 0)).Save()
if aid.Config.Fortnite.Everything {
allItemsBytes := storage.Asset("cosmetics.json")
var allItems []string
json.Unmarshal(*allItemsBytes, &allItems)
for _, item := range Cosmetics.Items {
if strings.Contains(strings.ToLower(item.ID), "random") {
continue
}
for _, item := range allItems {
person.AthenaProfile.Items.AddItem(p.NewItem(item, 1)).Save()
item := p.NewItem(item.Type.BackendValue + ":" + item.ID, 1)
item.HasSeen = true
person.AthenaProfile.Items.AddItem(item).Save()
}
}

View File

@ -1,10 +1,27 @@
package fortnite
import (
"sort"
"github.com/ectrc/snow/aid"
"github.com/ectrc/snow/person"
"github.com/google/uuid"
)
var (
Rarities = map[string]int{
"EFortRarity::Legendary": 2000,
"EFortRarity::Epic": 1500,
"EFortRarity::Rare": 1200,
"EFortRarity::Uncommon": 800,
"EFortRarity::Common": 500,
}
)
func GetPriceForRarity(rarity string) int {
return Rarities[rarity]
}
type Catalog struct {
RefreshIntervalHrs int `json:"refreshIntervalHrs"`
DailyPurchaseHrs int `json:"dailyPurchaseHrs"`
@ -63,87 +80,63 @@ func (s *Storefront) GenerateResponse(p *person.Person) aid.JSON {
"catalogEntries": []aid.JSON{},
}
names := []string{}
for _, entry := range s.CatalogEntries {
grantStrings := entry.Grants
sort.Strings(grantStrings)
for _, grant := range grantStrings {
entry.Name += grant + "-"
}
names = append(names, entry.Name)
}
aid.PrintJSON(names)
sort.Strings(names)
for _, devname := range names {
for _, entry := range s.CatalogEntries {
grantStrings := entry.Grants
sort.Strings(grantStrings)
for _, grant := range grantStrings {
entry.Name += grant + "-"
}
if devname == entry.Name {
json["catalogEntries"] = append(json["catalogEntries"].([]aid.JSON), entry.GenerateResponse(p))
}
}
}
return json
}
type Entry struct {
Price int
ID string
Name string
Title string
Description string
Type string
Price int
Meta []aid.JSON
Panel string
Priority int
Asset string
Grants []string
IsBundle bool
BundleMeta BundleMeta
DisplayAssetPath string
Title string
ShortDescription string
}
func NewItemEntry(id string, name string, price int) *Entry {
func NewCatalogEntry(meta ...aid.JSON) *Entry {
return &Entry{
Price: price,
ID: id,
Name: name,
Type: "StaticPrice",
ID: uuid.New().String(),
Meta: meta,
}
}
func NewBundleEntry(id string, name string, price int) *Entry {
return &Entry{
Price: price,
ID: id,
Name: name,
Type: "DynamicBundle",
IsBundle: true,
BundleMeta: BundleMeta{
FloorPrice: price,
RegularBasePrice: price,
DiscountedBasePrice: price,
},
}
}
type BundleMeta struct {
FloorPrice int
RegularBasePrice int
DiscountedBasePrice int
DisplayType string // "AmountOff" or "PercentOff"
BundleItems []BundleItem
}
type BundleItem struct {
TemplateID string
RegularPrice int
DiscountedPrice int
AlreadyOwnedPriceReduction int
}
func NewBundleItem(templateId string, regularPrice int, discountedPrice int, alreadyOwnedPriceReduction int) *BundleItem {
return &BundleItem{
TemplateID: templateId,
RegularPrice: regularPrice,
DiscountedPrice: discountedPrice,
AlreadyOwnedPriceReduction: alreadyOwnedPriceReduction,
}
}
func (e *Entry) AddGrant(templateId string) *Entry {
e.Grants = append(e.Grants, templateId)
return e
}
func (e *Entry) AddBundleGrant(B BundleItem) *Entry {
e.BundleMeta.BundleItems = append(e.BundleMeta.BundleItems, B)
return e
}
func (e *Entry) AddMeta(key string, value interface{}) *Entry {
e.Meta = append(e.Meta, aid.JSON{
"Key": key,
@ -152,11 +145,58 @@ func (e *Entry) AddMeta(key string, value interface{}) *Entry {
return e
}
func (e *Entry) TileSize(size string) *Entry {
e.Meta = append(e.Meta, aid.JSON{
"Key": "TileSize",
"Value": size,
})
return e
}
func (e *Entry) PanelType(panel string) *Entry {
e.Panel = panel
return e
}
func (e *Entry) Section(sectionId string) *Entry {
e.Meta = append(e.Meta, aid.JSON{
"Key": "SectionId",
"Value": sectionId,
})
return e
}
func (e *Entry) DisplayAsset(asset string) *Entry {
e.DisplayAssetPath = asset
return e
}
func (e *Entry) SetTitle(title string) *Entry {
e.Title = title
return e
}
func (e *Entry) SetShortDescription(description string) *Entry {
e.ShortDescription = description
return e
}
func (e *Entry) SetPrice(price int) *Entry {
e.Price = price
return e
}
func (e *Entry) GenerateResponse(p *person.Person) aid.JSON {
grantStrings := e.Grants
sort.Strings(grantStrings)
for _, grant := range grantStrings {
e.Name += grant + "-"
}
json := aid.JSON{
"offerId": e.ID,
"devName": e.Name,
"offerType": e.Type,
"offerType": "StaticPrice",
"prices": []aid.JSON{
{
"currencyType": "MtxCurrency",
@ -169,18 +209,23 @@ func (e *Entry) GenerateResponse(p *person.Person) aid.JSON {
},
},
"categories": []string{},
"catalogGroupPriority": e.Priority,
"catalogGroupPriority": 0,
"dailyLimit": -1,
"weeklyLimit": -1,
"monthlyLimit": -1,
"fufillmentIds": []string{},
"filterWeight": 0,
"filterWeight": e.Priority,
"appStoreId": []string{},
"refundable": false,
"itemGrants": []aid.JSON{},
"metaInfo": e.Meta,
"meta": aid.JSON{},
"displayAssetPath": e.Asset,
"title": e.Title,
"shortDescription": e.ShortDescription,
}
if e.DisplayAssetPath != "" {
json["displayAssetPath"] = "/" + e.DisplayAssetPath
}
grants := []aid.JSON{}
@ -211,45 +256,6 @@ func (e *Entry) GenerateResponse(p *person.Person) aid.JSON {
json["categories"] = []string{e.Panel}
}
if e.IsBundle {
json["dynamicBundleInfo"] = aid.JSON{
"discountedBasePrice": e.BundleMeta.DiscountedBasePrice,
"regularBasePrice": e.BundleMeta.RegularBasePrice,
"floorPrice": e.BundleMeta.FloorPrice,
"currencyType": "MtxCurrency",
"currencySubType": "Currency",
"displayType": "AmountOff",
"bundleItems": []aid.JSON{},
}
for _, bundleItem := range e.BundleMeta.BundleItems {
json["prices"] = []aid.JSON{}
json["dynamicBundleInfo"].(aid.JSON)["bundleItems"] = append(json["dynamicBundleInfo"].(aid.JSON)["bundleItems"].([]aid.JSON), aid.JSON{
"regularPrice": bundleItem.RegularPrice,
"discountedPrice": bundleItem.DiscountedPrice,
"alreadyOwnedPriceReduction": bundleItem.AlreadyOwnedPriceReduction,
"item": aid.JSON{
"templateId": bundleItem.TemplateID,
"quantity": 1,
},
})
grants = append(grants, aid.JSON{
"templateId": bundleItem.TemplateID,
"quantity": 1,
})
if item := p.AthenaProfile.Items.GetItemByTemplateID(bundleItem.TemplateID); item != nil {
requirements = append(requirements, aid.JSON{
"requirementType": "DenyOnItemOwnership",
"requiredId": item.ID,
"minQuantity": 1,
})
}
}
}
json["itemGrants"] = grants
json["requirements"] = requirements
json["metaInfo"] = meta

2
go.mod
View File

@ -17,10 +17,12 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lukechampine/randmap v0.0.0-20161125183226-9e3c222d0413 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect

4
go.sum
View File

@ -194,6 +194,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lukechampine/randmap v0.0.0-20161125183226-9e3c222d0413 h1:tysX0ocX3VYPAW06M8E5sEkRzr+7ygS162QpIi/N3hI=
github.com/lukechampine/randmap v0.0.0-20161125183226-9e3c222d0413/go.mod h1:CDBUzfMMkesqPDGmuMzuDrzBd2069GPe4wMJ3FC2sEw=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
@ -207,6 +209,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=

10
handlers/snow.go Normal file
View File

@ -0,0 +1,10 @@
package handlers
import (
"github.com/ectrc/snow/fortnite"
"github.com/gofiber/fiber/v2"
)
func GetPrelaodedCosmetics(c *fiber.Ctx) error {
return c.JSON(fortnite.Cosmetics)
}

View File

@ -4,39 +4,59 @@ import (
"github.com/goccy/go-json"
"github.com/ectrc/snow/aid"
"github.com/ectrc/snow/fortnite"
"github.com/ectrc/snow/person"
"github.com/ectrc/snow/storage"
"github.com/gofiber/fiber/v2"
)
func GetStorefrontCatalog(c *fiber.Ctx) error {
person := c.Locals("person").(*person.Person)
storefront := fortnite.NewCatalog()
// person := c.Locals("person").(*person.Person)
// storefront := fortnite.NewCatalog()
bundleStorefront := fortnite.NewStorefront("bundles")
{
bundle := fortnite.NewBundleEntry("v2:/hello_og", "OG Bundle", 300)
bundle.Asset = "/Game/Catalog/NewDisplayAssets/DAv2_CID_A_183_M_AntiquePal_S7A9W.DAv2_CID_A_183_M_AntiquePal_S7A9W"
bundle.AddBundleGrant(*fortnite.NewBundleItem("AthenaCharacter:CID_028_Athena_Commando_F", 1000, 500, 800))
bundle.AddBundleGrant(*fortnite.NewBundleItem("AthenaCharacter:CID_001_Athena_Commando_F", 1000, 500, 800))
bundle.AddMeta("AnalyticOfferGroupId", "3")
bundle.AddMeta("SectionId", "OGBundles")
bundle.AddMeta("TileSize", "DoubleWide")
bundle.AddMeta("NewDisplayAssetPath", bundle.Asset)
bundleStorefront.Add(*bundle)
// daily := fortnite.NewStorefront("BRDailyStorefront")
// weekly := fortnite.NewStorefront("BRWeeklyStorefront")
random := fortnite.NewItemEntry("v2:/random", "Random Bundle", 300)
random.AddGrant("AthenaCharacter:CID_Random")
random.AddMeta("AnalyticOfferGroupId", "3")
random.AddMeta("SectionId", "OGBundles")
random.AddMeta("TileSize", "DoubleWide")
// for len(weekly.CatalogEntries) < 8 {
// set := fortnite.Cosmetics.GetRandomSet()
bundleStorefront.Add(*random)
// for _, cosmetic := range set.Items {
// if cosmetic.Type.BackendValue == "AthenaBackpack" {
// continue
// }
// entry := fortnite.NewCatalogEntry().Section("Featured").DisplayAsset(cosmetic.DisplayAssetPath).SetPrice(fortnite.GetPriceForRarity(cosmetic.Rarity.BackendValue))
// entry.AddGrant(cosmetic.Type.BackendValue + ":" + cosmetic.ID)
// if cosmetic.Backpack != "" {
// entry.AddGrant("AthenaBackpack:" + cosmetic.Backpack)
// }
// if cosmetic.Type.BackendValue != "AthenaCharacter" {
// entry.TileSize("Small")
// }
// if cosmetic.Type.BackendValue == "AthenaCharacter" {
// entry.TileSize("Normal")
// entry.Priority = -99999
// }
// entry.Panel = set.Name
// weekly.Add(*entry)
// }
// }
// storefront.Add(daily)
// storefront.Add(weekly)
// return c.Status(fiber.StatusOK).JSON(storefront.GenerateFortniteCatalog(person))
var x aid.JSON
err := json.Unmarshal(*storage.Asset("hide_a.json"), &x)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(aid.JSON{"error":err.Error()})
}
storefront.Add(bundleStorefront)
return c.Status(fiber.StatusOK).JSON(storefront.GenerateFortniteCatalog(person))
return c.Status(fiber.StatusOK).JSON(x)
}
func GetStorefrontKeychain(c *fiber.Ctx) error {

View File

@ -28,6 +28,8 @@ func init() {
}
func init() {
fortnite.PreloadCosmetics(aid.Config.Fortnite.Season)
if aid.Config.Database.DropAllTables {
fortnite.NewFortnitePerson("ac", "1")
}
@ -95,6 +97,9 @@ func main() {
lightswitch := r.Group("/lightswitch/api")
lightswitch.Get("/service/bulk/status", handlers.GetLightswitchBulkStatus)
snow := r.Group("/snow")
snow.Get("/cosmetics", handlers.GetPrelaodedCosmetics)
r.Hooks().OnListen(func(ld fiber.ListenData) error {
aid.Print("Listening on " + ld.Host + ":" + ld.Port)
return nil

File diff suppressed because it is too large Load Diff