diff --git a/fortnite/person.go b/fortnite/person.go index 9cf45f6..eaed263 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -28,7 +28,9 @@ func NewFortnitePerson(displayName string, key string) *p.Person { person.AccessKey = key for _, item := range defaultAthenaItems { - person.AthenaProfile.Items.AddItem(p.NewItem(item, 1)) + item := p.NewItem(item, 1) + item.HasSeen = true + person.AthenaProfile.Items.AddItem(item) } for _, item := range defaultCommonCoreItems { @@ -51,10 +53,8 @@ func NewFortnitePerson(displayName string, key string) *p.Person { } if item == "Currency:MtxPurchased" { - item := p.NewItem(item, 0) - item.HasSeen = true - person.CommonCoreProfile.Items.AddItem(item).Save() - person.Profile0Profile.Items.AddItem(item).Save() + person.CommonCoreProfile.Items.AddItem(p.NewItem(item, 9999)).Save() + person.Profile0Profile.Items.AddItem(p.NewItem(item, 9999)).Save() continue } diff --git a/fortnite/shop.go b/fortnite/shop.go index af5fda1..97c7ca6 100644 --- a/fortnite/shop.go +++ b/fortnite/shop.go @@ -58,7 +58,7 @@ func (c *Catalog) GenerateFortniteCatalog(p *person.Person) aid.JSON { return json } -func (c *Catalog) IsDuplicate(entry Entry) bool { +func (c *Catalog) CheckIfOfferIsDuplicate(entry Entry) bool { for _, storefront := range c.Storefronts { for _, catalogEntry := range storefront.CatalogEntries { if catalogEntry.Grants[0] == entry.Grants[0] { @@ -70,6 +70,18 @@ func (c *Catalog) IsDuplicate(entry Entry) bool { return false } +func (c *Catalog) GetOfferById(id string) *Entry { + for _, storefront := range c.Storefronts { + for _, catalogEntry := range storefront.CatalogEntries { + if catalogEntry.ID == id { + return &catalogEntry + } + } + } + + return nil +} + type Storefront struct { Name string `json:"name"` CatalogEntries []Entry `json:"catalogEntries"` @@ -111,12 +123,14 @@ type Entry struct { NewDisplayAssetPath string Title string ShortDescription string + ProfileType string } -func NewCatalogEntry(meta ...aid.JSON) *Entry { +func NewCatalogEntry(profile string, meta ...aid.JSON) *Entry { return &Entry{ ID: uuid.New().String(), Meta: meta, + ProfileType: profile, } } @@ -305,7 +319,7 @@ func GenerateStorefront() { } item := Cosmetics.GetRandomItemByType("AthenaCharacter") - entry := NewCatalogEntry() + entry := NewCatalogEntry("athena") entry.SetSection("Daily") if !entry.IsNewDisplayAssetValid(item.ID) { @@ -322,7 +336,11 @@ func GenerateStorefront() { entry.SetTileSize("Normal") entry.Priority = 1 - if storefront.IsDuplicate(*entry) { + if item.Backpack != "" { + entry.AddGrant("AthenaBackpack:" + item.Backpack) + } + + if storefront.CheckIfOfferIsDuplicate(*entry) { continue } @@ -331,7 +349,7 @@ func GenerateStorefront() { for i := 0; i < 6; i++ { item := Cosmetics.GetRandomItemByNotType("AthenaCharacter") - entry := NewCatalogEntry() + entry := NewCatalogEntry("athena") entry.SetSection("Daily") if !entry.IsNewDisplayAssetValid(item.ID) { @@ -347,7 +365,7 @@ func GenerateStorefront() { entry.AddGrant(item.Type.BackendValue + ":" + item.ID) entry.SetTileSize("Small") - if storefront.IsDuplicate(*entry) { + if storefront.CheckIfOfferIsDuplicate(*entry) { continue } @@ -366,7 +384,7 @@ func GenerateStorefront() { itemsAdded := 0 itemsToAdd := []*Entry{} for _, item := range set.Items { - entry := NewCatalogEntry() + entry := NewCatalogEntry("athena") entry.SetSection("Featured") entry.SetPanel(set.BackendName) @@ -402,7 +420,7 @@ func GenerateStorefront() { } for _, entry := range itemsToAdd { - if storefront.IsDuplicate(*entry) { + if storefront.CheckIfOfferIsDuplicate(*entry) { continue } @@ -416,4 +434,5 @@ func GenerateStorefront() { storefront.Add(weekly) StaticCatalog = storefront + aid.Print("Generated new random storefront") } \ No newline at end of file diff --git a/handlers/profile.go b/handlers/profile.go index cb21389..9af3c0b 100644 --- a/handlers/profile.go +++ b/handlers/profile.go @@ -7,13 +7,14 @@ import ( "time" "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/fortnite" p "github.com/ectrc/snow/person" "github.com/gofiber/fiber/v2" ) var ( - profileActions = map[string]func(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { + profileActions = map[string]func(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { "QueryProfile": PostQueryProfileAction, "ClientQuestLogin": PostQueryProfileAction, "MarkItemSeen": PostMarkItemSeenAction, @@ -22,6 +23,7 @@ var ( "SetBattleRoyaleBanner": PostSetBattleRoyaleBannerAction, "SetCosmeticLockerSlot": PostSetCosmeticLockerSlotAction, "SetCosmeticLockerBanner": PostSetCosmeticLockerBannerAction, + "PurchaseCatalogEntry": PostPurchaseCatalogEntryAction, } ) @@ -32,27 +34,74 @@ func PostProfileAction(c *fiber.Ctx) error { } profile := person.GetProfileFromType(c.Query("profileId")) - if action, ok := profileActions[c.Params("action")]; ok && profile != nil { - defer profile.ClearProfileChanges() - before := profile.Snapshot() - if err := action(c, person, profile); err != nil { + if profile == nil { + return c.Status(404).JSON(aid.ErrorBadRequest("No Profile Found")) + } + defer profile.ClearProfileChanges() + + profileSnapshots := map[string]*p.ProfileSnapshot{ + "athena": nil, + "common_core": nil, + "common_public": nil, + } + + for key := range profileSnapshots { + profileSnapshots[key] = person.GetProfileFromType(key).Snapshot() + } + + notifications := []aid.JSON{} + + action, ok := profileActions[c.Params("action")]; + if ok && profile != nil { + if err := action(c, person, profile, ¬ifications); err != nil { return c.Status(400).JSON(aid.ErrorBadRequest(err.Error())) } - profile.Diff(before) + } + + for key, profileSnapshot := range profileSnapshots { + profile := person.GetProfileFromType(key) + if profile == nil { + continue + } + + if profileSnapshot == nil { + continue + } + + profile.Diff(profileSnapshot) } revision, _ := strconv.Atoi(c.Query("rvn")) - if revision == -1 && profile == nil { - revision = 1 - } - if revision == -1 && profile != nil { + if revision == -1 { revision = profile.Revision } revision++ + profile.Revision = revision + profile.Save() + delete(profileSnapshots, profile.Type) - changes := []interface{}{} - if profile != nil { - changes = profile.Changes + multiUpdate := []aid.JSON{} + for key := range profileSnapshots { + profile := person.GetProfileFromType(key) + if profile == nil { + continue + } + profile.Revision++ + + if len(profile.Changes) == 0 { + continue + } + + multiUpdate = append(multiUpdate, aid.JSON{ + "profileId": profile.Type, + "profileRevision": profile.Revision, + "profileCommandRevision": profile.Revision, + "profileChangesBaseRevision": profile.Revision - 1, + "profileChanges": profile.Changes, + }) + + profile.ClearProfileChanges() + profile.Save() } return c.Status(200).JSON(aid.JSON{ @@ -60,20 +109,20 @@ func PostProfileAction(c *fiber.Ctx) error { "profileRevision": revision, "profileCommandRevision": revision, "profileChangesBaseRevision": revision - 1, - "profileChanges": changes, - "multiUpdate": []aid.JSON{}, - "notifications": []aid.JSON{}, + "profileChanges": profile.Changes, + "multiUpdate": multiUpdate, + "notifications": notifications, "responseVersion": 1, "serverTime": time.Now().Format("2006-01-02T15:04:05.999Z"), }) } -func PostQueryProfileAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostQueryProfileAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { profile.CreateFullProfileUpdateChange() return nil } -func PostMarkItemSeenAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostMarkItemSeenAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { ItemIds []string `json:"itemIds"` } @@ -96,7 +145,7 @@ func PostMarkItemSeenAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) return nil } -func PostEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { SlotName string `json:"slotName" binding:"required"` ItemToSlot string `json:"itemToSlot"` @@ -141,7 +190,7 @@ func PostEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, pr return nil } -func PostSetBattleRoyaleBannerAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostSetBattleRoyaleBannerAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { HomebaseBannerColorID string `json:"homebaseBannerColorId" binding:"required"` HomebaseBannerIconID string `json:"homebaseBannerIconId" binding:"required"` @@ -182,7 +231,7 @@ func PostSetBattleRoyaleBannerAction(c *fiber.Ctx, person *p.Person, profile *p. return nil } -func PostSetItemFavoriteStatusBatchAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostSetItemFavoriteStatusBatchAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { ItemIds []string `json:"itemIds" binding:"required"` Favorite []bool `json:"itemFavStatus" binding:"required"` @@ -206,7 +255,7 @@ func PostSetItemFavoriteStatusBatchAction(c *fiber.Ctx, person *p.Person, profil return nil } -func PostSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { Category string `json:"category" binding:"required"` // item type e.g. Character ItemToSlot string `json:"itemToSlot" binding:"required"` // template id @@ -275,7 +324,7 @@ func PostSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile *p. return nil } -func PostSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile *p.Profile) error { +func PostSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { LockerItem string `json:"lockerItem" binding:"required"` // locker id BannerColorTemplateName string `json:"bannerColorTemplateName" binding:"required"` // template id @@ -294,7 +343,6 @@ func PostSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile * icon := profile.Items.GetItemByTemplateID("HomebaseBannerIcon:" + body.BannerIconTemplateName) if icon == nil { - // return fmt.Errorf("icon item not found") icon = &p.Item{ ID: body.BannerIconTemplateName, } @@ -309,5 +357,82 @@ func PostSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile * currentLocker.BannerID = icon.ID go currentLocker.Save() + return nil +} + +func PostPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct{ + OfferID string `json:"offerId" binding:"required"` + PurchaseQuantity int `json:"purchaseQuantity" binding:"required"` + ExpectedTotalPrice int `json:"expectedTotalPrice" binding:"required"` + } + + err := c.BodyParser(&body) + if err != nil { + return fmt.Errorf("invalid Body") + } + + offer := fortnite.StaticCatalog.GetOfferById(body.OfferID) + if offer == nil { + return fmt.Errorf("offer not found") + } + + if offer.Price != body.ExpectedTotalPrice { + return fmt.Errorf("invalid price") + } + + vbucks := profile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if vbucks == nil { + return fmt.Errorf("vbucks not found") + } + + profile0Vbucks := person.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if profile0Vbucks == nil { + return fmt.Errorf("profile0vbucks not found") + } + + if vbucks.Quantity < body.ExpectedTotalPrice { + return fmt.Errorf("not enough vbucks") + } + + vbucks.Quantity -= body.ExpectedTotalPrice + + go func() { + profile0Vbucks.Quantity = vbucks.Quantity // for season 2 and lower + vbucks.Save() + profile0Vbucks.Save() + }() + + if offer.ProfileType != "athena" { + return fmt.Errorf("save the world not implemeted yet!") + } + + loot := []aid.JSON{} + for i := 0; i < body.PurchaseQuantity; i++ { + for _, grant := range offer.Grants { + if profile.Items.GetItemByTemplateID(grant) != nil { + continue + } + + item := p.NewItem(grant, 1) + person.AthenaProfile.Items.AddItem(item) + + loot = append(loot, aid.JSON{ + "itemType": item.TemplateID, + "itemGuid": item.ID, + "quantity": item.Quantity, + "itemProfile": offer.ProfileType, + }) + } + } + + *notifications = append(*notifications, aid.JSON{ + "type": "CatalogPurchase", + "lootResult": aid.JSON{ + "items": loot, + }, + "primary": true, + }) + return nil } \ No newline at end of file diff --git a/person/person.go b/person/person.go index d6db938..cf7496a 100644 --- a/person/person.go +++ b/person/person.go @@ -13,6 +13,7 @@ type Person struct { CommonCoreProfile *Profile CommonPublicProfile *Profile Profile0Profile *Profile + CollectionsProfile *Profile } type Option struct { @@ -29,6 +30,7 @@ func NewPerson() *Person { CommonCoreProfile: NewProfile("common_core"), CommonPublicProfile: NewProfile("common_public"), Profile0Profile: NewProfile("profile0"), + CollectionsProfile: NewProfile("collections"), } } @@ -73,6 +75,7 @@ func findHelper(databasePerson *storage.DB_Person) *Person { commonCoreProfile := NewProfile("common_core") commonPublicProfile := NewProfile("common_public") profile0 := NewProfile("profile0") + collectionsProfile := NewProfile("collections") for _, profile := range databasePerson.Profiles { if profile.Type == "athena" { @@ -94,6 +97,11 @@ func findHelper(databasePerson *storage.DB_Person) *Person { profile0.ID = profile.ID profile0 = FromDatabaseProfile(&profile) } + + if profile.Type == "collections" { + collectionsProfile.ID = profile.ID + collectionsProfile = FromDatabaseProfile(&profile) + } } person := &Person{ @@ -104,6 +112,7 @@ func findHelper(databasePerson *storage.DB_Person) *Person { CommonCoreProfile: commonCoreProfile, CommonPublicProfile: commonPublicProfile, Profile0Profile: profile0, + CollectionsProfile: collectionsProfile, } cache.SavePerson(person) @@ -144,6 +153,8 @@ func (p *Person) GetProfileFromType(profileType string) *Profile { return p.CommonPublicProfile case "profile0": return p.Profile0Profile + case "collections": + return p.CollectionsProfile } return nil @@ -167,6 +178,7 @@ func (p *Person) ToDatabase() *storage.DB_Person { "athena": p.AthenaProfile, "common_public": p.CommonPublicProfile, "profile0": p.Profile0Profile, + "collections": p.CollectionsProfile, } for profileType, profile := range profilesToConvert { @@ -234,5 +246,6 @@ func (p *Person) Snapshot() *PersonSnapshot { CommonCoreProfile: *p.CommonCoreProfile.Snapshot(), CommonPublicProfile: *p.CommonPublicProfile.Snapshot(), Profile0Profile: *p.Profile0Profile.Snapshot(), + CollectionsProfile: *p.CollectionsProfile.Snapshot(), } } \ No newline at end of file diff --git a/person/profile.go b/person/profile.go index cd39a54..324444c 100644 --- a/person/profile.go +++ b/person/profile.go @@ -32,9 +32,9 @@ func NewProfile(profile string) *Profile { Quests: NewQuestMutex(&storage.DB_Profile{ID: id, Type: profile}), Attributes: NewAttributeMutex(&storage.DB_Profile{ID: id, Type: profile}), Loadouts: NewLoadoutMutex(&storage.DB_Profile{ID: id, Type: profile}), - Type: profile, + Type: profile, Revision: 0, - Changes: []interface{}{}, + Changes: []interface{}{}, } } @@ -129,7 +129,7 @@ func (p *Profile) GenerateFortniteProfileEntry() aid.JSON { } func (p *Profile) Save() { - // storage.Repo.SaveProfile(p.ToDatabase()) + storage.Repo.SaveProfile(p.ToDatabase()) } func (p *Profile) Snapshot() *ProfileSnapshot { @@ -398,4 +398,45 @@ func (p *Profile) CreateFullProfileUpdateChange() { func (p *Profile) ClearProfileChanges() { p.Changes = []interface{}{} +} + +func (p *Profile) ToDatabase() *storage.DB_Profile { + dbProfile := storage.DB_Profile{ + ID: p.ID, + PersonID: p.PersonID, + Type: p.Type, + Items: []storage.DB_Item{}, + Gifts: []storage.DB_Gift{}, + Quests: []storage.DB_Quest{}, + Loadouts: []storage.DB_Loadout{}, + Attributes: []storage.DB_PAttribute{}, + Revision: p.Revision, + } + + // p.Items.RangeItems(func(id string, item *Item) bool { + // dbProfile.Items = append(dbProfile.Items, *item.ToDatabase(dbProfile.PersonID)) + // return true + // }) + + p.Gifts.RangeGifts(func(id string, gift *Gift) bool { + dbProfile.Gifts = append(dbProfile.Gifts, *gift.ToDatabase(dbProfile.PersonID)) + return true + }) + + p.Quests.RangeQuests(func(id string, quest *Quest) bool { + dbProfile.Quests = append(dbProfile.Quests, *quest.ToDatabase(dbProfile.PersonID)) + return true + }) + + p.Attributes.RangeAttributes(func(key string, value *Attribute) bool { + dbProfile.Attributes = append(dbProfile.Attributes, *value.ToDatabase(dbProfile.PersonID)) + return true + }) + + p.Loadouts.RangeLoadouts(func(id string, loadout *Loadout) bool { + dbProfile.Loadouts = append(dbProfile.Loadouts, *loadout.ToDatabase(dbProfile.PersonID)) + return true + }) + + return &dbProfile } \ No newline at end of file diff --git a/person/snapshot.go b/person/snapshot.go index 577cdfb..8ce29fc 100644 --- a/person/snapshot.go +++ b/person/snapshot.go @@ -7,6 +7,7 @@ type PersonSnapshot struct { CommonCoreProfile ProfileSnapshot CommonPublicProfile ProfileSnapshot Profile0Profile ProfileSnapshot + CollectionsProfile ProfileSnapshot } type ProfileSnapshot struct {