diff --git a/fortnite/interact.go b/fortnite/interact.go index b6f3870..5588d65 100644 --- a/fortnite/interact.go +++ b/fortnite/interact.go @@ -2,9 +2,11 @@ package fortnite import ( "encoding/json" + "fmt" "io" "math/rand" "net/http" + "slices" "strings" "github.com/ectrc/snow/aid" @@ -26,6 +28,18 @@ type FAPI_Error struct { Error string `json:"error"` } +type FAPI_Cosmetic_Variant struct { + Channel string `json:"channel"` + Type string `json:"type"` + Options []FAPI_Cosmetic_VariantChannel `json:"options"` +} + +type FAPI_Cosmetic_VariantChannel struct { + Tag string `json:"tag"` + Name string `json:"name"` + Image string `json:"image"` +} + type FAPI_Cosmetic struct { ID string `json:"id"` Name string `json:"name"` @@ -62,15 +76,7 @@ type FAPI_Cosmetic struct { 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"` + Variants []FAPI_Cosmetic_Variant `json:"variants"` GameplayTags []string `json:"gameplayTags"` SearchTags []string `json:"searchTags"` MetaTags []string `json:"metaTags"` @@ -165,7 +171,13 @@ func (c *CosmeticData) GetRandomSet() Set { return c.GetRandomSet() } +var EXTRA_NUMERIC_STYLES = []string{"Soccer", "Football", "ScaryBall"} + func (c *CosmeticData) AddItem(item FAPI_Cosmetic) { + if slices.Contains(EXTRA_NUMERIC_STYLES, item.Set.BackendValue) { + item = c.AddNumericVariantChannelToItem(item) + } + c.Items[item.ID] = item if item.Set.BackendValue != "" { @@ -181,6 +193,23 @@ func (c *CosmeticData) AddItem(item FAPI_Cosmetic) { } } +func (c *CosmeticData) AddNumericVariantChannelToItem(item FAPI_Cosmetic) FAPI_Cosmetic { + owned := []FAPI_Cosmetic_VariantChannel{} + for i := 0; i < 100; i++ { + owned = append(owned, FAPI_Cosmetic_VariantChannel{ + Tag: fmt.Sprint(i), + }) + } + + item.Variants = append(item.Variants, FAPI_Cosmetic_Variant{ + Channel: "Numeric", + Type: "int", + Options: owned, + }) + + return item +} + var ( StaticAPI = NewFortniteAPI() Cosmetics = CosmeticData{ diff --git a/fortnite/person.go b/fortnite/person.go index 2ee86b6..8390f34 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -41,11 +41,24 @@ func GiveEverything(person *p.Person) { continue } - item := p.NewItem(item.Type.BackendValue + ":" + item.ID, 1) - item.HasSeen = true - person.AthenaProfile.Items.AddItem(item) + new := p.NewItem(item.Type.BackendValue + ":" + item.ID, 1) + new.HasSeen = true - items = append(items, *item.ToDatabase(person.AthenaProfile.ID)) + grouped := map[string][]string{} + for _, variant := range item.Variants { + grouped[variant.Channel] = []string{} + + for _, option := range variant.Options { + grouped[variant.Channel] = append(grouped[variant.Channel], option.Tag) + } + } + + for channel, tags := range grouped { + new.AddChannel(new.NewChannel(channel, tags, tags[0])) + } + + person.AthenaProfile.Items.AddItem(new) + items = append(items, *new.ToDatabase(person.AthenaProfile.ID)) } storage.Repo.BulkCreateItems(&items) diff --git a/handlers/auth.go b/handlers/auth.go index cf5c5f5..b8409c4 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -93,6 +93,7 @@ func PostTokenExchangeCode(c *fiber.Ctx, body *FortniteTokenBody) error { access, err := aid.JWTSign(aid.JSON{ "snow_id": person.ID, // custom "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + "am": "exchange_code", }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) @@ -101,6 +102,7 @@ func PostTokenExchangeCode(c *fiber.Ctx, body *FortniteTokenBody) error { refresh, err := aid.JWTSign(aid.JSON{ "snow_id": person.ID, "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + "am": "exchange_code", }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) @@ -143,6 +145,7 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { access, err := aid.JWTSign(aid.JSON{ "snow_id": person.ID, // custom "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + "am": "password", }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) @@ -151,6 +154,7 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { refresh, err := aid.JWTSign(aid.JSON{ "snow_id": person.ID, "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), + "am": "password", }) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) diff --git a/handlers/client.go b/handlers/client.go index 4bcb56a..c8bd480 100644 --- a/handlers/client.go +++ b/handlers/client.go @@ -153,6 +153,10 @@ func clientEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, SlotName string `json:"slotName" binding:"required"` ItemToSlot string `json:"itemToSlot"` IndexWithinSlot int `json:"indexWithinSlot"` + VariantUpdates []struct{ + Active string `json:"active"` + Channel string `json:"channel"` + } `json:"variantUpdates"` } if err := c.BodyParser(&body); err != nil { @@ -170,6 +174,16 @@ func clientEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, } } + for _, update := range body.VariantUpdates { + channel := item.GetChannel(update.Channel) + if channel == nil { + continue + } + + channel.Active = update.Active + go channel.Save() + } + attr := profile.Attributes.GetAttributeByKey("favorite_" + strings.ReplaceAll(strings.ToLower(body.SlotName), "wrap", "wraps")) if attr == nil { return fmt.Errorf("attribute not found") @@ -191,8 +205,8 @@ func clientEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, default: attr.ValueJSON = aid.JSONStringify(item.ID) } - go attr.Save() + go attr.Save() return nil } @@ -263,7 +277,10 @@ func clientSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile * ItemToSlot string `json:"itemToSlot" binding:"required"` // template id LockerItem string `json:"lockerItem" binding:"required"` // locker id SlotIndex int `json:"slotIndex" binding:"required"` // index of slot - VariantUpdates []aid.JSON `json:"variantUpdates" binding:"required"` // variant updates + VariantUpdates []struct{ + Active string `json:"active"` + Channel string `json:"channel"` + } `json:"variantUpdates" binding:"required"` // variant updates } if err := c.BodyParser(&body); err != nil { @@ -286,6 +303,16 @@ func clientSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile * return fmt.Errorf("current locker not found") } + for _, update := range body.VariantUpdates { + channel := item.GetChannel(update.Channel) + if channel == nil { + continue + } + + channel.Active = update.Active + go channel.Save() + } + switch body.Category { case "Character": currentLocker.CharacterID = item.ID @@ -321,8 +348,7 @@ func clientSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile * currentLocker.MusicPackID = item.ID } - go currentLocker.Save() - + go currentLocker.Save() return nil } @@ -434,8 +460,6 @@ func clientCopyCosmeticLoadoutAction(c *fiber.Ctx, person *p.Person, profile *p. } if body.TargetIndex >= len(loadouts) { - aid.Print("creating a new loadout with source", body.SourceIndex, "and target", body.TargetIndex) - newLoadout := p.NewLoadout(body.OptNewNameForTarget, profile) newLoadout.CopyFrom(lastAppliedLoadout) profile.Loadouts.AddLoadout(newLoadout) @@ -464,8 +488,6 @@ func clientCopyCosmeticLoadoutAction(c *fiber.Ctx, person *p.Person, profile *p. } if body.SourceIndex > 0 { - aid.Print("saving source loadout", body.SourceIndex, "to sandbox") - sourceLoadout := profile.Loadouts.GetLoadout(loadouts[body.SourceIndex]) if sourceLoadout == nil { return fmt.Errorf("target loadout not found") @@ -488,8 +510,6 @@ func clientCopyCosmeticLoadoutAction(c *fiber.Ctx, person *p.Person, profile *p. return nil } - aid.Print("loading target loadout", body.TargetIndex, "to sandbox") - targetLoadout := profile.Loadouts.GetLoadout(loadouts[body.TargetIndex]) if targetLoadout == nil { return fmt.Errorf("target loadout not found") diff --git a/main.go b/main.go index 37137a7..b5df41d 100644 --- a/main.go +++ b/main.go @@ -55,7 +55,7 @@ func init() { found.RemovePermission(perm) } - found.AddPermission("all") + found.AddPermission("asdasdasdasa") aid.Print("(snow) max account " + username + " loaded") } } diff --git a/person/item.go b/person/item.go index 5986ccb..0d5177b 100644 --- a/person/item.go +++ b/person/item.go @@ -44,6 +44,7 @@ func FromDatabaseItem(item *storage.DB_Item) *Item { variants := []*VariantChannel{} for _, variant := range item.Variants { + variants = append(variants, FromDatabaseVariant(&variant)) } @@ -126,6 +127,7 @@ func (i *Item) DeleteLoot() { func (i *Item) NewChannel(channel string, owned []string, active string) *VariantChannel { return &VariantChannel{ + ID: uuid.New().String(), ItemID: i.ID, Channel: channel, Owned: owned, @@ -139,12 +141,14 @@ func (i *Item) AddChannel(channel *VariantChannel) { } func (i *Item) RemoveChannel(channel *VariantChannel) { + var vId string for index, c := range i.Variants { if c.Channel == channel.Channel { + vId = c.ID i.Variants = append(i.Variants[:index], i.Variants[index+1:]...) } } - //storage.Repo.DeleteItemVariant(i.ID, channel) + storage.Repo.DeleteVariant(vId) } func (i *Item) GetChannel(channel string) *VariantChannel { @@ -164,6 +168,14 @@ func (i *Item) FillChannels(channels []*VariantChannel) { } } +func (i *Item) EquipChannel(channel string, variant string) { + for _, c := range i.Variants { + if c.Channel == channel { + c.Active = variant + } + } +} + func (i *Item) ToDatabase(profileId string) *storage.DB_Item { variants := []storage.DB_VariantChannel{} @@ -251,5 +263,5 @@ func (v *VariantChannel) ToDatabase() *storage.DB_VariantChannel { } func (v *VariantChannel) Save() { - //storage.Repo.SaveItemVariant(v.ToDatabase()) + storage.Repo.SaveVariant(v.ToDatabase()) } \ No newline at end of file diff --git a/person/loadout.go b/person/loadout.go index 74a9908..588bdee 100644 --- a/person/loadout.go +++ b/person/loadout.go @@ -1,6 +1,7 @@ package person import ( + "regexp" "strings" "github.com/ectrc/snow/aid" @@ -167,7 +168,6 @@ func (l *Loadout) GetAttribute(attribute string) interface{} { return l.GenerateFortniteLockerSlotsData() } - return nil } @@ -187,6 +187,43 @@ func (l *Loadout) GenerateFortniteLockerSlotsData() aid.JSON { } } +func (l *Loadout) GetSlotFromItemTemplateID(templateId string) string { + re := regexp.MustCompile(`Athena(.*):`) + match := re.FindStringSubmatch(templateId) + + if len(match) > 1 { + return match[1] + } + + return "" +} + +func (l *Loadout) GetItemFromSlot(slot string) *Item { + person := Find(l.PersonID) + if person == nil { + return nil + } + + switch slot { + case "Character": + return person.AthenaProfile.Items.GetItem(l.CharacterID) + case "Backpack": + return person.AthenaProfile.Items.GetItem(l.BackpackID) + case "Pickaxe": + return person.AthenaProfile.Items.GetItem(l.PickaxeID) + case "Glider": + return person.AthenaProfile.Items.GetItem(l.GliderID) + case "SkyDiveContrail": + return person.AthenaProfile.Items.GetItem(l.ContrailID) + case "LoadingScreen": + return person.AthenaProfile.Items.GetItem(l.LoadingScreenID) + case "MusicPack": + return person.AthenaProfile.Items.GetItem(l.MusicPackID) + } + + return nil +} + func (l *Loadout) GetItemSlotData(itemId string) aid.JSON { json := aid.JSON{ "items": []string{}, diff --git a/person/person.go b/person/person.go index 10b2591..d98926f 100644 --- a/person/person.go +++ b/person/person.go @@ -253,19 +253,24 @@ func (p *Person) Save() { storage.Repo.SavePerson(dbPerson) } +func (p *Person) SaveShallow() { + dbPerson := p.ToDatabaseShallow() + storage.Repo.SavePerson(dbPerson) +} + func (p *Person) Ban() { p.IsBanned = true - p.Save() + p.SaveShallow() } func (p *Person) Unban() { p.IsBanned = false - p.Save() + p.SaveShallow() } func (p *Person) AddPermission(permission string) { p.Permissions = append(p.Permissions, permission) - p.Save() + p.SaveShallow() } func (p *Person) RemovePermission(permission string) { @@ -275,7 +280,7 @@ func (p *Person) RemovePermission(permission string) { break } } - p.Save() + p.SaveShallow() } func (p *Person) HasPermission(permission Permission) bool { @@ -360,6 +365,24 @@ func (p *Person) ToDatabase() *storage.DB_Person { return &dbPerson } +func (p *Person) ToDatabaseShallow() *storage.DB_Person { + dbPerson := storage.DB_Person{ + ID: p.ID, + DisplayName: p.DisplayName, + Permissions: p.Permissions, + IsBanned: p.IsBanned, + Profiles: []storage.DB_Profile{}, + Stats: []storage.DB_SeasonStat{}, + Discord: storage.DB_DiscordPerson{}, + } + + if p.Discord != nil { + dbPerson.Discord = *p.Discord + } + + return &dbPerson +} + func (p *Person) AddAttribute(value *Attribute) { p.AthenaProfile.Attributes.AddAttribute(value) } diff --git a/person/profile.go b/person/profile.go index 324444c..c0b4fe1 100644 --- a/person/profile.go +++ b/person/profile.go @@ -183,6 +183,8 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { return nil } + loadout := p.GetActiveLoadout() + for _, change := range changes { switch change.Path[0] { case "Items": @@ -199,7 +201,14 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { } if change.Type == "update" && change.Path[2] != "Quantity" { - p.CreateItemAttributeChangedChange(p.Items.GetItem(change.Path[1]), change.Path[2]) + item := p.Items.GetItem(change.Path[1]) + p.CreateItemAttributeChangedChange(item, change.Path[2]) + + slotType := loadout.GetSlotFromItemTemplateID(item.TemplateID) + slotValue := loadout.GetItemFromSlot(slotType) + if slotValue != nil && slotValue.ID == item.ID { + p.CreateLoadoutChangedChange(loadout, slotType + "ID") + } } case "Quests": if change.Type == "create" && change.Path[2] == "ID" { @@ -223,7 +232,12 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { } if change.Type == "update" && change.Path[2] == "ValueJSON" { - p.CreateStatModifiedChange(p.Attributes.GetAttribute(change.Path[1])) + attribute := p.Attributes.GetAttribute(change.Path[1]) + p.CreateStatModifiedChange(attribute) + + if attribute.Key == "last_applied_loadout" { + p.CreateLoadoutChangedChange(p.GetActiveLoadout(), "CharacterID") + } } case "Loadouts": if change.Type == "create" && change.Path[2] == "ID" { @@ -243,6 +257,20 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { return changes } +func (p *Profile) GetActiveLoadout() *Loadout { + lastAppliedLoadoutAttribute := p.Attributes.GetAttributeByKey("last_applied_loadout") + if lastAppliedLoadoutAttribute == nil { + return nil + } + + lastAppliedLoadout := p.Loadouts.GetLoadout(AttributeConvert[string](lastAppliedLoadoutAttribute)) + if lastAppliedLoadout == nil { + return nil + } + + return lastAppliedLoadout +} + func (p *Profile) CreateAttribute(key string, value interface{}) *Attribute { p.Attributes.AddAttribute(NewAttribute(key, value)) return p.Attributes.GetAttribute(key) diff --git a/storage/postgres.go b/storage/postgres.go index 583dca1..029d77a 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -58,11 +58,11 @@ func (s *PostgresStorage) GetPerson(personId string) *DB_Person { Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). - // Preload("Profiles.Items.Variants"). - Preload("Profiles.Gifts.Loot"). Preload("Profiles.Attributes"). Preload("Profiles.Items"). + Preload("Profiles.Items.Variants"). Preload("Profiles.Gifts"). + Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). Preload("Discord"). Where("id = ?", personId). @@ -81,11 +81,11 @@ func (s *PostgresStorage) GetPersonByDisplay(displayName string) *DB_Person { Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). - // Preload("Profiles.Items.Variants"). - Preload("Profiles.Gifts.Loot"). Preload("Profiles.Attributes"). Preload("Profiles.Items"). + Preload("Profiles.Items.Variants"). Preload("Profiles.Gifts"). + Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). Preload("Discord"). Where("display_name = ?", displayName). @@ -104,11 +104,11 @@ func (s *PostgresStorage) GetPersonsByPartialDisplay(displayName string) []*DB_P Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). - // Preload("Profiles.Items.Variants"). - Preload("Profiles.Gifts.Loot"). Preload("Profiles.Attributes"). Preload("Profiles.Items"). + Preload("Profiles.Items.Variants"). Preload("Profiles.Gifts"). + Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). Preload("Discord"). Where("display_name LIKE ?", "%" + displayName + "%"). @@ -139,11 +139,11 @@ func (s *PostgresStorage) GetAllPersons() []*DB_Person { Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). - // Preload("Profiles.Items.Variants"). - Preload("Profiles.Gifts.Loot"). Preload("Profiles.Attributes"). Preload("Profiles.Items"). + Preload("Profiles.Items.Variants"). Preload("Profiles.Gifts"). + Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). Preload("Discord"). Find(&dbPersons) @@ -172,11 +172,11 @@ func (s *PostgresStorage) DeletePerson(personId string) { Model(&DB_Person{}). Preload("Profiles"). Preload("Profiles.Loadouts"). - // Preload("Profiles.Items.Variants"). - Preload("Profiles.Gifts.Loot"). Preload("Profiles.Attributes"). Preload("Profiles.Items"). + Preload("Profiles.Items.Variants"). Preload("Profiles.Gifts"). + Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). Preload("Discord"). Delete(&DB_Person{}, "id = ?", personId) @@ -226,6 +226,10 @@ func (s *PostgresStorage) SaveVariant(variant *DB_VariantChannel) { s.Postgres.Save(variant) } +func (s *PostgresStorage) BulkCreateVariants(variants *[]DB_VariantChannel) { + s.Postgres.Create(variants) +} + func (s *PostgresStorage) DeleteVariant(variantId string) { s.Postgres.Delete(&DB_VariantChannel{}, "id = ?", variantId) } diff --git a/storage/storage.go b/storage/storage.go index caf1364..e416d2d 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -31,6 +31,7 @@ type Storage interface { DeleteItem(itemId string) SaveVariant(variant *DB_VariantChannel) + BulkCreateVariants(variants *[]DB_VariantChannel) DeleteVariant(variantId string) SaveQuest(quest *DB_Quest) @@ -154,6 +155,10 @@ func (r *Repository) DeleteItem(itemId string) { r.Storage.DeleteItem(itemId) } +func (r *Repository) BulkCreateVariants(variants *[]DB_VariantChannel) { + r.Storage.BulkCreateVariants(variants) +} + func (r *Repository) SaveVariant(variant *DB_VariantChannel) { r.Storage.SaveVariant(variant) }