diff --git a/aid/aid.go b/aid/aid.go index 74f3239..b21196b 100644 --- a/aid/aid.go +++ b/aid/aid.go @@ -1,7 +1,6 @@ package aid import ( - m "math/rand" "os" "os/signal" "regexp" @@ -15,20 +14,6 @@ func WaitForExit() { <-sc } -func RandomString(n int) string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - - s := make([]rune, n) - for i := range s { - s[i] = letters[m.Intn(len(letters))] - } - return string(s) -} - -func RandomInt(min, max int) int { - return m.Intn(max - min) + min -} - func FormatNumber(number int) string { str := "" for i, char := range ReverseString(strconv.Itoa(number)) { @@ -60,7 +45,6 @@ func ToHex(number int) string { } func Regex(str, regex string) *string { - // reg := regexp.MustCompile(`(?:CID_)(\d+|A_\d+)(?:_.+)`).FindStringSubmatch(strings.Join(split[:], "_")) reg := regexp.MustCompile(regex).FindStringSubmatch(str) if len(reg) > 1 { return ®[1] diff --git a/aid/config.go b/aid/config.go index cd96bfd..0cabcc7 100644 --- a/aid/config.go +++ b/aid/config.go @@ -48,6 +48,7 @@ type CS struct { Everything bool Password bool DisableClientCredentials bool + ShopSeed int } } @@ -173,4 +174,5 @@ func LoadConfig(file []byte) { Config.Fortnite.Everything = cfg.Section("fortnite").Key("everything").MustBool(false) Config.Fortnite.Password = !(cfg.Section("fortnite").Key("disable_password").MustBool(false)) Config.Fortnite.DisableClientCredentials = cfg.Section("fortnite").Key("disable_client_credentials").MustBool(false) + Config.Fortnite.ShopSeed = cfg.Section("fortnite").Key("shop_seed").MustInt(0) } \ No newline at end of file diff --git a/aid/random.go b/aid/random.go new file mode 100644 index 0000000..edf5f16 --- /dev/null +++ b/aid/random.go @@ -0,0 +1,24 @@ +package aid + +import "math/rand" + +var Random *rand.Rand + +func SetRandom(r *rand.Rand) { + Random = r +} + +func RandomString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + + s := make([]rune, n) + for i := range s { + s[i] = letters[Random.Intn(len(letters))] + } + + return string(s) +} + +func RandomInt(min, max int) int { + return Random.Intn(max-min) + min +} diff --git a/aid/time.go b/aid/time.go index cd66d36..dd3a44d 100644 --- a/aid/time.go +++ b/aid/time.go @@ -17,6 +17,10 @@ func TimeEndOfWeekString() string { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 59, 59, 999999999, time.Now().Location()).AddDate(0, 0, 7).Format("2006-01-02T15:04:05.999Z") } +func CurrentDayUnix() int64 { + return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Now().Location()).Unix() +} + // everything below is taken from the golang standard library I just added the extra units to the map var unitMap = map[string]uint64{ diff --git a/default.config.ini b/default.config.ini index 13982ab..aec6b78 100644 --- a/default.config.ini +++ b/default.config.ini @@ -75,4 +75,8 @@ disable_password=false ; this will disable the client credentials grant type ; however this will also disable a user to get the hotfixes before login ; so xmpp and other hotfix related things will be delayed by ~1 minute -disable_client_credentials=false \ No newline at end of file +disable_client_credentials=false +; this is used to generate a random shop +; each number will generate a different shop for the day +; the shop will stay the same for the entire day even after server restarts +shop_seed=0 \ No newline at end of file diff --git a/fortnite/external.go b/fortnite/external.go index 757a5ab..87b48fd 100644 --- a/fortnite/external.go +++ b/fortnite/external.go @@ -157,7 +157,7 @@ func (c *ExternalDataClient) AddDisplayAssetToItem(displayAsset string) { if found == nil && split[0] == "CID" { r := aid.Regex(strings.Join(split[:], "_"), `(?:CID_)(\d+|A_\d+)(?:_.+)`) if r != nil { - found = ItemByShallowID(*r) + found = GetItemByShallowID(*r) } } @@ -193,7 +193,7 @@ func PreloadCosmetics() error { return nil } -func ItemByShallowID(shallowID string) *FortniteItem { +func GetItemByShallowID(shallowID string) *FortniteItem { for _, item := range External.TypedFortniteItems["AthenaCharacter"] { if strings.Contains(item.ID, shallowID) { return item @@ -203,43 +203,54 @@ func ItemByShallowID(shallowID string) *FortniteItem { return nil } -func RandomItemByType(itemType string) *FortniteItem { - items := External.TypedFortniteItemsWithDisplayAssets[itemType] +func GetRandomItemWithDisplayAsset() *FortniteItem { + items := External.FortniteItemsWithDisplayAssets if len(items) == 0 { return nil } - return items[aid.RandomInt(0, len(items))] + flat := []FortniteItem{} + for _, item := range items { + flat = append(flat, *item) + } + + slices.SortFunc[[]FortniteItem](flat, func(a, b FortniteItem) int { + return strings.Compare(a.ID, b.ID) + }) + + return &flat[aid.RandomInt(0, len(flat))] } -func RandomItemByNotType(notItemType string) *FortniteItem { - allItems := []*FortniteItem{} - - for key, items := range External.TypedFortniteItemsWithDisplayAssets { - if key == notItemType { +func GetRandomItemWithDisplayAssetOfNotType(notType string) *FortniteItem { + flat := []FortniteItem{} + + for t, items := range External.TypedFortniteItemsWithDisplayAssets { + if t == notType { continue } - allItems = append(allItems, items...) + for _, item := range items { + flat = append(flat, *item) + } } - return allItems[aid.RandomInt(0, len(allItems))] + slices.SortFunc[[]FortniteItem](flat, func(a, b FortniteItem) int { + return strings.Compare(a.ID, b.ID) + }) + + return &flat[aid.RandomInt(0, len(flat))] } -func RandomItemWithFeaturedImage() *FortniteItem { - items := External.FortniteItemsWithFeaturedImage - if len(items) == 0 { - return nil - } - return items[aid.RandomInt(0, len(items))] -} - -func RandomSet() *FortniteSet { - sets := []*FortniteSet{} +func GetRandomSet() *FortniteSet { + sets := []FortniteSet{} for _, set := range External.FortniteSets { - sets = append(sets, set) + sets = append(sets, *set) } - return sets[aid.RandomInt(0, len(sets))] + slices.SortFunc[[]FortniteSet](sets, func(a, b FortniteSet) int { + return strings.Compare(a.BackendName, b.BackendName) + }) + + return &sets[aid.RandomInt(0, len(sets))] } \ No newline at end of file diff --git a/fortnite/shop.go b/fortnite/shop.go index 35917f8..ee745a1 100644 --- a/fortnite/shop.go +++ b/fortnite/shop.go @@ -1,476 +1,350 @@ package fortnite import ( - "strings" + "math/rand" + "regexp" "github.com/ectrc/snow/aid" - "github.com/ectrc/snow/person" "github.com/google/uuid" ) var ( - Rarities = map[string]map[string]int{ + priceLookup = map[string]map[string]int{ "EFortRarity::Legendary": { "AthenaCharacter": 2000, - "AthenaBackpack": 1500, - "AthenaPickaxe": 1500, - "AthenaGlider": 1800, - "AthenaDance": 500, - "AthenaItemWrap": 800, + "AthenaBackpack": 1500, + "AthenaPickaxe": 1500, + "AthenaGlider": 1800, + "AthenaDance": 500, + "AthenaItemWrap": 800, }, "EFortRarity::Epic": { "AthenaCharacter": 1500, - "AthenaBackpack": 1200, - "AthenaPickaxe": 1200, - "AthenaGlider": 1500, - "AthenaDance": 800, - "AthenaItemWrap": 800, + "AthenaBackpack": 1200, + "AthenaPickaxe": 1200, + "AthenaGlider": 1500, + "AthenaDance": 800, + "AthenaItemWrap": 800, }, "EFortRarity::Rare": { "AthenaCharacter": 1200, - "AthenaBackpack": 800, - "AthenaPickaxe": 800, - "AthenaGlider": 800, - "AthenaDance": 500, - "AthenaItemWrap": 600, + "AthenaBackpack": 800, + "AthenaPickaxe": 800, + "AthenaGlider": 800, + "AthenaDance": 500, + "AthenaItemWrap": 600, }, "EFortRarity::Uncommon": { "AthenaCharacter": 800, - "AthenaBackpack": 200, - "AthenaPickaxe": 500, - "AthenaGlider": 500, - "AthenaDance": 200, - "AthenaItemWrap": 300, + "AthenaBackpack": 200, + "AthenaPickaxe": 500, + "AthenaGlider": 500, + "AthenaDance": 200, + "AthenaItemWrap": 300, }, "EFortRarity::Common": { "AthenaCharacter": 500, - "AthenaBackpack": 200, - "AthenaPickaxe": 500, - "AthenaGlider": 500, - "AthenaDance": 200, - "AthenaItemWrap": 300, + "AthenaBackpack": 200, + "AthenaPickaxe": 500, + "AthenaGlider": 500, + "AthenaDance": 200, + "AthenaItemWrap": 300, }, } - StaticCatalog = NewCatalog() + + dailyItemLookup = map[int]int{ + 2: 4, + 4: 6, + 13: 10, + } + + weeklySetLookup = map[int]int{ + 2: 2, + 4: 3, + 11: 4, + 13: 3, + } ) -func GetPriceForRarity(rarity string, backendType string) int { - return Rarities[rarity][backendType] +func price(rarity, type_ string) int { + return priceLookup[rarity][type_] } -type Catalog struct { - RefreshIntervalHrs int `json:"refreshIntervalHrs"` - DailyPurchaseHrs int `json:"dailyPurchaseHrs"` - Expiration string `json:"expiration"` - Storefronts []Storefront `json:"storefronts"` -} +func dailyItems(season int) int { + var items int -func NewCatalog() *Catalog { - return &Catalog{ - RefreshIntervalHrs: 24, - DailyPurchaseHrs: 24, - Expiration: aid.TimeEndOfDay(), - Storefronts: []Storefront{}, - } -} - -func (c *Catalog) Add(storefront *Storefront) { - c.Storefronts = append(c.Storefronts, *storefront) -} - -func (c *Catalog) GenerateFortniteCatalog(p *person.Person) aid.JSON { - json := aid.JSON{ - "refreshIntervalHrs": c.RefreshIntervalHrs, - "dailyPurchaseHrs": c.DailyPurchaseHrs, - "expiration": c.Expiration, - "storefronts": []aid.JSON{}, - } - - for _, storefront := range c.Storefronts { - json["storefronts"] = append(json["storefronts"].([]aid.JSON), storefront.GenerateResponse(p)) - } - - return json -} - -func (c *Catalog) CheckIfOfferIsDuplicate(entry Entry) bool { - for _, storefront := range c.Storefronts { - for _, catalogEntry := range storefront.CatalogEntries { - if catalogEntry.Grants[0] == entry.Grants[0] { - return true - } + for s, i := range dailyItemLookup { + if season >= s { + items = i } } - return false + return items } -func (c *Catalog) GetOfferById(id string) *Entry { - for _, storefront := range c.Storefronts { - for _, catalogEntry := range storefront.CatalogEntries { - if catalogEntry.ID == id { - return &catalogEntry +func weeklySets(season int) int { + var sets int + + for s, i := range weeklySetLookup { + if season >= s { + sets = i + } + } + + return sets +} + +type FortniteCatalogSectionOffer struct { + ID string + Grants []*FortniteItem + TotalPrice int + Meta struct { + DisplayAssetPath string + NewDisplayAssetPath string + SectionId string + TileSize string + Category string + ProfileId string + } + Frontend struct { + Title string + Description string + ShortDescription string + } + Giftable bool + BundleInfo struct { + IsBundle bool + PricePercent float32 + } +} + +func NewFortniteCatalogSectionOffer() *FortniteCatalogSectionOffer { + return &FortniteCatalogSectionOffer{} +} + +func (f *FortniteCatalogSectionOffer) GenerateID() { + for _, item := range f.Grants { + f.ID += item.Type.BackendValue + ":" + item.ID + "," + } + + f.ID = "v2:/" + aid.Hash([]byte(f.ID)) +} + +func (f *FortniteCatalogSectionOffer) GenerateTotalPrice() { + if !f.BundleInfo.IsBundle { + f.TotalPrice = price(f.Grants[0].Rarity.BackendValue, f.Grants[0].Type.BackendValue) + return + } + + for _, item := range f.Grants { + f.TotalPrice += price(item.Rarity.BackendValue, item.Rarity.BackendValue) + } +} + +func (f *FortniteCatalogSectionOffer) GenerateFortniteCatalogSectionOffer() aid.JSON { + f.GenerateTotalPrice() + + itemGrantResponse := []aid.JSON{} + purchaseRequirementsResponse := []aid.JSON{} + + for _, item := range f.Grants { + itemGrantResponse = append(itemGrantResponse, aid.JSON{ + "templateId": item.Type.BackendValue + ":" + item.ID, + "quantity": 1, + }) + + purchaseRequirementsResponse = append(purchaseRequirementsResponse, aid.JSON{ + "requirementType": "DenyOnItemOwnership", + "requiredId": item.Type.BackendValue + ":" + item.ID, + "minQuantity": 1, + }) + } + + return aid.JSON{ + "devName": uuid.New().String(), + "offerId": f.ID, + "offerType": "StaticPrice", + "prices": []aid.JSON{{ + "currencyType": "MtxCurrency", + "currencySubType": "", + "regularPrice": f.TotalPrice, + "dynamicRegularPrice": f.TotalPrice, + "finalPrice": f.TotalPrice, + "basePrice": f.TotalPrice, + "saleExpiration": "9999-12-31T23:59:59.999Z", + }}, + "itemGrants": itemGrantResponse, + "meta": aid.JSON{ + "TileSize": f.Meta.TileSize, + "SectionId": f.Meta.SectionId, + "NewDisplayAssetPath": f.Meta.NewDisplayAssetPath, + "DisplayAssetPath": f.Meta.DisplayAssetPath, + }, + "metaInfo": []aid.JSON{ + { + "Key": "TileSize", + "Value": f.Meta.TileSize, + }, + { + "Key": "SectionId", + "Value": f.Meta.SectionId, + }, + { + "Key": "NewDisplayAssetPath", + "Value": f.Meta.NewDisplayAssetPath, + }, + { + "Key": "DisplayAssetPath", + "Value": f.Meta.DisplayAssetPath, + }, + }, + "giftInfo": aid.JSON{ + "bIsEnabled": f.Giftable, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": purchaseRequirementsResponse, + "giftRecordIds": []string{}, + }, + "purchaseRequirements": purchaseRequirementsResponse, + "categories": []string{f.Meta.Category}, + "title": f.Frontend.Title, + "description": f.Frontend.Description, + "shortDescription": f.Frontend.ShortDescription, + "displayAssetPath": f.Meta.DisplayAssetPath, + "appStoreId": []string{}, + "fufillmentIds": []string{}, + "dailyLimit": -1, + "weeklyLimit": -1, + "monthlyLimit": -1, + "sortPriority": 0, + "catalogGroupPriority": 0, + "filterWeight": 0, + "refundable": true, + } +} + +type FortniteCatalogSection struct { + Name string + Offers []*FortniteCatalogSectionOffer +} + +func NewFortniteCatalogSection(name string) *FortniteCatalogSection { + return &FortniteCatalogSection{ + Name: name, + } +} + +func (f *FortniteCatalogSection) GenerateFortniteCatalogSection() aid.JSON { + catalogEntiresResponse := []aid.JSON{} + for _, offer := range f.Offers { + catalogEntiresResponse = append(catalogEntiresResponse, offer.GenerateFortniteCatalogSectionOffer()) + } + + return aid.JSON{ + "name": f.Name, + "catalogEntries": catalogEntiresResponse, + } +} + +func (f *FortniteCatalogSection) GetGroupedOffers() map[string][]*FortniteCatalogSectionOffer { + groupedOffers := map[string][]*FortniteCatalogSectionOffer{} + + for _, offer := range f.Offers { + if groupedOffers[offer.Meta.Category] == nil { + groupedOffers[offer.Meta.Category] = []*FortniteCatalogSectionOffer{} + } + + groupedOffers[offer.Meta.Category] = append(groupedOffers[offer.Meta.Category], offer) + } + + return groupedOffers +} + +type FortniteCatalog struct { + Sections []*FortniteCatalogSection +} + +func NewFortniteCatalog() *FortniteCatalog { + return &FortniteCatalog{} +} + +func (f *FortniteCatalog) GenerateFortniteCatalog() aid.JSON { + catalogSectionsResponse := []aid.JSON{} + for _, section := range f.Sections { + catalogSectionsResponse = append(catalogSectionsResponse, section.GenerateFortniteCatalogSection()) + } + + return aid.JSON{ + "storefronts": catalogSectionsResponse, + "refreshIntervalHrs": 24, + "dailyPurchaseHrs": 24, + "expiration": "9999-12-31T23:59:59.999Z", + } +} + +func NewRandomFortniteCatalog() *FortniteCatalog { + aid.SetRandom(rand.New(rand.NewSource(int64(aid.Config.Fortnite.ShopSeed) + aid.CurrentDayUnix()))) + catalog := NewFortniteCatalog() + + daily := NewFortniteCatalogSection("BRDailyStorefront") + for len(daily.Offers) < dailyItems(aid.Config.Fortnite.Season) { + entry := newEntryFromFortniteItem(GetRandomItemWithDisplayAssetOfNotType("AthenaCharacter"), false) + entry.Meta.SectionId = "Daily" + daily.Offers = append(daily.Offers, entry) + } + catalog.Sections = append(catalog.Sections, daily) + + weekly := NewFortniteCatalogSection("BRWeeklyStorefront") + for len(weekly.GetGroupedOffers()) < weeklySets(aid.Config.Fortnite.Season) { + set := GetRandomSet() + for _, item := range set.Items { + if item.DisplayAssetPath == "" || item.DisplayAssetPath2 == "" { + continue + } + + entry := newEntryFromFortniteItem(item, true) + entry.Meta.Category = set.BackendName + entry.Meta.SectionId = "Featured" + weekly.Offers = append(weekly.Offers, entry) + } + } + catalog.Sections = append(catalog.Sections, weekly) + + return catalog +} + +func newEntryFromFortniteItem(fortniteItem *FortniteItem, addAssets bool) *FortniteCatalogSectionOffer { + displayAsset := regexp.MustCompile(`[^/]+$`).FindString(fortniteItem.DisplayAssetPath) + + entry := NewFortniteCatalogSectionOffer() + entry.Meta.TileSize = "Small" + if fortniteItem.Type.BackendValue == "AthenaCharacter" { + entry.Meta.TileSize = "Normal" + } + if addAssets { + entry.Meta.NewDisplayAssetPath = "/Game/Catalog/NewDisplayAssets/" + fortniteItem.DisplayAssetPath2 + "." + fortniteItem.DisplayAssetPath2 + if displayAsset != "" { + entry.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/" + displayAsset + "." + displayAsset + } + } + entry.Meta.ProfileId = "athena" + entry.Giftable = true + entry.Grants = append(entry.Grants, fortniteItem) + entry.GenerateTotalPrice() + entry.GenerateID() + + return entry +} + +func GetOfferByOfferId(id string) *FortniteCatalogSectionOffer { + catalog := NewRandomFortniteCatalog() + + for _, section := range catalog.Sections { + for _, offer := range section.Offers { + if offer.ID == id { + return offer } } } return nil -} - -type Storefront struct { - Name string `json:"name"` - CatalogEntries []Entry `json:"catalogEntries"` -} - -func NewStorefront(name string) *Storefront { - return &Storefront{ - Name: name, - CatalogEntries: []Entry{}, - } -} - -func (s *Storefront) Add(entry Entry) { - s.CatalogEntries = append(s.CatalogEntries, entry) -} - -func (s *Storefront) GenerateResponse(p *person.Person) aid.JSON { - json := aid.JSON{ - "name": s.Name, - "catalogEntries": []aid.JSON{}, - } - - for _, entry := range s.CatalogEntries { - json["catalogEntries"] = append(json["catalogEntries"].([]aid.JSON), entry.GenerateResponse(p)) - } - - return json -} - -type Entry struct { - ID string - Name string - Price int - Meta []aid.JSON - Panel string - Priority int - Grants []string - DisplayAssetPath string - NewDisplayAssetPath string - Title string - ShortDescription string - ProfileType string -} - -func NewCatalogEntry(profile string, meta ...aid.JSON) *Entry { - return &Entry{ - ID: uuid.New().String(), - Meta: meta, - ProfileType: profile, - } -} - -func (e *Entry) AddGrant(templateId string) *Entry { - e.Grants = append(e.Grants, templateId) - return e -} - -func (e *Entry) AddMeta(key string, value interface{}) *Entry { - e.Meta = append(e.Meta, aid.JSON{ - "Key": key, - "Value": value, - }) - return e -} - -func (e *Entry) SetTileSize(size string) *Entry { - e.Meta = append(e.Meta, aid.JSON{ - "Key": "TileSize", - "Value": size, - }) - return e -} - -func (e *Entry) SetPanel(panel string) *Entry { - e.Panel = panel - return e -} - -func (e *Entry) SetSection(sectionId string) *Entry { - for _, m := range e.Meta { - if m["Key"] == "SectionId" { - m["Value"] = sectionId - return e - } - } - - e.Meta = append(e.Meta, aid.JSON{ - "Key": "SectionId", - "Value": sectionId, - }) - return e -} - -func (e *Entry) SetDisplayAsset(asset string) *Entry { - displayAsset := "DAv2_Featured_" + asset - e.DisplayAssetPath = "/Game/Catalog/DisplayAssets/" + displayAsset + "." + displayAsset - return e -} - -func (e *Entry) SetNewDisplayAsset(asset string) *Entry { - e.NewDisplayAssetPath = "/Game/Catalog/NewDisplayAssets/" + asset + "." + asset - return e -} - -func (e *Entry) SetDisplayAssetPath(path string) *Entry { - paths := strings.Split(path, "/") - id := paths[len(paths)-1] - - e.DisplayAssetPath = "/Game/Catalog/DisplayAssets/" + id + "." + id - return e -} - -func (e *Entry) SetNewDisplayAssetPath(path string) *Entry { - e.NewDisplayAssetPath = path - 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 - for _, grant := range grantStrings { - e.Name += grant + "-" - } - - if e.NewDisplayAssetPath == "" && len(e.Grants) != 0 { - safeTemplateId := strings.ReplaceAll(strings.Split(e.Grants[0], ":")[1], "Athena_Commando_", "") - newDisplayAsset := "DAv2_" + safeTemplateId - e.NewDisplayAssetPath = "/Game/Catalog/NewDisplayAssets/" + newDisplayAsset + "." + newDisplayAsset - } - e.AddMeta("NewDisplayAssetPath", e.NewDisplayAssetPath) - - if e.DisplayAssetPath == "" && len(e.Grants) != 0 { - displayAsset := "DA_Featured_" + strings.Split(e.Grants[0], ":")[1] - e.DisplayAssetPath = "/Game/Catalog/DisplayAssets/" + displayAsset + "." + displayAsset - } - e.AddMeta("DisplayAssetPath", e.DisplayAssetPath) - - json := aid.JSON{ - "offerId": e.ID, - "devName": e.Name, - "offerType": "StaticPrice", - "prices": []aid.JSON{ - { - "currencyType": "MtxCurrency", - "currencySubType": "Currency", - "regularPrice": e.Price, - "dynamicRegularPrice": e.Price, - "finalPrice": e.Price, - "basePrice": e.Price, - "saleExpiration": aid.TimeEndOfDay(), - }, - }, - "categories": []string{}, - "catalogGroupPriority": 0, - "sortPriority": e.Priority, - "dailyLimit": -1, - "weeklyLimit": -1, - "monthlyLimit": -1, - "fufillmentIds": []string{}, - "filterWeight": 0.0, - "appStoreId": []string{}, - "refundable": false, - "itemGrants": []aid.JSON{}, - "metaInfo": e.Meta, - "meta": aid.JSON{}, - "title": e.Title, - "displayAssetPath": e.DisplayAssetPath, - "shortDescription": e.ShortDescription, - } - grants := []aid.JSON{} - requirements := []aid.JSON{} - purchaseRequirements := []aid.JSON{} - meta := []aid.JSON{} - - for _, templateId := range e.Grants { - grants = append(grants, aid.JSON{ - "templateId": templateId, - "quantity": 1, - }) - - if item := p.AthenaProfile.Items.GetItemByTemplateID(templateId); item != nil { - requirements = append(requirements, aid.JSON{ - "requirementType": "DenyOnItemOwnership", - "requiredId": item.ID, - "minQuantity": 1, - }) - - purchaseRequirements = append(purchaseRequirements, aid.JSON{ - "requirementType": "DenyOnItemOwnership", - "requiredId": item.ID, - "minQuantity": 1, - }) - } - } - - for _, m := range e.Meta { - meta = append(meta, m) - json["meta"].(aid.JSON)[m["Key"].(string)] = m["Value"] - } - - if e.Panel != "" { - json["categories"] = []string{e.Panel} - } - - json["itemGrants"] = grants - json["requirements"] = requirements - json["metaInfo"] = meta - json["giftInfo"] = aid.JSON{ - "bIsEnabled": true, - "forcedGiftBoxTemplateId": "", - "purchaseRequirements": purchaseRequirements, - "giftRecordIds": []any{}, - } - - - return json -} - -func GenerateRandomStorefront() { - storefront := NewCatalog() - - daily := NewStorefront("BRDailyStorefront") - weekly := NewStorefront("BRWeeklyStorefront") - - for i := 0; i < 4; i++ { - if aid.Config.Fortnite.Season < 14 { - break - } - - item := RandomItemByType("AthenaCharacter") - entry := NewCatalogEntry("athena") - entry.SetSection("Daily") - entry.SetNewDisplayAsset(item.DisplayAssetPath2) - - if item.DisplayAssetPath != "" { - entry.SetDisplayAssetPath(item.DisplayAssetPath) - } - entry.SetPrice(GetPriceForRarity(item.Rarity.BackendValue, item.Type.BackendValue)) - entry.AddGrant(item.Type.BackendValue + ":" + item.ID) - entry.SetTileSize("Normal") - entry.Priority = 1 - - if item.Backpack != nil { - entry.AddGrant("AthenaBackpack:" + item.Backpack.ID) - } - - if storefront.CheckIfOfferIsDuplicate(*entry) { - continue - } - - daily.Add(*entry) - } - - for i := 0; i < 6; i++ { - item := RandomItemByNotType("AthenaCharacter") - entry := NewCatalogEntry("athena") - entry.SetSection("Daily") - - if item.DisplayAssetPath2 == "" { - i-- - continue - } - entry.SetNewDisplayAsset(item.DisplayAssetPath2) - - if item.DisplayAssetPath != "" { - entry.SetDisplayAssetPath(item.DisplayAssetPath) - } - entry.SetPrice(GetPriceForRarity(item.Rarity.BackendValue, item.Type.BackendValue)) - entry.AddGrant(item.Type.BackendValue + ":" + item.ID) - entry.SetTileSize("Small") - - if storefront.CheckIfOfferIsDuplicate(*entry) { - continue - } - - daily.Add(*entry) - } - - minimumItems := 8 - if aid.Config.Fortnite.Season < 11 { - minimumItems = 3 - } - - minimumSets := 4 - if aid.Config.Fortnite.Season <+ 12 { - minimumSets = 3 - } - - setsAdded := 0 - for len(weekly.CatalogEntries) < minimumItems || setsAdded < minimumSets { - set := RandomSet() - - itemsAdded := 0 - itemsToAdd := []*Entry{} - for _, item := range set.Items { - entry := NewCatalogEntry("athena") - entry.SetSection("Featured") - entry.SetPanel(set.BackendName) - entry.SetNewDisplayAsset(item.DisplayAssetPath2) - - if item.Type.BackendValue == "AthenaCharacter" { - entry.SetTileSize("Normal") - if aid.Config.Fortnite.Season < 14 { - itemsAdded += 1 - } else { - itemsAdded += 2 - } - entry.Priority = 1 - } else { - entry.SetTileSize("Small") - itemsAdded += 1 - } - - if item.DisplayAssetPath != "" { - entry.SetDisplayAssetPath(item.DisplayAssetPath) - } - entry.SetPrice(GetPriceForRarity(item.Rarity.BackendValue, item.Type.BackendValue)) - entry.AddGrant(item.Type.BackendValue + ":" + item.ID) - - itemsToAdd = append(itemsToAdd, entry) - } - - if itemsAdded % 2 != 0 { - itemsToAdd = itemsToAdd[:len(itemsToAdd)-1] - } - - for _, entry := range itemsToAdd { - if storefront.CheckIfOfferIsDuplicate(*entry) { - continue - } - - weekly.Add(*entry) - } - - setsAdded++ - } - - storefront.Add(daily) - storefront.Add(weekly) - - StaticCatalog = storefront - aid.Print("(snow) generated random storefront") } \ No newline at end of file diff --git a/handlers/client.go b/handlers/client.go index e892dfa..1fd7b9d 100644 --- a/handlers/client.go +++ b/handlers/client.go @@ -601,12 +601,12 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p return fmt.Errorf("invalid Body") } - offer := fortnite.StaticCatalog.GetOfferById(body.OfferID) + offer := fortnite.GetOfferByOfferId(body.OfferID) if offer == nil { return fmt.Errorf("offer not found") } - if offer.Price != body.ExpectedTotalPrice { + if offer.TotalPrice != body.ExpectedTotalPrice { return fmt.Errorf("invalid price") } @@ -629,7 +629,7 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p vbucks.Save() profile0Vbucks.Save() - if offer.ProfileType != "athena" { + if offer.Meta.ProfileId != "athena" { return fmt.Errorf("save the world not implemeted yet") } @@ -637,15 +637,16 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p purchase := p.NewPurchase(body.OfferID, body.ExpectedTotalPrice) for i := 0; i < body.PurchaseQuantity; i++ { for _, grant := range offer.Grants { - if profile.Items.GetItemByTemplateID(grant) != nil { - item := profile.Items.GetItemByTemplateID(grant) + templateId := grant.Type.BackendValue + ":" + grant.ID + if profile.Items.GetItemByTemplateID(templateId) != nil { + item := profile.Items.GetItemByTemplateID(templateId) item.Quantity++ go item.Save() continue } - item := p.NewItem(grant, 1) + item := p.NewItem(templateId, 1) person.AthenaProfile.Items.AddItem(item) purchase.AddLoot(item) @@ -653,7 +654,7 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p "itemType": item.TemplateID, "itemGuid": item.ID, "quantity": item.Quantity, - "itemProfile": offer.ProfileType, + "itemProfile": offer.Meta.ProfileId, }) } } @@ -746,12 +747,12 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro return fmt.Errorf("invalid Body") } - offer := fortnite.StaticCatalog.GetOfferById(body.OfferId) + offer := fortnite.GetOfferByOfferId(body.OfferId) if offer == nil { return fmt.Errorf("offer not found") } - if offer.Price != body.ExpectedTotalPrice { + if offer.TotalPrice != body.ExpectedTotalPrice { return fmt.Errorf("invalid price") } @@ -762,13 +763,13 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro } for _, grant := range offer.Grants { - if receiverPerson.AthenaProfile.Items.GetItemByTemplateID(grant) != nil { + if receiverPerson.AthenaProfile.Items.GetItemByTemplateID(grant.Type.BackendValue + ":" + grant.ID) != nil { return fmt.Errorf("one or more receivers has one of the items") } } } - price := offer.Price * len(body.ReceiverAccountIds) + price := offer.TotalPrice * len(body.ReceiverAccountIds) vbucks := profile.Items.GetItemByTemplateID("Currency:MtxPurchased") if vbucks == nil { @@ -793,8 +794,8 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro receiverPerson := p.Find(receiverAccountId) gift := p.NewGift(body.GiftWrapTemplateId, 1, person.ID, body.PersonalMessage) for _, grant := range offer.Grants { - item := p.NewItem(grant, 1) - item.ProfileType = offer.ProfileType + item := p.NewItem(grant.Type.BackendValue + ":" + grant.ID, 1) + item.ProfileType = offer.Meta.ProfileId gift.AddLoot(item) } diff --git a/handlers/snow.go b/handlers/snow.go index 8dcf62b..fbc87c0 100644 --- a/handlers/snow.go +++ b/handlers/snow.go @@ -72,5 +72,6 @@ func GetSnowParties(c *fiber.Ctx) error { } func GetSnowShop(c *fiber.Ctx) error { - return c.JSON(fortnite.StaticCatalog) + shop := fortnite.NewRandomFortniteCatalog() + return c.JSON(shop.GenerateFortniteCatalog()) } \ No newline at end of file diff --git a/handlers/socket.go b/handlers/socket.go index e16a750..5c595f7 100644 --- a/handlers/socket.go +++ b/handlers/socket.go @@ -21,7 +21,7 @@ func MiddlewareWebsocket(c *fiber.Ctx) error { protocol = "matchmaking" } - c.Locals("identifier", "ws-"+aid.RandomString(8)) + c.Locals("identifier", "ws-"+aid.RandomString(18)) c.Locals("protocol", protocol) return c.Next() diff --git a/handlers/storefront.go b/handlers/storefront.go index f5df828..244fb3c 100644 --- a/handlers/storefront.go +++ b/handlers/storefront.go @@ -5,15 +5,13 @@ import ( "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) - - return c.Status(fiber.StatusOK).JSON(fortnite.StaticCatalog.GenerateFortniteCatalog(person)) + shop := fortnite.NewRandomFortniteCatalog() + return c.Status(200).JSON(shop.GenerateFortniteCatalog()) } func GetStorefrontKeychain(c *fiber.Ctx) error { @@ -23,5 +21,5 @@ func GetStorefrontKeychain(c *fiber.Ctx) error { return c.Status(fiber.StatusInternalServerError).JSON(aid.JSON{"error":err.Error()}) } - return c.Status(fiber.StatusOK).JSON(keychain) + return c.Status(200).JSON(keychain) } \ No newline at end of file diff --git a/handlers/tcp.go b/handlers/tcp.go index 03560f2..df1a22a 100644 --- a/handlers/tcp.go +++ b/handlers/tcp.go @@ -105,7 +105,7 @@ func (t *tcpServer) handle(conn net.Conn) { c: &conn, buffer: make([]byte, 1024), } - tcpClient.jabber = socket.NewJabberSocket(tcpClient, "tcp-"+aid.RandomString(8), socket.JabberData{}) + tcpClient.jabber = socket.NewJabberSocket(tcpClient, "tcp-"+aid.RandomString(18), socket.JabberData{}) socket.JabberSockets.Set(tcpClient.jabber.ID, tcpClient.jabber) tcpClient.loop() diff --git a/main.go b/main.go index f558590..6e7390d 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,6 @@ var configFile []byte func init() { aid.LoadConfig(configFile) - var device storage.Storage switch aid.Config.Database.Type { case "postgres": @@ -46,7 +45,7 @@ func init() { func init() { discord.IntialiseClient() fortnite.PreloadCosmetics() - fortnite.GenerateRandomStorefront() + fortnite.NewRandomFortniteCatalog() for _, username := range aid.Config.Accounts.Gods { found := person.FindByDisplay(username) @@ -192,8 +191,8 @@ func main() { r.All("*", func(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(aid.ErrorNotFound) }) if aid.Config.Fortnite.Season <= 2 { - // t := handlers.NewServer() - // go t.Listen() + t := handlers.NewServer() + go t.Listen() } err := r.Listen("0.0.0.0" + aid.Config.API.Port) diff --git a/readme.md b/readme.md index 50b039d..7e8fd59 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,6 @@ ## What's up next? -- Seeded randomization for the **Item Shop** instead of a random number generator. This will ensure that even if the backend is restarted, the same random items will be in the shop during that day. - Purchasing the **Battle Pass**. This will require the Battle Pass Storefront ID for every build. I am yet to think of a solution for this. - Interaction with a Game Server to handle **Event Tracking** for player statistics and challenges. This will be a very large task as a new specialised game server will need to be created. - After the game server addition, a **Matchmaking System** will be added to match players together for a game. It will use a bin packing algorithm to ensure that games are filled as much as possible. @@ -27,10 +26,10 @@ And once battle royale is completed ... - **XMPP** For interacting with friends, parties and gifting. - **Friends** On every build, this will allow for adding, removing and blocking friends. - **Party System V2** This replaces the legacy xmpp driven party system. +- **Automatic Item Shop** Will automatically update the item shop for the day, for all builds. - **Gifting** Of any item shop entry to any friend. - **Locker Loadouts** On seasons 12 onwards, this allows for the saving and loading of multiple locker presets. - **Item Refunding** Of previous shop purchases, will use a refund ticket if refunded in time. -- **Universal Item Shop** Works on all builds and will be updated every 24 hours. - **Client Settings Storage** Uses amazon buckets to store client settings. - **Support A Creator 5%** Use any display name and each purchase will give them 5% of the vbucks spent. - **Discord Bot** Very useful to control players, their inventory and their settings diff --git a/socket/jabber.go b/socket/jabber.go index 13840dc..946c39a 100644 --- a/socket/jabber.go +++ b/socket/jabber.go @@ -3,7 +3,6 @@ package socket import ( "fmt" "reflect" - "strings" "github.com/beevik/etree" "github.com/ectrc/snow/aid" @@ -26,11 +25,8 @@ var jabberHandlers = map[string]func(*Socket[JabberData], *etree.Document) error } func HandleNewJabberSocket(identifier string) { - aid.Print("new jabber handle: " + identifier) - socket, ok := JabberSockets.Get(identifier) if !ok { - aid.Print("socket not found", identifier) return } defer JabberSockets.Delete(socket.ID) @@ -38,7 +34,6 @@ func HandleNewJabberSocket(identifier string) { for { _, message, failed := socket.Connection.ReadMessage() if failed != nil { - aid.Print("jabber message failed", failed) break } @@ -47,14 +42,8 @@ func HandleNewJabberSocket(identifier string) { } func JabberSocketOnMessage(socket *Socket[JabberData], message []byte) { - if strings.Contains(string(message), `">`) { - message = []byte(strings.ReplaceAll(string(message), `">`, `"/>`)) - } - - aid.Print("jabber message", string(message)) parsed := etree.NewDocument() if err := parsed.ReadFromString(string(message)); err != nil { - aid.Print("jabber message failed to parse", err) return } @@ -63,11 +52,8 @@ func JabberSocketOnMessage(socket *Socket[JabberData], message []byte) { socket.Connection.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error())) return } - return } - - aid.Print("jabber message handled") } func jabberStreamHandler(socket *Socket[JabberData], parsed *etree.Document) error {