diff --git a/aid/aid.go b/aid/aid.go index b21196b..bfa1018 100644 --- a/aid/aid.go +++ b/aid/aid.go @@ -1,6 +1,7 @@ package aid import ( + "fmt" "os" "os/signal" "regexp" @@ -26,6 +27,13 @@ func FormatNumber(number int) string { return ReverseString(str) } +func FormatPrice(number int) string { + last := number % 100 + number /= 100 + str := fmt.Sprintf("%d.%02d", number, last) + return str +} + func ReverseString(input string) string { str := "" for _, char := range input { @@ -53,9 +61,23 @@ func Regex(str, regex string) *string { return nil } +// if condition is true, return a, else return b func Ternary[T any](condition bool, a, b T) T { if condition { return a } return b +} + +func ToInt(str string) int { + i, _ := strconv.Atoi(str) + return i +} + +func Flatten[T any](arr [][]T) []T { + var flat []T + for _, a := range arr { + flat = append(flat, a...) + } + return flat } \ No newline at end of file diff --git a/aid/config.go b/aid/config.go index 2579c39..10d4dbf 100644 --- a/aid/config.go +++ b/aid/config.go @@ -22,6 +22,7 @@ type CS struct { Secret string Token string Guild string + CallbackURL string } Amazon struct { Enabled bool @@ -38,6 +39,10 @@ type CS struct { Port string FrontendPort string Debug bool + XMPP struct { + Host string + Port string + } } JWT struct { Secret string @@ -106,6 +111,11 @@ func LoadConfig(file []byte) { panic("Discord Guild ID is empty") } + Config.Discord.CallbackURL = cfg.Section("api").Key("discord_url").String() + if Config.Discord.CallbackURL == "" { + panic("Discord Callback URL is empty") + } + Config.Amazon.Enabled = true Config.Amazon.BucketURI = cfg.Section("amazon").Key("uri").String() if Config.Amazon.BucketURI == "" { @@ -144,6 +154,16 @@ func LoadConfig(file []byte) { Config.API.Debug = cfg.Section("api").Key("debug").MustBool(false) + Config.API.XMPP.Host = cfg.Section("api").Key("xmpp_host").String() + if Config.API.XMPP.Host == "" { + panic("API XMPP Host is empty") + } + + Config.API.XMPP.Port = cfg.Section("api").Key("xmpp_port").String() + if Config.API.XMPP.Port == "" { + panic("API XMPP Port is empty") + } + Config.JWT.Secret = cfg.Section("jwt").Key("secret").String() if Config.JWT.Secret == "" { panic("JWT Secret is empty") diff --git a/aid/fiber.go b/aid/fiber.go index 1eb1211..0c0bc4c 100644 --- a/aid/fiber.go +++ b/aid/fiber.go @@ -1,6 +1,7 @@ package aid import ( + "slices" "time" "github.com/gofiber/fiber/v2" @@ -13,7 +14,21 @@ func FiberLogger() fiber.Handler { return logger.New(logger.Config{ Format: "(${method}) (${status}) (${latency}) ${path}\n", Next: func(c *fiber.Ctx) bool { - return c.Response().StatusCode() == 302 + if (slices.Contains[[]int]( + []int{302, 101}, + c.Response().StatusCode(), + )) { + return true + } + + if (slices.Contains[[]string]( + []string{"/snow/log", "/purchase/assets/", " /favicon.ico"}, + c.Path(), + )) { + return true + } + + return false }, }) } @@ -42,4 +57,5 @@ func FiberGetQueries(c *fiber.Ctx, queryKeys ...string) map[string][]string { } } return argsMaps -} \ No newline at end of file +} + diff --git a/aid/json.go b/aid/json.go index 681fffc..886ee10 100644 --- a/aid/json.go +++ b/aid/json.go @@ -29,4 +29,10 @@ func JSONParse(input string) interface{} { var output interface{} json.Unmarshal([]byte(input), &output) return output +} + +func JSONParseG[T interface{}](input string) T { + var output T + json.Unmarshal([]byte(input), &output) + return output } \ No newline at end of file diff --git a/default.config.ini b/default.config.ini index 6a25562..4e19673 100644 --- a/default.config.ini +++ b/default.config.ini @@ -45,17 +45,29 @@ guild="1234567890..." level="info" [api] +; this will enable some routes to show information about the backend +; this is useful for debugging +; this should be disabled in production +debug=true ; port to listen on port=":3000" ; host that the api is running on ; e.g. if you are running the api on your local machine, you would set this to 127.0.0.1 ; if you are running the api on a server, you would set this to the ip of the server or the domain name -; localhost will not work with the xmpp from testing host="127.0.0.1" -; this will enable some routes to show information about the backend -; this is useful for debugging -; this should be disabled in production -debug=true +; host that the xmpp on the fortnite client will try and connect to +; if you are running the api on your local machine, you would set this to the same as the host +; if you are running the api on a server, you would set this to the ip of the server or the domain name +; localhost will not work with the xmpp from testing +xmpp_host="127.0.0.1" +; port that the xmpp on the fortnite client will try and connect to +; if you are running the api on your local machine, you would set this to the same as the port +; if you are running the api on a server, you would set this to the port that you are running the api on +xmpp_port=":3000" +; this this is the beginning of the url that the discord bot will use to send messages to the api +; this includes the protocol and the host + port +; for a public api, this could be "https://snows.rocks" for example +discord_url="http://127.0.0.1:3000" [jwt] ; secret for jwt signing diff --git a/discord/admin.go b/discord/admin.go index 5e8ec76..c6b8942 100644 --- a/discord/admin.go +++ b/discord/admin.go @@ -32,10 +32,10 @@ func informationHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { Data: &discordgo.InteractionResponseData{ Embeds: []*discordgo.MessageEmbed{ NewEmbedBuilder(). - SetTitle("Information"). + SetTitle("Snow Information"). SetColor(0x2b2d31). AddField("Players Registered", aid.FormatNumber(playerCount), true). - AddField("Players Online", aid.FormatNumber(0), true). + AddField("Players Online", aid.FormatNumber(socket.JabberSockets.Len()), true). AddField("VBucks in Circulation", aid.FormatNumber(totalVbucks), false). Build(), }, @@ -287,15 +287,7 @@ func addItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker } snapshot := player.GetProfileFromType(profile).Snapshot() - foundItem := player.GetProfileFromType(profile).Items.GetItemByTemplateID(item) - switch (foundItem) { - case nil: - foundItem = person.NewItem(item, int(qty)) - player.GetProfileFromType(profile).Items.AddItem(foundItem) - default: - foundItem.Quantity += int(qty) - } - foundItem.Save() + fortnite.GrantToPerson(player, fortnite.NewItemGrant(item, int(qty))) player.GetProfileFromType(profile).Diff(snapshot) s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ diff --git a/discord/discord.go b/discord/discord.go index 1b40f8a..2c66d63 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -55,7 +55,7 @@ func IntialiseClient() { err := StaticClient.Client.Open() if err != nil { - panic(err) + aid.Print("(discord) failed to connect; will be disabled") } } diff --git a/fortnite/arena.go b/fortnite/arena.go new file mode 100644 index 0000000..3d34af8 --- /dev/null +++ b/fortnite/arena.go @@ -0,0 +1,416 @@ +package fortnite + +import ( + "time" + + "github.com/ectrc/snow/aid" +) + +type ArenaScoringRule struct { + StatName string + MatchRule string + RewardTiers []struct{ + Value int + Points int + Multiply bool + } +} + +func NewScoringRule(stat, rule string) *ArenaScoringRule { + return &ArenaScoringRule{ + StatName: stat, + MatchRule: rule, + RewardTiers: new(ArenaScoringRule).RewardTiers, + } +} + +func (sr *ArenaScoringRule) AddTier(value, points int, multiply bool) *ArenaScoringRule { + sr.RewardTiers = append(sr.RewardTiers, struct{ + Value int + Points int + Multiply bool + }{ + Value: value, + Points: points, + Multiply: multiply, + }) + + return sr +} + +func (sr *ArenaScoringRule) GenerateFortniteScoringRule() aid.JSON { + tiers := make([]aid.JSON, 0) + + for _, tier := range sr.RewardTiers { + tiers = append(tiers, aid.JSON{ + "keyValue": tier.Value, + "pointsEarned": tier.Points, + "multiplicative": tier.Multiply, + }) + } + + return aid.JSON{ + "trackedStat": sr.StatName, + "matchRule": sr.MatchRule, + "rewardTiers": tiers, + } +} + +type ArenaEventTemplate struct { + ID string + MatchLimit int + PlaylistID string + ScoringRules []*ArenaScoringRule +} + +func NewEventTemplate(id string, limit int) *ArenaEventTemplate { + return &ArenaEventTemplate{ + ID: id, + MatchLimit: limit, + ScoringRules: make([]*ArenaScoringRule, 0), + } +} + +func (et *ArenaEventTemplate) AddScoringRule(rule ...*ArenaScoringRule) { + et.ScoringRules = append(et.ScoringRules, rule...) +} + +func (et *ArenaEventTemplate) GenerateFortniteEventTemplate() aid.JSON { + rules := make([]aid.JSON, 0) + + for _, rule := range et.ScoringRules { + rules = append(rules, rule.GenerateFortniteScoringRule()) + } + + return aid.JSON{ + "gameId": "Fortnite", + "eventTemplateId": et.ID, + "playlistId": et.PlaylistID, + "persistentScoreId": "Hype", + "matchCap": et.MatchLimit, + "scoringRules": rules, + } +} + +type ArenaEventWindow struct { + ID string + ParentEvent *Event + Template *ArenaEventTemplate + Round int + ToBeDetermined bool + CanLiveSpectate bool + Meta struct { + DivisionRank int + ThresholdToAdvanceDivision int + } +} + +func NewEventWindow(id string, template *ArenaEventTemplate) *ArenaEventWindow { + return &ArenaEventWindow{ + ID: id, + Meta: new(ArenaEventWindow).Meta, + Template: template, + } +} + +func (ew *ArenaEventWindow) GenerateFortniteEventWindow() aid.JSON { + meta := aid.JSON{ + "divisionRank": ew.Meta.DivisionRank, + "ThresholdToAdvanceDivision": ew.Meta.ThresholdToAdvanceDivision, + "RoundType": "Arena", + } + + allTokens := []string{ + "ARENA_S8_Division1", + "ARENA_S8_Division2", + "ARENA_S8_Division3", + "ARENA_S8_Division4", + "ARENA_S8_Division5", + "ARENA_S8_Division6", + "ARENA_S8_Division7", + } + requireAll := []string{} + requireNone := []string{} + + for index, token := range allTokens { + if index == ew.Meta.DivisionRank { + requireAll = append(requireAll, token) + continue + } + + requireNone = append(requireNone, token) + } + + return aid.JSON{ + "eventWindowId": ew.ID, + "eventTemplateId": ew.Template.ID, + "countdownBeginTime": "2023-06-15T15:00:00.000Z", + "beginTime": time.Now().Add(time.Hour * -24).Format(time.RFC3339), + "endTime": "9999-12-31T23:59:59.000Z", + "payoutDelay": 30, + "round": ew.Round, + "isTBD": ew.ToBeDetermined, + "canLiveSpectate": ew.CanLiveSpectate, + "visibility": "public", + "scoreLocations": []aid.JSON{}, + "blackoutPeriods": []string{}, + "requireAnyTokens": []string{}, + "requireAllTokens": requireAll, + "requireAllTokensCaller": []string{}, + "requireNoneTokensCaller": requireNone, + "requireAnyTokensCaller": []string{}, + "additionalRequirements": []string{}, + "teammateEligibility": "any", + "metadata": meta, + } +} + +type Event struct { + ID string + DisplayID string + Windows []*ArenaEventWindow +} + +func NewEvent(id string, displayId string) *Event { + return &Event{ + ID: id, + DisplayID: displayId, + Windows: make([]*ArenaEventWindow, 0), + } +} + +func (e *Event) AddWindow(window *ArenaEventWindow) { + window.ParentEvent = e + e.Windows = append(e.Windows, window) +} + +func (e *Event) GenerateFortniteEvent() aid.JSON { + eventWindows := make([]aid.JSON, 0) + + for _, window := range e.Windows { + eventWindows = append(eventWindows, window.GenerateFortniteEventWindow()) + } + + return aid.JSON{ + "gameId": "Fortnite", + "eventId": e.ID, + "eventGroup": "", + "regions": []string{ "NAE", "ME", "NAW", "OCE", "ASIA", "EU", "BR", }, + "regionMappings": aid.JSON{}, + "platforms": []string{ "PS4", "XboxOne", "Switch", "Android", "IOS", "Windows", }, + "platformMappings": aid.JSON{}, + "displayDataId": e.DisplayID, + "eventWindows": eventWindows, + "appId": nil, + "link": nil, + "metadata": aid.JSON{ + "minimumAccountLevel": 1, + "TrackedStats": []string{ + "PLACEMENT_STAT_INDEX", + "TEAM_ELIMS_STAT_INDEX", + "MATCH_PLAYED_STAT", + }, + }, + "environment": nil, + "announcementTime": time.Now().Format(time.RFC3339), + "beginTime": time.Now().Add(time.Hour * -24).Format(time.RFC3339), + "endTime": "9999-12-31T23:59:59.000Z", + } +} + +var ( + ArenaEvents = make([]*Event, 0) +) + +func PreloadEvents() { + if aid.Config.Fortnite.Season < 8 { + return + } + + ArenaEvents = []*Event{ + createDuoEvent(), + createSoloEvent(), + } +} + +func createSoloEvent() *Event { + ArenaSolo := NewEvent("epicgames_Arena_S8_Solo", "SnowArenaSolo") + + defaultPlacement := NewScoringRule("PLACEMENT_STAT_INDEX", "lte") + defaultPlacement.AddTier(1, 3, false) + defaultPlacement.AddTier(5, 2, false) + defaultPlacement.AddTier(15, 2, false) + defaultPlacement.AddTier(25, 3, false) + defaultEliminations := NewScoringRule("TEAM_ELIMS_STAT_INDEX", "gte") + defaultEliminations.AddTier(1, 1, true) + + soloOpen1T := NewEventTemplate("eventTemplate_Arena_S8_Division1_Solo", 100) + soloOpen1T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloOpen1T.AddScoringRule(defaultPlacement, defaultEliminations) + soloOpen1W := NewEventWindow("Arena_S8_Division1_Solo", soloOpen1T) + soloOpen1W.ToBeDetermined = false + soloOpen1W.CanLiveSpectate = false + soloOpen1W.Round = 0 + soloOpen1W.Meta.DivisionRank = 0 + soloOpen1W.Meta.ThresholdToAdvanceDivision = 25 + ArenaSolo.AddWindow(soloOpen1W) + + soloOpen2T := NewEventTemplate("eventTemplate_Arena_S8_Division2_Solo", 100) + soloOpen2T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloOpen2T.AddScoringRule(defaultPlacement, defaultEliminations) + soloOpen2W := NewEventWindow("Arena_S8_Division2_Solo", soloOpen2T) + soloOpen2W.ToBeDetermined = false + soloOpen2W.CanLiveSpectate = false + soloOpen2W.Round = 1 + soloOpen2W.Meta.DivisionRank = 1 + soloOpen2W.Meta.ThresholdToAdvanceDivision = 75 + ArenaSolo.AddWindow(soloOpen2W) + + soloOpen3T := NewEventTemplate("eventTemplate_Arena_S8_Division3_Solo", 100) + soloOpen3T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloOpen3T.AddScoringRule(defaultPlacement, defaultEliminations) + soloOpen3W := NewEventWindow("Arena_S8_Division3_Solo", soloOpen3T) + soloOpen3W.Round = 2 + soloOpen3W.ToBeDetermined = false + soloOpen3W.CanLiveSpectate = false + soloOpen3W.Meta.DivisionRank = 2 + soloOpen3W.Meta.ThresholdToAdvanceDivision = 125 + ArenaSolo.AddWindow(soloOpen3W) + + soloContender4T := NewEventTemplate("eventTemplate_Arena_S8_Division4_Solo", 100) + soloContender4T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloContender4T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -2, false), defaultEliminations) + soloConteder4W := NewEventWindow("Arena_S8_Division4_Solo", soloContender4T) + soloConteder4W.Round = 3 + soloConteder4W.ToBeDetermined = false + soloConteder4W.CanLiveSpectate = false + soloConteder4W.Meta.DivisionRank = 3 + soloConteder4W.Meta.ThresholdToAdvanceDivision = 175 + ArenaSolo.AddWindow(soloConteder4W) + + soloContender5T := NewEventTemplate("eventTemplate_Arena_S8_Division5_Solo", 100) + soloContender5T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloContender5T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -4, false), defaultEliminations) + soloConteder5W := NewEventWindow("Arena_S8_Division5_Solo", soloContender5T) + soloConteder5W.Round = 4 + soloConteder5W.ToBeDetermined = false + soloConteder5W.CanLiveSpectate = false + soloConteder5W.Meta.DivisionRank = 4 + soloConteder5W.Meta.ThresholdToAdvanceDivision = 225 + ArenaSolo.AddWindow(soloConteder5W) + + soloContender6T := NewEventTemplate("eventTemplate_Arena_S8_Division6_Solo", 100) + soloContender6T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloContender6T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -6, false), defaultEliminations) + soloConteder6W := NewEventWindow("Arena_S8_Division6_Solo", soloContender6T) + soloConteder6W.Round = 5 + soloConteder6W.ToBeDetermined = false + soloConteder6W.CanLiveSpectate = false + soloConteder6W.Meta.DivisionRank = 5 + soloConteder6W.Meta.ThresholdToAdvanceDivision = 300 + ArenaSolo.AddWindow(soloConteder6W) + + soloChampions7T := NewEventTemplate("eventTemplate_Arena_S8_Division7_Solo", 100) + soloChampions7T.PlaylistID = "Playlist_ShowdownAlt_Solo" + soloChampions7T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -8, false), defaultEliminations) + soloChampions7W := NewEventWindow("Arena_S8_Division7_Solo", soloChampions7T) + soloChampions7W.Round = 6 + soloChampions7W.ToBeDetermined = true + soloChampions7W.CanLiveSpectate = false + soloChampions7W.Meta.DivisionRank = 6 + soloChampions7W.Meta.ThresholdToAdvanceDivision = 9999999999 + ArenaSolo.AddWindow(soloChampions7W) + + return ArenaSolo +} + +func createDuoEvent() *Event { + ArenaDuo := NewEvent("epicgames_Arena_S8_Duos", "SnowArenaDuos") + + defaultPlacement := NewScoringRule("PLACEMENT_STAT_INDEX", "lte") + defaultPlacement.AddTier(1, 3, false) + defaultPlacement.AddTier(3, 2, false) + defaultPlacement.AddTier(7, 2, false) + defaultPlacement.AddTier(12, 3, false) + defaultEliminations := NewScoringRule("TEAM_ELIMS_STAT_INDEX", "gte") + defaultEliminations.AddTier(1, 1, true) + + duoOpen1T := NewEventTemplate("eventTemplate_Arena_S8_Division1_Duos", 100) + duoOpen1T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoOpen1T.AddScoringRule(defaultPlacement, defaultEliminations) + duoOpen1W := NewEventWindow("Arena_S8_Division1_Duos", duoOpen1T) + duoOpen1W.ToBeDetermined = false + duoOpen1W.CanLiveSpectate = false + duoOpen1W.Round = 0 + duoOpen1W.Meta.DivisionRank = 0 + duoOpen1W.Meta.ThresholdToAdvanceDivision = 25 + ArenaDuo.AddWindow(duoOpen1W) + + duoOpen2T := NewEventTemplate("eventTemplate_Arena_S8_Division2_Duos", 100) + duoOpen2T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoOpen2T.AddScoringRule(defaultPlacement, defaultEliminations) + duoOpen2W := NewEventWindow("Arena_S8_Division2_Duos", duoOpen2T) + duoOpen2W.ToBeDetermined = false + duoOpen2W.CanLiveSpectate = false + duoOpen2W.Round = 1 + duoOpen2W.Meta.DivisionRank = 1 + duoOpen2W.Meta.ThresholdToAdvanceDivision = 75 + ArenaDuo.AddWindow(duoOpen2W) + + duoOpen3T := NewEventTemplate("eventTemplate_Arena_S8_Division3_Duos", 100) + duoOpen3T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoOpen3T.AddScoringRule(defaultPlacement, defaultEliminations) + duoOpen3W := NewEventWindow("Arena_S8_Division3_Duos", duoOpen3T) + duoOpen3W.Round = 2 + duoOpen3W.ToBeDetermined = false + duoOpen3W.CanLiveSpectate = false + duoOpen3W.Meta.DivisionRank = 2 + duoOpen3W.Meta.ThresholdToAdvanceDivision = 125 + ArenaDuo.AddWindow(duoOpen3W) + + duoContender4T := NewEventTemplate("eventTemplate_Arena_S8_Division4_Duos", 100) + duoContender4T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoContender4T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -2, false), defaultEliminations) + duoConteder4W := NewEventWindow("Arena_S8_Division4_Duos", duoContender4T) + duoConteder4W.Round = 3 + duoConteder4W.ToBeDetermined = false + duoConteder4W.CanLiveSpectate = false + duoConteder4W.Meta.DivisionRank = 3 + duoConteder4W.Meta.ThresholdToAdvanceDivision = 175 + ArenaDuo.AddWindow(duoConteder4W) + + duoContender5T := NewEventTemplate("eventTemplate_Arena_S8_Division5_Duos", 100) + duoContender5T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoContender5T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -4, false), defaultEliminations) + duoConteder5W := NewEventWindow("Arena_S8_Division5_Duos", duoContender5T) + duoConteder5W.Round = 4 + duoConteder5W.ToBeDetermined = false + duoConteder5W.CanLiveSpectate = false + duoConteder5W.Meta.DivisionRank = 4 + duoConteder5W.Meta.ThresholdToAdvanceDivision = 225 + ArenaDuo.AddWindow(duoConteder5W) + + duoContender6T := NewEventTemplate("eventTemplate_Arena_S8_Division6_Duos", 100) + duoContender6T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoContender6T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -6, false), defaultEliminations) + duoConteder6W := NewEventWindow("Arena_S8_Division6_Duos", duoContender6T) + duoConteder6W.Round = 5 + duoConteder6W.ToBeDetermined = false + duoConteder6W.CanLiveSpectate = false + duoConteder6W.Meta.DivisionRank = 5 + duoConteder6W.Meta.ThresholdToAdvanceDivision = 300 + ArenaDuo.AddWindow(duoConteder6W) + + duoChampions7T := NewEventTemplate("eventTemplate_Arena_S8_Division7_Duos", 100) + duoChampions7T.PlaylistID = "Playlist_ShowdownAlt_Duos" + duoChampions7T.AddScoringRule(defaultPlacement, NewScoringRule("MATCH_PLAYED_STAT", "gtw").AddTier(1, -8, false), defaultEliminations) + duoChampions7W := NewEventWindow("Arena_S8_Division7_Duos", duoChampions7T) + duoChampions7W.Round = 6 + duoChampions7W.ToBeDetermined = true + duoChampions7W.CanLiveSpectate = false + duoChampions7W.Meta.DivisionRank = 6 + duoChampions7W.Meta.ThresholdToAdvanceDivision = 9999999999 + ArenaDuo.AddWindow(duoChampions7W) + + return ArenaDuo +} \ No newline at end of file diff --git a/fortnite/external.go b/fortnite/external.go index e0b2921..356563a 100644 --- a/fortnite/external.go +++ b/fortnite/external.go @@ -18,29 +18,30 @@ var ( type dataClient struct { h *http.Client - FortniteSets map[string]*FortniteSet `json:"sets"` - FortniteItems map[string]*FortniteItem `json:"items"` - FortniteItemsWithDisplayAssets map[string]*FortniteItem `json:"-"` - FortniteItemsWithFeaturedImage []*FortniteItem `json:"-"` - TypedFortniteItems map[string][]*FortniteItem `json:"-"` - TypedFortniteItemsWithDisplayAssets map[string][]*FortniteItem `json:"-"` + 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]*FortniteItem), - FortniteSets: make(map[string]*FortniteSet), - FortniteItemsWithDisplayAssets: make(map[string]*FortniteItem), - FortniteItemsWithFeaturedImage: []*FortniteItem{}, - TypedFortniteItems: make(map[string][]*FortniteItem), - TypedFortniteItemsWithDisplayAssets: make(map[string][]*FortniteItem), + 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}, @@ -55,7 +56,7 @@ func NewDataClient() *dataClient { StorefrontCosmeticOfferPriceLookup: map[string]map[string]int{ "EFortRarity::Legendary": { "AthenaCharacter": 2000, - "AthenaBackpack": 1500, + "AthenaBackpack": 300, "AthenaPickaxe": 1500, "AthenaGlider": 1800, "AthenaDance": 500, @@ -63,7 +64,7 @@ func NewDataClient() *dataClient { }, "EFortRarity::Epic": { "AthenaCharacter": 1500, - "AthenaBackpack": 1200, + "AthenaBackpack": 250, "AthenaPickaxe": 1200, "AthenaGlider": 1500, "AthenaDance": 800, @@ -71,7 +72,7 @@ func NewDataClient() *dataClient { }, "EFortRarity::Rare": { "AthenaCharacter": 1200, - "AthenaBackpack": 800, + "AthenaBackpack": 200, "AthenaPickaxe": 800, "AthenaGlider": 800, "AthenaDance": 500, @@ -133,14 +134,14 @@ func (c *dataClient) LoadExternalData() { return } - content := &FortniteCosmeticsResponse{} + content := &APICosmeticsResponse{} err = json.Unmarshal(bodyBytes, content) if err != nil { return } for _, item := range content.Data { - c.LoadItem(&item) + c.LoadItemDefinition(&item) } for _, item := range c.TypedFortniteItems["AthenaBackpack"] { @@ -156,7 +157,7 @@ func (c *dataClient) LoadExternalData() { c.AddDisplayAssetToItem(displayAsset) } - variantTokens := storage.HttpAsset[map[string]SnowCosmeticVariantToken]("variants.snow.json") + variantTokens := storage.HttpAsset[map[string]SnowCosmeticVariantDefinition]("variants.snow.json") if variantTokens == nil { return } @@ -188,23 +189,142 @@ func (c *dataClient) LoadExternalData() { 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) LoadItem(item *FortniteItem) { +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] = &FortniteSet{ + c.FortniteSets[item.Set.BackendValue] = &APISetDefinition{ BackendName: item.Set.Value, DisplayName: item.Set.Text, - Items: []*FortniteItem{}, + Items: []*APICosmeticDefinition{}, } } if c.TypedFortniteItems[item.Type.BackendValue] == nil { - c.TypedFortniteItems[item.Type.BackendValue] = []*FortniteItem{} + c.TypedFortniteItems[item.Type.BackendValue] = []*APICosmeticDefinition{} } c.FortniteItems[item.ID] = item @@ -228,7 +348,7 @@ func (c *dataClient) LoadItem(item *FortniteItem) { c.FortniteItemsWithFeaturedImage = append(c.FortniteItemsWithFeaturedImage, item) } -func (c *dataClient) AddBackpackToItem(backpack *FortniteItem) { +func (c *dataClient) AddBackpackToItem(backpack *APICosmeticDefinition) { if backpack.ItemPreviewHeroPath == "" { return } @@ -239,7 +359,7 @@ func (c *dataClient) AddBackpackToItem(backpack *FortniteItem) { return } - character.Backpack = backpack + character.BackpackDefinition = backpack } func (c *dataClient) AddDisplayAssetToItem(displayAsset string) { @@ -257,20 +377,20 @@ func (c *dataClient) AddDisplayAssetToItem(displayAsset string) { return } - found.DisplayAssetPath2 = displayAsset + 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 *FortniteItem) { - ownedStyles := []FortniteVariantChannel{} +func (c *dataClient) AddNumericStylesToItem(item *APICosmeticDefinition) { + ownedStyles := []APICosmeticDefinitionVariant{} for i := 0; i < 100; i++ { - ownedStyles = append(ownedStyles, FortniteVariantChannel{ + ownedStyles = append(ownedStyles, APICosmeticDefinitionVariant{ Tag: fmt.Sprint(i), }) } - item.Variants = append(item.Variants, FortniteVariant{ + item.Variants = append(item.Variants, APICosmeticDefinitionVariantChannel{ Channel: "Numeric", Type: "int", Options: ownedStyles, @@ -290,12 +410,12 @@ func (c *dataClient) GetStorefrontDailyItemCount(season int) int { func (c *dataClient) GetStorefrontWeeklySetCount(season int) int { currentValue := 2 - for _, item := range c.StorefrontWeeklySetCountLookup { - if item.Season > season { - continue - } - currentValue = item.Sets - } + // for _, item := range c.StorefrontWeeklySetCountLookup { + // if item.Season > season { + // continue + // } + // currentValue = item.Sets + // } return currentValue } @@ -307,7 +427,7 @@ func (c *dataClient) GetStorefrontCurrencyOfferPrice(currency string, amount int return c.StorefrontCurrencyOfferPriceLookup[currency][amount] } -func (c *dataClient) GetLocalizedPrice(currency string, amount int) int { +func (c *dataClient) GetStorefrontLocalizedOfferPrice(currency string, amount int) int { return int(float64(amount) * c.StorefrontCurrencyMultiplier[currency]) } @@ -319,7 +439,7 @@ func PreloadCosmetics() error { return nil } -func GetItemByShallowID(shallowID string) *FortniteItem { +func GetItemByShallowID(shallowID string) *APICosmeticDefinition { for _, item := range DataClient.TypedFortniteItems["AthenaCharacter"] { if strings.Contains(item.ID, shallowID) { return item @@ -329,26 +449,26 @@ func GetItemByShallowID(shallowID string) *FortniteItem { return nil } -func GetRandomItemWithDisplayAsset() *FortniteItem { +func GetRandomItemWithDisplayAsset() *APICosmeticDefinition { items := DataClient.FortniteItemsWithDisplayAssets if len(items) == 0 { return nil } - flat := []FortniteItem{} + flat := []APICosmeticDefinition{} for _, item := range items { flat = append(flat, *item) } - slices.SortFunc[[]FortniteItem](flat, func(a, b FortniteItem) int { + 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) *FortniteItem { - flat := []FortniteItem{} +func GetRandomItemWithDisplayAssetOfNotType(notType string) *APICosmeticDefinition { + flat := []APICosmeticDefinition{} for t, items := range DataClient.TypedFortniteItemsWithDisplayAssets { if t == notType { @@ -360,15 +480,15 @@ func GetRandomItemWithDisplayAssetOfNotType(notType string) *FortniteItem { } } - slices.SortFunc[[]FortniteItem](flat, func(a, b FortniteItem) int { + 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() *FortniteSet { - sets := []FortniteSet{} +func GetRandomSet() *APISetDefinition { + sets := []APISetDefinition{} for _, set := range DataClient.FortniteSets { if set.BackendName == "" { continue @@ -376,7 +496,7 @@ func GetRandomSet() *FortniteSet { sets = append(sets, *set) } - slices.SortFunc[[]FortniteSet](sets, func(a, b FortniteSet) int { + slices.SortFunc[[]APISetDefinition](sets, func(a, b APISetDefinition) int { return strings.Compare(a.BackendName, b.BackendName) }) diff --git a/fortnite/granting.go b/fortnite/granting.go new file mode 100644 index 0000000..0a0fbea --- /dev/null +++ b/fortnite/granting.go @@ -0,0 +1,182 @@ +package fortnite + +import ( + "fmt" + "strings" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/person" +) + +var ( + grantLookupTable = map[string]func(*person.Person, *LootResult, *ItemGrant) error { + "AthenaCharacter": grantAthenaCosmetic, + "AthenaBackpack": grantAthenaCosmetic, + "AthenaPickaxe": grantAthenaCosmetic, + "AthenaDance": grantAthenaCosmetic, + "AthenaGlider": grantAthenaCosmetic, + "AthenaLoadingScreen": grantAthenaCosmetic, + "AthenaMusicPack": grantAthenaCosmetic, + "AthenaPet": grantAthenaCosmetic, + "AthenaSkyDiveContrail": grantAthenaCosmetic, + "AthenaSpray": grantAthenaCosmetic, + "AthenaToy": grantAthenaCosmetic, + "AthenaEmoji": grantAthenaCosmetic, + "AthenaItemWrap": grantAthenaCosmetic, + "Currency": grantCurrency, + "Token": grantCommonCoreCosmetic, + "HomebaseBannerIcon":grantCommonCoreCosmetic, + "HomebaseBannerColor": grantCommonCoreCosmetic, + "CosmeticVariantToken": grantCosmeticVariantToken, + "PersistentResource": grantPersistentResource, + "AccountResource": grantPersistentResource, + "Snow": grantSnowCustomReward, + } +) + +// This will either update the quantity of an +// already exisiting item or create a new item. +func GrantToPerson(p *person.Person, grants ...*ItemGrant) (*LootResult, error) { + loot := NewLootResult() + + for _, grant := range grants { + templateData := strings.Split(grant.TemplateID, ":") + if len(templateData) < 2 { + continue + } + + handler, ok := grantLookupTable[templateData[0]] + if !ok { + continue + } + + err := handler(p, loot, grant) + if err != nil { + return nil, err + } + } + + return loot, nil +} + +func grantAthenaCosmetic(p *person.Person, loot *LootResult, grant *ItemGrant) error { + parts := strings.Split(grant.TemplateID, ":") + + newTemplateId := "" + switch parts[0] { + case "AthenaPet": + newTemplateId = "AthenaBackpack:" + parts[1] + case "AthenaSpray": + newTemplateId = "AthenaDance:" + parts[1] + case "AthenaEmoji": + newTemplateId = "AthenaDance:" + parts[1] + case "AthenaToy": + newTemplateId = "AthenaDance:" + parts[1] + default: + newTemplateId = parts[0] + ":" + parts[1] + } + + if item := p.AthenaProfile.Items.GetItemByTemplateID(newTemplateId); item != nil { + item.Quantity++ + item.Save() + return nil + } + + item := person.NewItem(newTemplateId, grant.Quantity) + p.AthenaProfile.Items.AddItem(item).Save() + loot.AddItem(item) + + return nil +} + +func grantCurrency(p *person.Person, loot *LootResult, grant *ItemGrant) error { + p.GiveAndSyncVbucks(grant.Quantity) + return nil +} + +func grantCommonCoreCosmetic(p *person.Person, loot *LootResult, grant *ItemGrant) error { + if item := p.CommonCoreProfile.Items.GetItemByTemplateID(grant.TemplateID); item != nil { + item.Quantity++ + item.Save() + return nil + } + + item := person.NewItem(grant.TemplateID, grant.Quantity) + p.CommonCoreProfile.Items.AddItem(item).Save() + loot.AddItem(item) + return nil +} + +func grantCosmeticVariantToken(p *person.Person, loot *LootResult, grant *ItemGrant) error { + parts := strings.Split(grant.TemplateID, ":") + newTemplateId := "CosmeticVariantToken:" + parts[1] + if variantToken := p.AthenaProfile.VariantTokens.GetVariantToken(newTemplateId); variantToken != nil { + return fmt.Errorf("variant token already owned") + } + + tokenData, ok := DataClient.SnowVariantTokens[parts[1]] + if !ok { + return fmt.Errorf("invalid variant token data") + } + + found := p.AthenaProfile.Items.GetItemByTemplateID(tokenData.Item.Type.BackendValue + ":" + tokenData.Item.ID) + if found == nil { + aid.Print("tried to give variant for nil item" + tokenData.Item.Type.BackendValue + ":" + tokenData.Item.ID) + return fmt.Errorf("tried to give variant for nil item" + tokenData.Item.Type.BackendValue + ":" + tokenData.Item.ID) + } + + g := map[string][]string{} + for _, variant := range tokenData.Grants { + if _, ok := g[variant.Channel]; !ok { + g[variant.Channel] = []string{} + } + + g[variant.Channel] = append(g[variant.Channel], variant.Value) + } + + for c, tags := range g { + channel := found.GetChannel(c) + if channel == nil { + channel = found.NewChannel(c, tags, tags[0]) + found.AddChannel(channel) + continue + } + + channel.Owned = append(channel.Owned, tags...) + } + found.Save() + + p.AthenaProfile.CreateItemAttributeChangedChange(found, "Variants") + return nil +} + +func grantPersistentResource(p *person.Person, loot *LootResult, grant *ItemGrant) error { + parts := strings.Split(grant.TemplateID, ":") + switch parts[1] { + case "AthenaSeasonalXP": + p.CurrentSeasonStats.SeasonXP += grant.Quantity + p.CurrentSeasonStats.Save() + p.AthenaProfile.Attributes.GetAttributeByKey("level").SetValue(DataClient.SnowSeason.GetSeasonLevel(p.CurrentSeasonStats)).Save() + p.AthenaProfile.Attributes.GetAttributeByKey("xp").SetValue(DataClient.SnowSeason.GetRelativeSeasonXP(p.CurrentSeasonStats)).Save() + case "AthenaBattleStar": + p.CurrentSeasonStats.BookXP += grant.Quantity + p.CurrentSeasonStats.Save() + p.AthenaProfile.Attributes.GetAttributeByKey("book_level").SetValue(DataClient.SnowSeason.GetBookLevel(p.CurrentSeasonStats)).Save() + p.AthenaProfile.Attributes.GetAttributeByKey("book_xp").SetValue(DataClient.SnowSeason.GetRelativeBookXP(p.CurrentSeasonStats)).Save() + break + } + return nil +} + +func grantSnowCustomReward(p *person.Person, loot *LootResult, grant *ItemGrant) error { + parts := strings.Split(grant.TemplateID, ":") + switch parts[1] { + case "BattlePass": + p.CurrentSeasonStats.BookPurchased = true + p.CurrentSeasonStats.Save() + p.AthenaProfile.Attributes.GetAttributeByKey("book_purchased").SetValue(true).Save() + } + + DataClient.SnowSeason.GrantUnredeemedBookRewards(p, "GB_BattlePassPurchased") + return nil +} diff --git a/fortnite/person.go b/fortnite/person.go index 2a4816d..862970b 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -2,11 +2,9 @@ package fortnite import ( "strconv" - "strings" "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" - "github.com/ectrc/snow/storage" "github.com/google/uuid" ) @@ -29,39 +27,14 @@ func NewFortnitePerson(displayName string, everything bool) *p.Person { } func GiveEverything(person *p.Person) { - items := make([]storage.DB_Item, 0) - for _, item := range DataClient.FortniteItems { - if strings.Contains(strings.ToLower(item.ID), "random") { - continue - } - - has := person.AthenaProfile.Items.GetItemByTemplateID(item.ID) - if has != nil { - continue - } - - new := p.NewItem(item.Type.BackendValue + ":" + item.ID, 1) - new.HasSeen = true - - 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)) + GrantToPerson(person, NewItemGrant(item.Type.BackendValue+":"+item.ID, 1)) + } + + for key := range DataClient.SnowVariantTokens { + GrantToPerson(person, NewItemGrant("CosmeticVariantToken:"+key, 1)) } - storage.Repo.BulkCreateItems(&items) person.Save() } @@ -78,25 +51,21 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. for _, item := range defaultCommonCoreItems { if item == "HomebaseBannerIcon:StandardBanner" { for i := 1; i < 32; i++ { - item := p.NewItem(item+strconv.Itoa(i), 1) - item.HasSeen = true - person.CommonCoreProfile.Items.AddItem(item).Save() + GrantToPerson(person, NewItemGrant(item+strconv.Itoa(i), 1)) } continue } if item == "HomebaseBannerColor:DefaultColor" { for i := 1; i < 22; i++ { - item := p.NewItem(item+strconv.Itoa(i), 1) - item.HasSeen = true - person.CommonCoreProfile.Items.AddItem(item).Save() + GrantToPerson(person, NewItemGrant(item+strconv.Itoa(i), 1)) } continue } if item == "Currency:MtxPurchased" { - person.CommonCoreProfile.Items.AddItem(p.NewItem(item, 0)).Save() - person.Profile0Profile.Items.AddItem(p.NewItem(item, 0)).Save() + person.CommonCoreProfile.Items.AddItem(p.NewItem(item, 9999999)).Save() + person.Profile0Profile.Items.AddItem(p.NewItem(item, 99999999)).Save() continue } @@ -111,10 +80,11 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("inventory_limit_bonus", 0)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("daily_rewards", []aid.JSON{})).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("competitive_identity", aid.JSON{})).Save() + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("permissions", []aid.JSON{})).Save() + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("season_update", 0)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("season_num", aid.Config.Fortnite.Season)).Save() - person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("permissions", []aid.JSON{})).Save() - + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("accountLevel", 1)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("level", 1)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("xp", 0)).Save() @@ -122,11 +92,15 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("rested_xp", 0)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("rested_xp_mult", 0)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("rested_xp_exchange", 0)).Save() - + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("book_purchased", false)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("book_level", 1)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("book_xp", 0)).Save() + seasonStats := p.NewSeasonStats(aid.Config.Fortnite.Season) + seasonStats.PersonID = person.ID + seasonStats.Save() + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_character", person.AthenaProfile.Items.GetItemByTemplateID("AthenaCharacter:CID_001_Athena_Commando_F_Default").ID)).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_backpack", "")).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_pickaxe", person.AthenaProfile.Items.GetItemByTemplateID("AthenaPickaxe:DefaultPickaxe").ID)).Save() @@ -136,8 +110,8 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_itemwraps", make([]string, 7))).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_loadingscreen", "")).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("favorite_musicpack", "")).Save() - person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("banner_icon", "")).Save() - person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("banner_color", "")).Save() + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("banner_icon", "StandardBanner1")).Save() + person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("banner_color", "DefaultColor1")).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("mfa_enabled", true)).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("mtx_affiliate", "")).Save() @@ -151,10 +125,19 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("allowed_to_receive_gifts", true)).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("allowed_to_send_gifts", true)).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("gift_history", aid.JSON{})).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("in_app_purchases", aid.JSON{ + "receipts": []string{}, + "ignoredReceipts": []string{}, + "fulfillmentCounts": map[string]int{}, + "refreshTimers": aid.JSON{}, + })).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("party.recieveIntents", "ALL")).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("party.recieveInvites", "ALL")).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("season.bookFreeClaimedUpTo", 0)).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("season.bookPaidClaimedUpTo", 0)).Save() + person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("season.levelClaimedUpTo", 0)).Save() loadout := p.NewLoadout("PRESET 1", person.AthenaProfile) person.AthenaProfile.Loadouts.AddLoadout(loadout).Save() diff --git a/fortnite/season.go b/fortnite/season.go new file mode 100644 index 0000000..73ad360 --- /dev/null +++ b/fortnite/season.go @@ -0,0 +1,331 @@ +package fortnite + +import ( + "fmt" + "regexp" + "strings" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/person" +) + +type SeasonObjectReference struct { + ObjectName string `json:"ObjectName"` + ObjectPath string `json:"ObjectPath"` +} + +type SeasonAssetReference struct { + AssetPathName string `json:"AssetPathName"` + SubPathString string `json:"SubPathString"` +} + +type SeasonObjectTag struct { + TagName string `json:"TagName"` +} + +type SeasonRewardGiftBox struct { + GiftBoxToUse SeasonAssetReference `json:"GiftBoxToUse"` + GiftBoxFormatData []any `json:"GiftBoxFormatData"` +} + +type ObjectSeasonReward struct { + ItemDefinition SeasonAssetReference `json:"ItemDefinition"` + TemplateID string `json:"TemplateId"` + Quantity int `json:"Quantity"` + RewardGiftBox SeasonRewardGiftBox `json:"RewardGiftBox"` + IsChaseReward bool `json:"IsChaseReward"` + RewardType string `json:"RewardType"` +} + +type SeasonScheduleLevel struct { + Rewards []ObjectSeasonReward `json:"Rewards"` +} + +type SeasonSchedule struct { + Levels []SeasonScheduleLevel `json:"Levels"` +} + +type UnrealSeasonProperties struct { + SeasonNumber int `json:"SeasonNumber"` + NumSeasonLevels int `json:"NumSeasonLevels"` + NumBookLevels int `json:"NumBookLevels"` + ChallengesVisibility string `json:"ChallengesVisibility"` + SeasonXpCurve SeasonObjectReference `json:"SeasonXpCurve"` + BookXpCurve SeasonObjectReference `json:"BookXpCurve"` + SeasonStorefront string `json:"SeasonStorefront"` + FreeTokenItemPrimaryAssetId struct { + PrimaryAssetType struct { + Name string `json:"Name"` + } `json:"PrimaryAssetType"` + PrimaryAssetName string `json:"PrimaryAssetName"` + } `json:"FreeTokenItemPrimaryAssetId"` + + BattlePassOfferId string `json:"BattlePassOfferId"` + BattlePassBundleOfferId string `json:"BattlePassBundleOfferId"` + BattlePassLevelOfferID string `json:"BattlePassLevelOfferId"` + + ChallengeSchedulesAlwaysShown []SeasonObjectReference `json:"ChallengeSchedulesAlwaysShown"` + FreeLevelsThatNavigateToBattlePass []int `json:"FreeLevelsThatNavigateToBattlePass"` + FreeLevelsThatAutoOpenTheAboutScreen []int `json:"FreeLevelsThatAutoOpenTheAboutScreen"` + FreeSeasonItemContentTag SeasonObjectTag `json:"FreeSeasonItemContentTag"` + SeasonFirstWinItemContentTag SeasonObjectTag `json:"SeasonFirstWinItemContentTag"` + SeasonGrantsToEveryoneItemContentTag SeasonObjectTag `json:"SeasonGrantsToEveryoneItemContentTag"` + BattlePassPaidItemContentTag SeasonObjectTag `json:"BattlePassPaidItemContentTag"` + BattlePassFreeItemContentTag SeasonObjectTag `json:"BattlePassFreeItemContentTag"` + + SeasonXpScheduleFree SeasonSchedule `json:"SeasonXpScheduleFree"` + BookXpScheduleFree SeasonSchedule `json:"BookXpScheduleFree"` + BookXpSchedulePaid SeasonSchedule `json:"BookXpSchedulePaid"` + SeasonGrantsToEveryone SeasonScheduleLevel `json:"SeasonGrantsToEveryone"` + SeasonFirstWinRewards SeasonScheduleLevel `json:"SeasonFirstWinRewards"` + BattleStarSubstitutionReward SeasonScheduleLevel `json:"BattleStarSubstitutionReward"` + ExpiringRewardTypes []SeasonAssetReference `json:"ExpiringRewardTypes"` + TokensToRemoveAtSeasonEnd []SeasonAssetReference `json:"TokensToRemoveAtSeasonEnd"` + + DisplayName struct { + Key string `json:"Key"` + SourceString string `json:"SourceString"` + LocalizedString string `json:"LocalizedString"` + } +} + +type UnrealProgressionProperties struct { + RowStruct SeasonObjectReference `json:"RowStruct"` +} + +type UnrealEngineObjectProperties interface { + UnrealSeasonProperties | UnrealProgressionProperties +} + +type UnrealNoRows struct{} +type UnrealProgressionRows struct { + Level int `json:"Level"` + XpToNextLevel int `json:"XpToNextLevel"` + XpTotal int `json:"XpTotal"` +} + +type UnrealEngineObjectRows interface { + UnrealNoRows | UnrealProgressionRows +} + +type UnrealEngineObject[T UnrealEngineObjectProperties, K UnrealEngineObjectRows] struct { + Type string `json:"Type"` + Name string `json:"Name"` + Class string `json:"Class"` + Properties T `json:"Properties"` + Rows *map[string]K `json:"Rows"` +} + +type SnowSeasonDefinition struct { + DefaultOfferID string + BundleOfferID string + TierOfferID string + LevelProgression []*UnrealProgressionRows + BookProgression []*UnrealProgressionRows + TierRewardsPremium [][]*ItemGrant + TierRewardsFree [][]*ItemGrant + LevelRewards [][]*ItemGrant + VictoryRewards []*ItemGrant + SeasonTokenRemoval []*ItemGrant + BookXPReplacements []*ItemGrant +} + +func NewSeasonDefinition() *SnowSeasonDefinition { + return &SnowSeasonDefinition{ + LevelProgression: make([]*UnrealProgressionRows, 101), + BookProgression: make([]*UnrealProgressionRows, 101), + TierRewardsPremium: make([][]*ItemGrant, 101), + TierRewardsFree: make([][]*ItemGrant, 101), + LevelRewards: make([][]*ItemGrant, 101), + SeasonTokenRemoval: make([]*ItemGrant, 0), + VictoryRewards: make([]*ItemGrant, 0), + BookXPReplacements: make([]*ItemGrant, 0), + } +} + +func convertAssetPathToTemplateId(assetPath string) string { + templateIdParts := make([]string, 2) + regex := regexp.MustCompile(`\.(.*)`) + assetPathParts := regex.FindStringSubmatch(assetPath) + if len(assetPathParts) <= 1 { + return "" + } + templateIdParts[1] = assetPathParts[1] + + switch { + case strings.Contains(assetPath, "Game/Items/PersistentResource"): + templateIdParts[0] = "AccountResource" + case strings.Contains(assetPath, "Game/Items/Currency"): + templateIdParts[0] = "Currency" + case strings.Contains(assetPath, "Game/Items/Tokens"): + templateIdParts[0] = "Token" + case strings.Contains(assetPath, "Game/Athena/Items/ChallengeBundleSchedules"): + templateIdParts[0] = "ChallengeBundleSchedule" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Pickaxes"): + templateIdParts[0] = "AthenaPickaxe" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Dances"): + templateIdParts[0] = "AthenaDance" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Sprays"): + templateIdParts[0] = "AthenaDance" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Backpacks"): + templateIdParts[0] = "AthenaBackpack" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/PetCarriers"): + templateIdParts[0] = "AthenaBackpack" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Pets"): + templateIdParts[0] = "AthenaBackpack" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/MusicPacks"): + templateIdParts[0] = "AthenaMusicPack" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Characters"): + templateIdParts[0] = "AthenaCharacter" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Gliders"): + templateIdParts[0] = "AthenaGlider" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/LoadingScreens"): + templateIdParts[0] = "AthenaLoadingScreen" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Toys"): + templateIdParts[0] = "AthenaDance" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/ItemWraps"): + templateIdParts[0] = "AthenaItemWrap" + case strings.Contains(assetPath, "Game/Athena/Items/Cosmetics/Contrails"): + templateIdParts[0] = "AthenaSkydiveContrail" + case strings.Contains(assetPath, "Game/Athena/Items/CosmeticVariantTokens"): + templateIdParts[0] = "CosmeticVariantToken" + default: + aid.Print("Unknown asset path:", assetPath) + } + + return strings.Join(templateIdParts, ":") +} + +func (s *SnowSeasonDefinition) GetSeasonLevel(stats *person.SeasonStats) int { + level := 0 + + for i, data := range s.LevelProgression { + if i == 0 { + continue + } + + if stats.SeasonXP < data.XpTotal { + break + } + + level = i + } + + return level +} + +func (s *SnowSeasonDefinition) GetRelativeSeasonXP(stats *person.SeasonStats) int { + level := s.GetSeasonLevel(stats) + if level == 0 { + return 0 + } + + return stats.SeasonXP - s.LevelProgression[level].XpTotal +} + +func (s *SnowSeasonDefinition) GetBookLevel(stats *person.SeasonStats) int { + level := 0 + + for i, data := range s.BookProgression { + if i == 0 { + continue + } + + level = i + if stats.BookXP - s.BookProgression[i - 1].XpTotal < data.XpToNextLevel { + break + } + } + + return level +} + +func (s *SnowSeasonDefinition) GetRelativeBookXP(stats *person.SeasonStats) int { + level := s.GetBookLevel(stats) + if level == 0 { + return 0 + } + + return stats.BookXP - s.BookProgression[level - 1].XpTotal +} + +func (s *SnowSeasonDefinition) GrantUnredeemedBookRewards(p *person.Person, giftBoxId string) bool { + changed := false + gift := person.NewGift(fmt.Sprintf("GiftBox:%s", giftBoxId), 1, "", "") + + grantUpTo := s.GetBookLevel(p.CurrentSeasonStats) + freeClaimedUpTo := aid.JSONParseG[int](p.CommonCoreProfile.Attributes.GetAttributeByKey("season.bookFreeClaimedUpTo").ValueJSON) + paidClaimedUpTo := aid.JSONParseG[int](p.CommonCoreProfile.Attributes.GetAttributeByKey("season.bookPaidClaimedUpTo").ValueJSON) + + if freeClaimedUpTo >= len(s.TierRewardsFree) - 1 { + return changed + } + + if freeClaimedUpTo > grantUpTo { + freeClaimedUpTo = grantUpTo + } + + if paidClaimedUpTo > grantUpTo { + paidClaimedUpTo = grantUpTo + } + + freeRewards := aid.Flatten[*ItemGrant](s.TierRewardsFree[freeClaimedUpTo+1:grantUpTo+1]) + paidRewards := aid.Flatten[*ItemGrant](s.TierRewardsPremium[paidClaimedUpTo+1:grantUpTo+1]) + + rewards := []*ItemGrant{} + rewards = append(rewards, freeRewards...) + rewards = append(rewards, aid.Ternary[[]*ItemGrant](p.CurrentSeasonStats.BookPurchased, paidRewards, []*ItemGrant{})...) + + for _, reward := range rewards { + gift.AddLoot(person.NewItem(reward.TemplateID, reward.Quantity)) + } + + if p.CurrentSeasonStats.BookPurchased { + p.CommonCoreProfile.Attributes.GetAttributeByKey("season.bookPaidClaimedUpTo").SetValue(grantUpTo).Save() + } + p.CommonCoreProfile.Attributes.GetAttributeByKey("season.bookFreeClaimedUpTo").SetValue(grantUpTo).Save() + + if len(gift.Loot) > 0 { + p.CommonCoreProfile.Gifts.AddGift(gift).Save() + changed = true + } + + return changed +} + +func (s *SnowSeasonDefinition) GrantUnredeemedLevelRewards(p *person.Person) bool { + changed := false + grantUpTo := s.GetSeasonLevel(p.CurrentSeasonStats) + bookLevel := s.GetBookLevel(p.CurrentSeasonStats) + + levelClaimedUpTo := aid.JSONParseG[int](p.CommonCoreProfile.Attributes.GetAttributeByKey("season.levelClaimedUpTo").ValueJSON) + if levelClaimedUpTo > grantUpTo { + levelClaimedUpTo = grantUpTo + } + + if levelClaimedUpTo >= len(s.LevelRewards) - 1 { + return changed + } + + wantedRewards := aid.Flatten[*ItemGrant](s.LevelRewards[levelClaimedUpTo+1:grantUpTo+1]) + replacementRewards := []*ItemGrant{} + + for _, reward := range wantedRewards { + for _, replacement := range s.BookXPReplacements { + replacementRewards = append(replacementRewards, aid.Ternary[*ItemGrant](reward.TemplateID == "AccountResource:AthenaBattleStar", replacement, reward)) + } + } + + realRewards := aid.Ternary[[]*ItemGrant](bookLevel > 100, replacementRewards, wantedRewards) + for _, reward := range realRewards { + GrantToPerson(p, reward) + } + + p.CommonCoreProfile.Attributes.GetAttributeByKey("season.levelClaimedUpTo").SetValue(grantUpTo).Save() + + if len(realRewards) > 0 { + changed = true + } + + return changed +} \ No newline at end of file diff --git a/fortnite/shop.go b/fortnite/shop.go deleted file mode 100644 index 0ece97f..0000000 --- a/fortnite/shop.go +++ /dev/null @@ -1,852 +0,0 @@ -package fortnite - -import ( - "fmt" - "math/rand" - "regexp" - "strings" - "time" - - "github.com/ectrc/snow/aid" - "github.com/google/uuid" -) - -type FortniteCatalogStarterPackGrant struct { - TemplateID string - Quantity int -} - -func NewFortniteCatalogStarterPackGrant(templateID string, quantity int) *FortniteCatalogStarterPackGrant { - return &FortniteCatalogStarterPackGrant{ - TemplateID: templateID, - Quantity: quantity, - } -} - -type FortniteCatalogStarterPack struct { - ID string - DevName string - Grants []*FortniteCatalogStarterPackGrant - Meta struct { - IconSize string - BannerOverride string - DisplayAssetPath string - NewDisplayAssetPath string - OriginalOffer int - ExtraBonus int - } - Price struct { - PriceType string - PriceToPay int - } - Title string - Description string - LongDescription string - Priority int - SeasonsAllowed []int -} - -func NewFortniteCatalogStarterPack(price int) *FortniteCatalogStarterPack { - return &FortniteCatalogStarterPack{ - ID: "v2:/" + aid.RandomString(32), - Price: struct { - PriceType string - PriceToPay int - }{"RealMoney", price}, - } -} - -func (f *FortniteCatalogStarterPack) GenerateFortniteCatalogStarterPackResponse() aid.JSON { - grantsResponse := []aid.JSON{} - for _, grant := range f.Grants { - grantsResponse = append(grantsResponse, aid.JSON{ - "templateId": grant.TemplateID, - "quantity": grant.Quantity, - }) - } - - prices := []aid.JSON{} - switch f.Price.PriceType { - case "RealMoney": - prices = append(prices, aid.JSON{ - "currencyType": "RealMoney", - "currencySubType": "", - "regularPrice": 0, - "dynamicRegularPrice": -1, - "finalPrice": 0, - "saleExpiration": "9999-12-31T23:59:59.999Z", - "basePrice": 0, - }) - case "MtxCurrency": - prices = append(prices, aid.JSON{ - "currencyType": "MtxCurrency", - "currencySubType": "", - "regularPrice": f.Price.PriceToPay, - "dynamicRegularPrice": f.Price.PriceToPay, - "finalPrice": f.Price.PriceToPay, - "saleExpiration": "9999-12-31T23:59:59.999Z", - "basePrice": f.Price.PriceToPay, - }) - } - - return aid.JSON{ - "offerId": f.ID, - "devName": f.DevName, - "offerType": "StaticPrice", - "prices": prices, - "categories": []string{}, - "dailyLimit": -1, - "weeklyLimit": -1, - "monthlyLimit": -1, - "refundable": false, - "appStoreId": []string{ - "", - "app-" + f.ID, - }, - "requirements": []aid.JSON{}, - "metaInfo": []aid.JSON{ - { - "key": "SectionId", - "value": "LimitedTime", - }, - { - "key": "IconSize", - "value": f.Meta.IconSize, - }, - { - "key": "BannerOverride", - "value": f.Meta.BannerOverride, - }, - { - "key": "DisplayAssetPath", - "value": f.Meta.DisplayAssetPath, - }, - { - "key": "NewDisplayAssetPath", - "value": f.Meta.NewDisplayAssetPath, - }, - { - "key": "MtxQuantity", - "value": f.Meta.OriginalOffer + f.Meta.ExtraBonus, - }, - { - "key": "MtxBonus", - "value": f.Meta.ExtraBonus, - }, - }, - "meta": aid.JSON{ - "IconSize": f.Meta.IconSize, - "BannerOverride": f.Meta.BannerOverride, - "SectionID": "LimitedTime", - "DisplayAssetPath": f.Meta.DisplayAssetPath, - "NewDisplayAssetPath": f.Meta.NewDisplayAssetPath, - "MtxQuantity": f.Meta.OriginalOffer + f.Meta.ExtraBonus, - "MtxBonus": f.Meta.ExtraBonus, - }, - "catalogGroup": "", - "catalogGroupPriority": 0, - "sortPriority": f.Priority, - "bannerOverride": f.Meta.BannerOverride, - "title": f.Title, - "shortDescription": "", - "description": f.Description, - "displayAssetPath": f.Meta.DisplayAssetPath, - "itemGrants": []aid.JSON{}, - } -} - -func (f *FortniteCatalogStarterPack) GenerateFortniteCatalogBulkOfferResponse() aid.JSON { - return aid.JSON{ - "id": "app-" + f.ID, - "title": f.Title, - "description": f.Description, - "longDescription": f.LongDescription, - "technicalDetails": "", - "keyImages": []aid.JSON{}, - "categories": []aid.JSON{}, - "namespace": "fn", - "status": "ACTIVE", - "creationDate": time.Now().Format(time.RFC3339), - "lastModifiedDate": time.Now().Format(time.RFC3339), - "customAttributes": aid.JSON{}, - "internalName": f.Title, - "recurrence": "ONCE", - "items": []aid.JSON{}, - "price": DataClient.GetLocalizedPrice("GBP", f.Price.PriceToPay), - "currentPrice": DataClient.GetLocalizedPrice("GBP", f.Price.PriceToPay), - "currencyCode": "GBP", - "basePrice": DataClient.GetLocalizedPrice("USD", f.Price.PriceToPay), - "basePriceCurrencyCode": "USD", - "recurringPrice": 0, - "freeDays": 0, - "maxBillingCycles": 0, - "seller": aid.JSON{}, - "viewableDate": time.Now().Format(time.RFC3339), - "effectiveDate": time.Now().Format(time.RFC3339), - "expiryDate": "9999-12-31T23:59:59.999Z", - "vatIncluded": true, - "isCodeRedemptionOnly": false, - "isFeatured": false, - "taxSkuId": "FN_Currency", - "merchantGroup": "FN_MKT", - "priceTier": fmt.Sprintf("%d", DataClient.GetLocalizedPrice("USD", f.Price.PriceToPay)), - "urlSlug": "fortnite--" + f.Title, - "roleNamesToGrant": []aid.JSON{}, - "tags": []aid.JSON{}, - "purchaseLimit": -1, - "ignoreOrder": false, - "fulfillToGroup": false, - "fraudItemType": "V-Bucks", - "shareRevenue": false, - "offerType": "OTHERS", - "unsearchable": false, - "releaseDate": time.Now().Format(time.RFC3339), - "releaseOffer": "", - "title4Sort": f.Title, - "countriesBlacklist": []string{}, - "selfRefundable": false, - "refundType": "NON_REFUNDABLE", - "pcReleaseDate": time.Now().Format(time.RFC3339), - "priceCalculationMode": "FIXED", - "assembleMode": "SINGLE", - "publisherDisplayName": "Epic Games", - "developerDisplayName": "Epic Games", - "visibilityType": "IS_LISTED", - "currencyDecimals": 2, - "allowPurchaseForPartialOwned": true, - "shareRevenueWithUnderageAffiliates": false, - "platformWhitelist": []string{}, - "platformBlacklist": []string{}, - "partialItemPrerequisiteCheck": false, - "upgradeMode": "UPGRADED_WITH_PRICE_FULL", - } -} - -func (startPack *FortniteCatalogStarterPack) AddGrant(g *FortniteCatalogStarterPackGrant) { - startPack.Grants = append(startPack.Grants, g) -} - -type FortniteCatalogCurrencyOffer struct { - ID string - DevName string - Price struct { - OriginalOffer int - ExtraBonus int - } - Meta struct { - IconSize string - CurrencyAnalyticsName string - BannerOverride string - } - Title string - Description string - LongDescription string - Priority int -} - -func NewFortniteCatalogCurrencyOffer(original, bonus int) *FortniteCatalogCurrencyOffer { - return &FortniteCatalogCurrencyOffer{ - ID: "v2:/"+aid.RandomString(32), - Price: struct { - OriginalOffer int - ExtraBonus int - }{original, bonus}, - } -} - -func (f *FortniteCatalogCurrencyOffer) GenerateFortniteCatalogCurrencyOfferResponse() aid.JSON { - return aid.JSON{ - "offerId": f.ID, - "devName": f.DevName, - "offerType": "StaticPrice", - "prices": []aid.JSON{{ - "currencyType": "RealMoney", - "currencySubType": "", - "regularPrice": 0, - "dynamicRegularPrice": -1, - "finalPrice": 0, - "saleExpiration": "9999-12-31T23:59:59.999Z", - "basePrice": 0, - }}, - "categories": []string{}, - "dailyLimit": -1, - "weeklyLimit": -1, - "monthlyLimit": -1, - "refundable": false, - "appStoreId": []string{ - "", - "app-" + f.ID, - }, - "requirements": []aid.JSON{}, - "metaInfo": []aid.JSON{ - { - "key": "MtxQuantity", - "value": f.Price.OriginalOffer + f.Price.ExtraBonus, - }, - { - "key": "MtxBonus", - "value": f.Price.ExtraBonus, - }, - { - "key": "IconSize", - "value": f.Meta.IconSize, - }, - { - "key": "BannerOverride", - "value": f.Meta.BannerOverride, - }, - { - "Key": "CurrencyAnalyticsName", - "Value": f.Meta.CurrencyAnalyticsName, - }, - }, - "meta": aid.JSON{ - "IconSize": f.Meta.IconSize, - "CurrencyAnalyticsName": f.Meta.CurrencyAnalyticsName, - "BannerOverride": f.Meta.BannerOverride, - "MtxQuantity": f.Price.OriginalOffer + f.Price.ExtraBonus, - "MtxBonus": f.Price.ExtraBonus, - }, - "catalogGroup": "", - "catalogGroupPriority": 0, - "sortPriority": f.Priority, - "bannerOverride": f.Meta.BannerOverride, - "title": f.Title, - "shortDescription": "", - "description": f.Description, - "displayAssetPath": "/Game/Catalog/DisplayAssets/DA_" + f.Meta.CurrencyAnalyticsName + ".DA_" + f.Meta.CurrencyAnalyticsName, - "itemGrants": []aid.JSON{}, - } -} - -func (f *FortniteCatalogCurrencyOffer) GenerateFortniteCatalogBulkOfferResponse() aid.JSON{ - return aid.JSON{ - "id": "app-" + f.ID, - "title": f.Title, - "description": f.Description, - "longDescription": f.LongDescription, - "technicalDetails": "", - "keyImages": []aid.JSON{}, - "categories": []aid.JSON{}, - "namespace": "fn", - "status": "ACTIVE", - "creationDate": time.Now().Format(time.RFC3339), - "lastModifiedDate": time.Now().Format(time.RFC3339), - "customAttributes": aid.JSON{}, - "internalName": f.Title, - "recurrence": "ONCE", - "items": []aid.JSON{}, - "price": DataClient.GetStorefrontCurrencyOfferPrice("GBP", f.Price.OriginalOffer + f.Price.ExtraBonus), - "currentPrice": DataClient.GetStorefrontCurrencyOfferPrice("GBP", f.Price.OriginalOffer + f.Price.ExtraBonus), - "currencyCode": "GBP", - "basePrice": DataClient.GetStorefrontCurrencyOfferPrice("USD", f.Price.OriginalOffer + f.Price.ExtraBonus), - "basePriceCurrencyCode": "USD", - "recurringPrice": 0, - "freeDays": 0, - "maxBillingCycles": 0, - "seller": aid.JSON{}, - "viewableDate": time.Now().Format(time.RFC3339), - "effectiveDate": time.Now().Format(time.RFC3339), - "expiryDate": "9999-12-31T23:59:59.999Z", - "vatIncluded": true, - "isCodeRedemptionOnly": false, - "isFeatured": false, - "taxSkuId": "FN_Currency", - "merchantGroup": "FN_MKT", - "priceTier": fmt.Sprintf("%d", DataClient.GetStorefrontCurrencyOfferPrice("USD", f.Price.OriginalOffer + f.Price.ExtraBonus)), - "urlSlug": "fortnite--" + f.Title, - "roleNamesToGrant": []aid.JSON{}, - "tags": []aid.JSON{}, - "purchaseLimit": -1, - "ignoreOrder": false, - "fulfillToGroup": false, - "fraudItemType": "V-Bucks", - "shareRevenue": false, - "offerType": "OTHERS", - "unsearchable": false, - "releaseDate": time.Now().Format(time.RFC3339), - "releaseOffer": "", - "title4Sort": f.Title, - "countriesBlacklist": []string{}, - "selfRefundable": false, - "refundType": "NON_REFUNDABLE", - "pcReleaseDate": time.Now().Format(time.RFC3339), - "priceCalculationMode": "FIXED", - "assembleMode": "SINGLE", - "publisherDisplayName": "Epic Games", - "developerDisplayName": "Epic Games", - "visibilityType": "IS_LISTED", - "currencyDecimals": 2, - "allowPurchaseForPartialOwned": true, - "shareRevenueWithUnderageAffiliates": false, - "platformWhitelist": []string{}, - "platformBlacklist": []string{}, - "partialItemPrerequisiteCheck": false, - "upgradeMode": "UPGRADED_WITH_PRICE_FULL", - } -} - -type FortniteCatalogCosmeticOffer 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() *FortniteCatalogCosmeticOffer { - return &FortniteCatalogCosmeticOffer{} -} - -func (f *FortniteCatalogCosmeticOffer) GenerateID() { - for _, item := range f.Grants { - f.ID += item.Type.BackendValue + ":" + item.ID + "," - } - - f.ID = "v2:/" + aid.Hash([]byte(f.ID)) -} - -func (f *FortniteCatalogCosmeticOffer) GenerateTotalPrice() { - if !f.BundleInfo.IsBundle { - f.TotalPrice = DataClient.GetStorefrontCosmeticOfferPrice(f.Grants[0].Rarity.BackendValue, f.Grants[0].Type.BackendValue) - return - } - - for _, item := range f.Grants { - f.TotalPrice += DataClient.GetStorefrontCosmeticOfferPrice(item.Rarity.BackendValue, item.Rarity.BackendValue) - } -} - -func (f *FortniteCatalogCosmeticOffer) GenerateFortniteCatalogCosmeticOfferResponse() 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 []*FortniteCatalogCosmeticOffer -} - -func NewFortniteCatalogSection(name string) *FortniteCatalogSection { - return &FortniteCatalogSection{ - Name: name, - } -} - -func (f *FortniteCatalogSection) GenerateFortniteCatalogSectionResponse() aid.JSON { - catalogEntiresResponse := []aid.JSON{} - for _, offer := range f.Offers { - catalogEntiresResponse = append(catalogEntiresResponse, offer.GenerateFortniteCatalogCosmeticOfferResponse()) - } - - return aid.JSON{ - "name": f.Name, - "catalogEntries": catalogEntiresResponse, - } -} - -func (f *FortniteCatalogSection) GetGroupedOffers() map[string][]*FortniteCatalogCosmeticOffer { - groupedOffers := map[string][]*FortniteCatalogCosmeticOffer{} - - for _, offer := range f.Offers { - if groupedOffers[offer.Meta.Category] == nil { - groupedOffers[offer.Meta.Category] = []*FortniteCatalogCosmeticOffer{} - } - - groupedOffers[offer.Meta.Category] = append(groupedOffers[offer.Meta.Category], offer) - } - - return groupedOffers -} - -type FortniteCatalog struct { - Sections []*FortniteCatalogSection - MoneyOffers []*FortniteCatalogCurrencyOffer - StarterPacks []*FortniteCatalogStarterPack -} - -func NewFortniteCatalog() *FortniteCatalog { - return &FortniteCatalog{ - Sections: []*FortniteCatalogSection{}, - MoneyOffers: []*FortniteCatalogCurrencyOffer{}, - StarterPacks: []*FortniteCatalogStarterPack{}, - } -} - -func (f *FortniteCatalog) AddSection(section *FortniteCatalogSection) { - f.Sections = append(f.Sections, section) -} - -func (f *FortniteCatalog) AddMoneyOffer(offer *FortniteCatalogCurrencyOffer) { - offer.Priority = -len(f.MoneyOffers) - f.MoneyOffers = append(f.MoneyOffers, offer) -} - -func (f *FortniteCatalog) AddStarterPack(pack *FortniteCatalogStarterPack) { - pack.Priority = -len(f.StarterPacks) - f.StarterPacks = append(f.StarterPacks, pack) -} - -func (f *FortniteCatalog) GenerateFortniteCatalogResponse() aid.JSON { - catalogSectionsResponse := []aid.JSON{} - - for _, section := range f.Sections { - catalogSectionsResponse = append(catalogSectionsResponse, section.GenerateFortniteCatalogSectionResponse()) - } - - currencyOffersResponse := []aid.JSON{} - for _, offer := range f.MoneyOffers { - currencyOffersResponse = append(currencyOffersResponse, offer.GenerateFortniteCatalogCurrencyOfferResponse()) - } - catalogSectionsResponse = append(catalogSectionsResponse, aid.JSON{ - "name": "CurrencyStorefront", - "catalogEntries": currencyOffersResponse, - }) - - starterPacksResponse := []aid.JSON{} - for _, pack := range f.StarterPacks { - for _, season := range pack.SeasonsAllowed { - if season == aid.Config.Fortnite.Season { - starterPacksResponse = append(starterPacksResponse, pack.GenerateFortniteCatalogStarterPackResponse()) - break - } - } - } - catalogSectionsResponse = append(catalogSectionsResponse, aid.JSON{ - "name": "BRStarterKits", - "catalogEntries": starterPacksResponse, - }) - - return aid.JSON{ - "storefronts": catalogSectionsResponse, - "refreshIntervalHrs": 24, - "dailyPurchaseHrs": 24, - "expiration": "9999-12-31T23:59:59.999Z", - } -} - -func (f *FortniteCatalog) FindCosmeticOfferById(id string) *FortniteCatalogCosmeticOffer { - for _, section := range f.Sections { - for _, offer := range section.Offers { - if offer.ID == id { - return offer - } - } - } - - return nil -} - -func (f *FortniteCatalog) FindCurrencyOfferById(id string) *FortniteCatalogCurrencyOffer { - for _, offer := range f.MoneyOffers { - if offer.ID == id { - return offer - } - } - - return nil -} - -func (f *FortniteCatalog) FindStarterPackById(id string) *FortniteCatalogStarterPack { - for _, pack := range f.StarterPacks { - if pack.ID == id { - return pack - } - } - - return nil -} - -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) < DataClient.GetStorefrontDailyItemCount(aid.Config.Fortnite.Season) { - entry := newCosmeticOfferFromFortniteitem(GetRandomItemWithDisplayAssetOfNotType("AthenaCharacter"), false) - entry.Meta.SectionId = "Daily" - daily.Offers = append(daily.Offers, entry) - } - catalog.AddSection(daily) - - weekly := NewFortniteCatalogSection("BRWeeklyStorefront") - for len(weekly.GetGroupedOffers()) < DataClient.GetStorefrontWeeklySetCount(aid.Config.Fortnite.Season) { - set := GetRandomSet() - for _, item := range set.Items { - if item.DisplayAssetPath == "" || item.DisplayAssetPath2 == "" { - continue - } - - entry := newCosmeticOfferFromFortniteitem(item, true) - entry.Meta.Category = set.BackendName - entry.Meta.SectionId = "Featured" - weekly.Offers = append(weekly.Offers, entry) - } - } - catalog.AddSection(weekly) - - if aid.Config.Fortnite.EnableVBucks { - smallCurrencyOffer := newCurrencyOfferFromName("Small Currency Pack", 1000, 0) - smallCurrencyOffer.Meta.IconSize = "XSmall" - smallCurrencyOffer.Meta.CurrencyAnalyticsName = "MtxPack1000" - catalog.AddMoneyOffer(smallCurrencyOffer) - - mediumCurrencyOffer := newCurrencyOfferFromName("Medium Currency Pack", 2000, 800) - mediumCurrencyOffer.Meta.IconSize = "Small" - mediumCurrencyOffer.Meta.CurrencyAnalyticsName = "MtxPack2800" - mediumCurrencyOffer.Meta.BannerOverride = "12PercentExtra" - catalog.AddMoneyOffer(mediumCurrencyOffer) - - intermediateCurrencyOffer := newCurrencyOfferFromName("Intermediate Currency Pack", 6000, 1500) - intermediateCurrencyOffer.Meta.IconSize = "Medium" - intermediateCurrencyOffer.Meta.CurrencyAnalyticsName = "MtxPack7500" - intermediateCurrencyOffer.Meta.BannerOverride = "25PercentExtra" - catalog.AddMoneyOffer(intermediateCurrencyOffer) - - jumboCurrencyOffer := newCurrencyOfferFromName("Jumbo Currency Pack", 10000, 3500) - jumboCurrencyOffer.Meta.IconSize = "XLarge" - jumboCurrencyOffer.Meta.CurrencyAnalyticsName = "MtxPack13500" - jumboCurrencyOffer.Meta.BannerOverride = "35PercentExtra" - catalog.AddMoneyOffer(jumboCurrencyOffer) - - rogueAgentStarterPack := newStarterPackOfferFromName("The Rogue Agent Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_090_Athena_Commando_M_Tactical", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_030_TacticalRogue", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - rogueAgentStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_090_Athena_Commando_M_Tactical.DA_Featured_CID_090_Athena_Commando_M_Tactical" - rogueAgentStarterPack.SeasonsAllowed = []int{4} - catalog.AddStarterPack(rogueAgentStarterPack) - - wingmanStarterPack := newStarterPackOfferFromName("The Wingman Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_139_Athena_Commando_M_FighterPilot", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_056_FighterPilot", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - wingmanStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_139_Athena_Commando_M_FighterPilot.DA_Featured_CID_139_Athena_Commando_M_FighterPilot" - wingmanStarterPack.SeasonsAllowed = []int{4, 5} - catalog.AddStarterPack(wingmanStarterPack) - - aceStarterPack := newStarterPackOfferFromName("The Ace Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_195_Athena_Commando_F_Bling", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_101_BlingFemale", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - aceStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_195_Athena_Commando_F_Bling.DA_Featured_CID_195_Athena_Commando_F_Bling" - aceStarterPack.SeasonsAllowed = []int{5, 6} - catalog.AddStarterPack(aceStarterPack) - - summitStarterPack := newStarterPackOfferFromName("The Summit Striker Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_253_Athena_Commando_M_MilitaryFashion2", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_134_MilitaryFashion", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - summitStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_253_Athena_Commando_M_MilitaryFashion2.DA_Featured_CID_253_Athena_Commando_M_MilitaryFashion2" - summitStarterPack.SeasonsAllowed = []int{6, 7} - catalog.AddStarterPack(summitStarterPack) - - cobaltStarterPack := newStarterPackOfferFromName("The Cobalt Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_327_Athena_Commando_M_BlueMystery", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_203_BlueMystery", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - cobaltStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_327_Athena_Commando_M_BlueMystery.DA_Featured_CID_327_Athena_Commando_M_BlueMystery" - cobaltStarterPack.SeasonsAllowed = []int{7} - catalog.AddStarterPack(cobaltStarterPack) - - lagunaStarterPack := newStarterPackOfferFromName("The Laguna Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_367_Athena_Commando_F_Tropical", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_231_TropicalFemale", 1), - NewFortniteCatalogStarterPackGrant("AthenaItemWrap:Wrap_033_TropicalGirl", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - lagunaStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_367_Athena_Commando_F_Tropical.DA_Featured_CID_367_Athena_Commando_F_Tropical" - lagunaStarterPack.SeasonsAllowed = []int{8} - catalog.AddStarterPack(lagunaStarterPack) - - wildeStarterPack := newStarterPackOfferFromName("The Wilde Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_420_Athena_Commando_F_WhiteTiger", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_277_WhiteTiger", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - wildeStarterPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_420_Athena_Commando_F_WhiteTiger.DA_Featured_CID_420_Athena_Commando_F_WhiteTiger" - wildeStarterPack.SeasonsAllowed = []int{9} - catalog.AddStarterPack(wildeStarterPack) - - redStrikePack := newStarterPackOfferFromName("The Red Strike Pack", 499, []*FortniteCatalogStarterPackGrant{ - NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_384_Athena_Commando_M_StreetAssassin", 1), - NewFortniteCatalogStarterPackGrant("AthenaBackpack:BID_247_StreetAssassin", 1), - NewFortniteCatalogStarterPackGrant("Currency:MtxPurchased", 600), - }...) - redStrikePack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_384_Athena_Commando_M_StreetAssasin.DA_Featured_CID_384_Athena_Commando_M_StreetAssasin" - redStrikePack.SeasonsAllowed = []int{10} - catalog.AddStarterPack(redStrikePack) - - // Below is an example of a custom starter pack - // Uncomment to use. - // snowCustomPack := newStarterPackOfferFromName("Snow Gift", 0, []*FortniteCatalogStarterPackGrant{ - // NewFortniteCatalogStarterPackGrant("AthenaCharacter:CID_384_Athena_Commando_M_StreetAssassin", 1), - // }...) - // snowCustomPack.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_TBD_Athena_Commando_M_RaptorArcticCamo_Bundle.DA_Featured_CID_TBD_Athena_Commando_M_RaptorArcticCamo_Bundle" - // snowCustomPack.SeasonsAllowed = []int{1,2,3,4,5,6,7,8,9,10} - // snowCustomPack.Meta.OriginalOffer = 1000 - // snowCustomPack.Meta.ExtraBonus = 500 - // snowCustomPack.Description = "" - // snowCustomPack.LongDescription = "Thank you for using Snow! Here's a special offer for you!" - // catalog.AddStarterPack(snowCustomPack) - } - return catalog -} - -func newCosmeticOfferFromFortniteitem(fortniteItem *FortniteItem, addAssets bool) *FortniteCatalogCosmeticOffer { - 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 newCurrencyOfferFromName(name string, original, bonus int) *FortniteCatalogCurrencyOffer { - formattedPrice := aid.FormatNumber(original + bonus) - offer := NewFortniteCatalogCurrencyOffer(original, bonus) - offer.Meta.IconSize = "Small" - offer.Meta.CurrencyAnalyticsName = name - offer.DevName = name - offer.Title = formattedPrice + " V-Bucks" - offer.Description = "Buy " + formattedPrice + " Fortnite V-Bucks, the in-game currency that can be spent in Fortnite Battle Royale and Creative modes. You can purchase new customization items like Outfits, Gliders, Pickaxes, Emotes, Wraps and the latest season's Battle Pass! Gliders and Contrails may not be used in Save the World mode." - offer.LongDescription = "Buy " + formattedPrice + " Fortnite V-Bucks, the in-game currency that can be spent in Fortnite Battle Royale and Creative modes. You can purchase new customization items like Outfits, Gliders, Pickaxes, Emotes, Wraps and the latest season's Battle Pass! Gliders and Contrails may not be used in Save the World mode.\n\nAll V-Bucks purchased on the Epic Games Store are not redeemable or usable on Nintendo Switch™." - - return offer -} - -func newStarterPackOfferFromName(name string, totalPrice int, grants ...*FortniteCatalogStarterPackGrant) *FortniteCatalogStarterPack { - mainString := "Jump into Fortnite Battle Royale with the " + strings.ReplaceAll(name, "The ", "") + ". Includes:\n\n- 600 V-Bucks" - - for _, grant := range grants { - fortniteItem := DataClient.FortniteItems[strings.Split(grant.TemplateID, ":")[1]] - if fortniteItem != nil { - mainString += "\n- " + fortniteItem.Name + " " + fortniteItem.Type.DisplayValue + " - Battle Royale Only" - } - } - - offer := NewFortniteCatalogStarterPack(totalPrice) - offer.DevName = name + "StarterPack" - offer.Title = name - offer.Description = mainString - offer.LongDescription = mainString + "\n\nV-Bucks are an in-game currency that can be spent in both the Battle Royale PvP mode and the Save the World PvE campaign. In Battle Royale, you can use V-bucks to purchase new customization items like outfits, emotes, pickaxes, gliders, and more! In Save the World you can purchase Llama Pinata card packs that contain weapon, trap and gadget schematics as well as new Heroes and more! \n\nNote: Items do not transfer between the Battle Royale mode and the Save the World campaign." - offer.Meta.OriginalOffer = 500 - offer.Meta.ExtraBonus = 100 - - for _, grant := range grants { - offer.AddGrant(grant) - } - - return offer -} \ No newline at end of file diff --git a/fortnite/types.go b/fortnite/types.go deleted file mode 100644 index ceee608..0000000 --- a/fortnite/types.go +++ /dev/null @@ -1,100 +0,0 @@ -package fortnite - -type FortniteVariantChannel struct { - Tag string `json:"tag"` - Name string `json:"name"` - Image string `json:"image"` -} - -type FortniteVariant struct { - Channel string `json:"channel"` - Type string `json:"type"` - Options []FortniteVariantChannel `json:"options"` -} - -type FortniteItem 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 []FortniteVariant `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"` - DisplayAssetPath2 string - ItemPreviewHeroPath string `json:"itemPreviewHeroPath"` - Backpack *FortniteItem `json:"backpack"` - Path string `json:"path"` - Added string `json:"added"` - ShopHistory []string `json:"shopHistory"` - BattlePass bool `json:"battlePass"` -} - -type FortniteSet struct { - BackendName string `json:"backendName"` - DisplayName string `json:"displayName"` - Items []*FortniteItem `json:"items"` -} - -type FortniteCosmeticsResponse struct { - Status int `json:"status"` - Data []FortniteItem `json:"data"` -} - -type SnowCosmeticVariantToken struct { - Grants []struct { - Channel string `json:"channel"` - Value string `json:"value"` - } `json:"grants"` - Item string `json:"item"` - Name string `json:"name"` - Gift bool `json:"gift"` - Equip bool `json:"equip"` - Unseen bool `json:"unseen"` -} - -type FortniteVariantToken struct { - Grants []struct { - Channel string `json:"channel"` - Value string `json:"value"` - } `json:"grants"` - Item *FortniteItem `json:"item"` - Name string `json:"name"` - Gift bool `json:"gift"` - Equip bool `json:"equip"` - Unseen bool `json:"unseen"` -} \ No newline at end of file diff --git a/fortnite/typings.go b/fortnite/typings.go new file mode 100644 index 0000000..741ad62 --- /dev/null +++ b/fortnite/typings.go @@ -0,0 +1,159 @@ +package fortnite + +import ( + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/person" +) + +type APICosmeticDefinitionVariant struct { + Tag string `json:"tag"` + Name string `json:"name"` + Image string `json:"image"` +} + +type APICosmeticDefinitionVariantChannel struct { + Channel string `json:"channel"` + Type string `json:"type"` + Options []APICosmeticDefinitionVariant `json:"options"` +} + +type APICosmeticDefinition 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 []APICosmeticDefinitionVariantChannel `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"` + NewDisplayAssetPath string + ItemPreviewHeroPath string `json:"itemPreviewHeroPath"` + BackpackDefinition *APICosmeticDefinition `json:"backpack"` + Path string `json:"path"` + Added string `json:"added"` + ShopHistory []string `json:"shopHistory"` + BattlePass bool `json:"battlePass"` +} + +type APISetDefinition struct { + BackendName string `json:"backendName"` + DisplayName string `json:"displayName"` + Items []*APICosmeticDefinition `json:"items"` +} + +type APICosmeticsResponse struct { + Status int `json:"status"` + Data []APICosmeticDefinition `json:"data"` +} + +type SnowCosmeticVariantDefinition struct { + Grants []struct { + Channel string `json:"channel"` + Value string `json:"value"` + } `json:"grants"` + Item string `json:"item"` + Name string `json:"name"` + Gift bool `json:"gift"` + Equip bool `json:"equip"` + Unseen bool `json:"unseen"` +} + +type FortniteVariantToken struct { + Grants []struct { + Channel string `json:"channel"` + Value string `json:"value"` + } `json:"grants"` + Item *APICosmeticDefinition `json:"item"` + Name string `json:"name"` + Gift bool `json:"gift"` + Equip bool `json:"equip"` + Unseen bool `json:"unseen"` +} + +type ItemGrant struct { + TemplateID string + Quantity int + ProfileType string +} + +func NewItemGrant(templateId string, quantity int) *ItemGrant { + return &ItemGrant{ + TemplateID: templateId, + Quantity: quantity, + } +} + +type LootResultLoot struct { + TemplateID string + ItemID string + Quantity int + ItemProfileType string +} + +type LootResult struct { + Items []*LootResultLoot +} + +func NewLootResult() *LootResult { + return &LootResult{ + Items: make([]*LootResultLoot, 0), + } +} + +func (l *LootResult) AddItem(i *person.Item) { + l.Items = append(l.Items, &LootResultLoot{ + TemplateID: i.TemplateID, + ItemID: i.ID, + Quantity: i.Quantity, + ItemProfileType: i.ProfileType, + }) +} + +func (l *LootResult) GenerateFortniteLootResultEntry() []aid.JSON { + loot := []aid.JSON{} + + for _, item := range l.Items { + loot = append(loot, aid.JSON{ + "itemType": item.TemplateID, + "itemGuid": item.ItemID, + "itemProfile": item.ItemProfileType, + "quantity": item.Quantity, + }) + } + + return loot +} \ No newline at end of file diff --git a/go.sum b/go.sum index 51e2805..ed9552d 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/karrick/tparse v2.4.2+incompatible h1:+cW306qKAzrASC5XieHkgN7/vPaGKIuK62Q7nI7DIRc= -github.com/karrick/tparse v2.4.2+incompatible/go.mod h1:ASPA+vrIcN1uEW6BZg8vfWbzm69ODPSYZPU6qJyfdK0= github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -98,8 +96,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/handlers/auth.go b/handlers/auth.go index 0f162bc..770f4f3 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -100,7 +100,7 @@ func PostTokenExchangeCode(c *fiber.Ctx, body *FortniteTokenBody) error { return c.Status(400).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) } - if expire.Add(time.Hour).Before(time.Now()) { + if expire.Add(time.Minute).Before(time.Now()) { return c.Status(400).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) } diff --git a/handlers/client.go b/handlers/client.go index c057c4e..1abeea8 100644 --- a/handlers/client.go +++ b/handlers/client.go @@ -2,13 +2,13 @@ package handlers import ( "fmt" - "strconv" "strings" "time" "github.com/ectrc/snow/aid" "github.com/ectrc/snow/fortnite" p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/shop" "github.com/ectrc/snow/socket" "github.com/gofiber/fiber/v2" @@ -33,6 +33,11 @@ var ( "RemoveGiftBox": clientRemoveGiftBoxAction, "SetAffiliateName": clientSetAffiliateNameAction, "SetReceiveGiftsEnabled": clientSetReceiveGiftsEnabledAction, + "VerifyRealMoneyPurchase": clientVerifyRealMoneyPurchaseAction, + } + + repeatingActions = []func(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error{ + clientCalculateTierAndLevel, } ) @@ -66,6 +71,12 @@ func PostClientProfileAction(c *fiber.Ctx) error { } } + for _, action := range repeatingActions { + if err := action(c, person, profile, ¬ifications); err != nil { + return c.Status(400).JSON(aid.ErrorBadRequest(err.Error())) + } + } + for key, profileSnapshot := range profileSnapshots { profile := person.GetProfileFromType(key) if profile == nil { @@ -78,13 +89,8 @@ func PostClientProfileAction(c *fiber.Ctx) error { profile.Diff(profileSnapshot) } - - revision, _ := strconv.Atoi(c.Query("rvn")) - if revision == -1 { - revision = profile.Revision - } - revision++ - profile.Revision = revision + + profile.Revision = aid.Ternary[int](c.QueryInt("rvn") == -1, profile.Revision, c.QueryInt("rvn"))+1 go profile.Save() delete(profileSnapshots, profile.Type) @@ -94,11 +100,11 @@ func PostClientProfileAction(c *fiber.Ctx) error { if profile == nil { continue } - profile.Revision++ - + if len(profile.Changes) == 0 { continue } + profile.Revision++ multiUpdate = append(multiUpdate, aid.JSON{ "profileId": profile.Type, @@ -126,11 +132,32 @@ func PostClientProfileAction(c *fiber.Ctx) error { } func clientQueryProfileAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + accountLevel := 0 + person.AllSeasonsStats.Range(func(key string, value *p.SeasonStats) bool { + accountLevel += fortnite.DataClient.SnowSeason.GetSeasonLevel(value) + return true + }) + profile.CreateFullProfileUpdateChange() return nil } func clientClientQuestLoginAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + // _g := p.NewGift("GiftBox:GB_FortnitemaresChallenges", 1, "", "") + // _g.AddLoot(p.NewItemWithType("Token:FoundersPackDailyRewardToken", 1, "common_core")) + // _g.AddLoot(p.NewItemWithType("Token:MysteryToken", 1, "common_core")) + // _g.AddLoot(p.NewItemWithType("Token:AccountInventoryBonus", 1, "common_core")) + // _g.AddLoot(p.NewItemWithType("Token:DeniedOrDisabledCosmeticPlaceholderToken", 1, "athena")) + // _g.AddLoot(p.NewItemWithType("Token:WorldInventoryBonus", 23, "athena")) + // _g.AddLoot(p.NewItemWithType("Token:CTF_Dom_Key", 1, "common_core")) + // _g.AddLoot(p.NewItemWithType("FortIngredient:Ingredient_Crystal_ShadowShard", 32, "common_core")) + // _g.AddLoot(p.NewItemWithType("Ingredient:Ingredient_Crystal_ShadowShard", 4232, "common_core")) + // _g.AddLoot(p.NewItemWithType("AccountResource:AthenaBattleStar", 3, "common_core")) + // _g.AddLoot(p.NewItemWithType("AccountResource:AthenaSeasonalXP", 2, "common_core")) + // _g.AddLoot(p.NewItemWithType("HomebaseNode:QuestReward_BuildingUpgradeLevel2", 5, "common_core")) + // _g.AddLoot(p.NewItemWithType("FortHomebaseNode:QuestReward_BuildingUpgradeLevel2", 8, "common_core")) + // _g.AddLoot(p.NewItemWithType("Token:NeighborhoodCurrency", 2, "common_core")) + // person.CommonCoreProfile.Gifts.AddGift(_g).Save() return nil } @@ -601,66 +628,73 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p return fmt.Errorf("invalid Body") } - shop := fortnite.NewRandomFortniteCatalog() - offer := shop.FindCosmeticOfferById(body.OfferID) - if offer == nil { + storefront := shop.GetShop() + offerRaw, type_ := storefront.GetOfferByID(body.OfferID) + if offerRaw == nil { return fmt.Errorf("offer not found") } - if offer.TotalPrice != body.ExpectedTotalPrice { - return fmt.Errorf("invalid price") + switch type_ { + case shop.StorefrontCatalogOfferEnumItem: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeItem) + if (offer.Price.FinalPrice * body.PurchaseQuantity) != body.ExpectedTotalPrice { + return fmt.Errorf("invalid price") + } + case shop.StorefrontCatalogOfferEnumBattlePass: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeBattlePass) + if (offer.Price.FinalPrice * body.PurchaseQuantity) != body.ExpectedTotalPrice { + return fmt.Errorf("invalid price") + } + default: + return fmt.Errorf("invalid offer type") } - vbucks := profile.Items.GetItemByTemplateID("Currency:MtxPurchased") - if vbucks == nil { - return fmt.Errorf("vbucks not found") + purchaseLookup := map[shop.StorefrontCatalogOfferEnum]func(quantity int, offerRaw interface{}, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error{ + shop.StorefrontCatalogOfferEnumItem: clientPurchaseCatalogItemEntryAction, + shop.StorefrontCatalogOfferEnumBattlePass: clientPurchaseCatalogBattlePassEntryAction, } - profile0Vbucks := person.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased") - if profile0Vbucks == nil { - return fmt.Errorf("profile0vbucks not found") + if purchaseFunc, ok := purchaseLookup[type_]; ok { + return purchaseFunc(body.PurchaseQuantity, offerRaw, person, profile, notifications) } - if vbucks.Quantity < body.ExpectedTotalPrice { - return fmt.Errorf("not enough vbucks") - } + return nil +} - vbucks.Quantity -= body.ExpectedTotalPrice - profile0Vbucks.Quantity = vbucks.Quantity - vbucks.Save() - profile0Vbucks.Save() - - if offer.Meta.ProfileId != "athena" { - return fmt.Errorf("save the world not implemeted yet") +func clientPurchaseCatalogItemEntryAction(quantity int, offerRaw interface{}, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeItem) + for _, grant := range offer.Rewards { + if grant.ProfileType != shop.ShopGrantProfileTypeAthena { + return fmt.Errorf("save the world not implemeted yet") + } } + person.TakeAndSyncVbucks(offer.Price.FinalPrice * quantity) loot := []aid.JSON{} - purchase := p.NewPurchase(body.OfferID, body.ExpectedTotalPrice) - for i := 0; i < body.PurchaseQuantity; i++ { - for _, grant := range offer.Grants { - templateId := grant.Type.BackendValue + ":" + grant.ID - if profile.Items.GetItemByTemplateID(templateId) != nil { - item := profile.Items.GetItemByTemplateID(templateId) - item.Quantity++ - go item.Save() + purchase := p.NewPurchase(offer.OfferID, offer.Price.FinalPrice) - continue - } - - item := p.NewItem(templateId, 1) - person.AthenaProfile.Items.AddItem(item) - purchase.AddLoot(item) - - loot = append(loot, aid.JSON{ - "itemType": item.TemplateID, - "itemGuid": item.ID, - "quantity": item.Quantity, - "itemProfile": offer.Meta.ProfileId, - }) + groupedRewards := map[string]int{} + for i := 0; i < quantity; i++ { + for _, grant := range offer.Rewards { + groupedRewards[grant.TemplateID] += grant.Quantity } } - person.AthenaProfile.Purchases.AddPurchase(purchase).Save() + for templateID, quantity := range groupedRewards { + r, err := fortnite.GrantToPerson(person, fortnite.NewItemGrant(templateID, quantity)) + if err != nil { + continue + } + + loot = append(loot, r.GenerateFortniteLootResultEntry()...) + } + + for _, item := range loot { + purchaseItem := p.NewItem(item["itemType"].(string), 1) + purchaseItem.ID = item["itemGuid"].(string) + purchaseItem.ProfileType = item["itemProfile"].(string) + purchase.AddLoot(purchaseItem) + } *notifications = append(*notifications, aid.JSON{ "type": "CatalogPurchase", @@ -669,21 +703,56 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p }, "primary": true, }) + person.AthenaProfile.Purchases.AddPurchase(purchase).Save() affiliate := person.CommonCoreProfile.Attributes.GetAttributeByKey("mtx_affiliate") if affiliate == nil { - return c.Status(400).JSON(aid.ErrorBadRequest("Invalid affiliate attribute")) + return nil } creator := p.Find(p.AttributeConvert[string](affiliate)) if creator != nil { - creator.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += body.ExpectedTotalPrice - creator.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += body.ExpectedTotalPrice + creator.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += int(float64(offer.Price.FinalPrice) * 0.10) + creator.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += int(float64(offer.Price.FinalPrice) * 0.10) } return nil } +func clientPurchaseCatalogBattlePassEntryAction(quantity int, offerRaw interface{}, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeBattlePass) + person.TakeAndSyncVbucks(offer.Price.FinalPrice * quantity) + + groupedRewards := map[string]int{} + for i := 0; i < quantity; i++ { + for _, grant := range offer.Rewards { + groupedRewards[grant.TemplateID] += grant.Quantity + } + } + + for templateID, quantity := range groupedRewards { + _, err := fortnite.GrantToPerson(person, fortnite.NewItemGrant(templateID, quantity)) + if err != nil { + continue + } + } + + receipt := p.NewReceipt(offer.GetOfferID(), 0) + receipt.SetState("OK") + person.Receipts.AddReceipt(receipt).Save() + affiliate := person.CommonCoreProfile.Attributes.GetAttributeByKey("mtx_affiliate") + if affiliate == nil { + return nil + } + + creator := p.Find(p.AttributeConvert[string](affiliate)) + if creator != nil { + creator.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += int(float64(offer.Price.FinalPrice) * 0.10) + creator.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased").Quantity += int(float64(offer.Price.FinalPrice) * 0.10) + } + return nil +} + func clientRefundMtxPurchaseAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { PurchaseID string `json:"purchaseId" binding:"required"` @@ -703,35 +772,18 @@ func clientRefundMtxPurchaseAction(c *fiber.Ctx, person *p.Person, profile *p.Pr } person.RefundTickets-- - for _, item := range purchase.Loot { - person.GetProfileFromType(item.ProfileType).Items.DeleteItem(item.ID) - person.GetProfileFromType(item.ProfileType).CreateItemRemovedChange(item.ID) + for _, lootItem := range purchase.Loot { + person.GetProfileFromType(lootItem.ProfileType).Items.DeleteItem(lootItem.ID) + person.GetProfileFromType(lootItem.ProfileType).CreateItemRemovedChange(lootItem.ID) } - - purchase.RefundedAt = time.Now() - purchase.Save() - - vbucks := profile.Items.GetItemByTemplateID("Currency:MtxPurchased") - if vbucks == nil { - return fmt.Errorf("vbucks not found") - } - - vbucks.Quantity += purchase.TotalPaid - vbucks.Save() - - profile0Vbucks := person.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased") - if profile0Vbucks == nil { - return fmt.Errorf("profile0vbucks not found") - } - - profile0Vbucks.Quantity = vbucks.Quantity - profile0Vbucks.Save() + person.GiveAndSyncVbucks(purchase.TotalPaid) return nil } func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { + OfferID string `json:"offerId" binding:"required"` Currency string `json:"currency" binding:"required"` CurrencySubType string `json:"currencySubType" binding:"required"` ExpectedTotalPrice int `json:"expectedTotalPrice" binding:"required"` @@ -739,20 +791,23 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro GiftWrapTemplateId string `json:"giftWrapTemplateId" binding:"required"` PersonalMessage string `json:"personalMessage" binding:"required"` ReceiverAccountIds []string `json:"receiverAccountIds" binding:"required"` - OfferId string `json:"offerId" binding:"required"` } if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } - shop := fortnite.NewRandomFortniteCatalog() - offer := shop.FindCosmeticOfferById(body.OfferId) - if offer == nil { + storefront := shop.GetShop() + offerRaw, type_ := storefront.GetOfferByID(body.OfferID) + if offerRaw == nil { return fmt.Errorf("offer not found") } + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeItem) + if type_ != shop.StorefrontCatalogOfferEnumItem { + return fmt.Errorf("invalid offer type") + } - if offer.TotalPrice != body.ExpectedTotalPrice { + if offer.Price.FinalPrice != body.ExpectedTotalPrice { return fmt.Errorf("invalid price") } @@ -762,40 +817,22 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro return fmt.Errorf("one or more receivers not found") } - for _, grant := range offer.Grants { - if receiverPerson.AthenaProfile.Items.GetItemByTemplateID(grant.Type.BackendValue + ":" + grant.ID) != nil { + for _, grant := range offer.Rewards { + if receiverPerson.AthenaProfile.Items.GetItemByTemplateID(grant.TemplateID) != nil { return fmt.Errorf("one or more receivers has one of the items") } } } - price := offer.TotalPrice * len(body.ReceiverAccountIds) - - 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 < price { - return fmt.Errorf("not enough vbucks") - } - - vbucks.Quantity -= price - profile0Vbucks.Quantity = price - vbucks.Save() - profile0Vbucks.Save() + price := offer.Price.FinalPrice * len(body.ReceiverAccountIds) + person.TakeAndSyncVbucks(price) for _, receiverAccountId := range body.ReceiverAccountIds { receiverPerson := p.Find(receiverAccountId) gift := p.NewGift(body.GiftWrapTemplateId, 1, person.ID, body.PersonalMessage) - for _, grant := range offer.Grants { - item := p.NewItem(grant.Type.BackendValue + ":" + grant.ID, 1) - item.ProfileType = offer.Meta.ProfileId + for _, grant := range offer.Rewards { + item := p.NewItem(grant.TemplateID, grant.Quantity) + item.ProfileType = string(grant.ProfileType) gift.AddLoot(item) } @@ -808,7 +845,7 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro func clientRemoveGiftBoxAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { var body struct { - GiftBoxItemId string `json:"giftBoxItemId" binding:"required"` + GiftBoxItemId string `json:"giftBoxItemId" binding:"required"` } if err := c.BodyParser(&body); err != nil { @@ -820,13 +857,27 @@ func clientRemoveGiftBoxAction(c *fiber.Ctx, person *p.Person, profile *p.Profil return fmt.Errorf("gift not found") } - aid.Print(gift.TemplateID) - + loot := []aid.JSON{} for _, item := range gift.Loot { - person.GetProfileFromType(item.ProfileType).Items.AddItem(item).Save() + result, err := fortnite.GrantToPerson(person, fortnite.NewItemGrant(item.TemplateID, item.Quantity)) + if err != nil { + continue + } + + loot = append(loot, result.GenerateFortniteLootResultEntry()...) + item.DeleteLoot() } person.CommonCoreProfile.Gifts.DeleteGift(gift.ID) + + *notifications = append(*notifications, aid.JSON{ + "type": "CatalogPurchase", + "lootResult": aid.JSON{ + "items": loot, + }, + "primary": true, + }) + return nil } @@ -867,13 +918,63 @@ func clientSetReceiveGiftsEnabledAction(c *fiber.Ctx, person *p.Person, profile return fmt.Errorf("invalid Body") } - attribute := profile.Attributes.GetAttributeByKey("allowed_to_receive_gifts") - if attribute == nil { - return fmt.Errorf("attribute not found") + profile.Attributes.GetAttributeByKey("allowed_to_receive_gifts").SetValue(body.ReceiveGifts).Save() + return nil +} + +func clientVerifyRealMoneyPurchaseAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + AppStore string `json:"appStore" binding:"required"` + AppStoreId string `json:"appStoreId" binding:"required"` + PurchaseCorrelationId string `json:"purchaseCorrelationId" binding:"required"` + ReceiptId string `json:"receiptId" binding:"required"` + ReceiptInfo string `json:"receiptInfo" binding:"required"` } - attribute.ValueJSON = aid.JSONStringify(body.ReceiveGifts) - go attribute.Save() + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + receipt := person.Receipts.GetReceipt(body.ReceiptId) + if receipt == nil { + return fmt.Errorf("receipt does not exist") + } + + if receipt.OfferID != body.AppStoreId { + return fmt.Errorf("receipt does not match offer") + } + + gift := p.NewGift("GiftBox:GB_MakeGood", 1, "", "Thank you for your purchase!") + for _, grant := range receipt.Loot { + item := p.NewItem(grant.TemplateID, grant.Quantity) + item.ProfileType = grant.ProfileType + gift.AddLoot(item) + } + + person.CommonCoreProfile.Gifts.AddGift(gift).Save() + person.SetInAppPurchasesAttribute() + person.SyncVBucks("common_core") + receipt.SetState("OK") + receipt.Save() + return nil +} + +func clientCalculateTierAndLevel(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + for { + tierChanged := fortnite.DataClient.SnowSeason.GrantUnredeemedBookRewards(person, "GB_BattlePass") + levelChanged := fortnite.DataClient.SnowSeason.GrantUnredeemedLevelRewards(person) + + if !tierChanged && !levelChanged { + break + } + } + + person.AthenaProfile.Attributes.GetAttributeByKey("season_num").SetValue(person.CurrentSeasonStats.Season).Save() + person.AthenaProfile.Attributes.GetAttributeByKey("level").SetValue(fortnite.DataClient.SnowSeason.GetSeasonLevel(person.CurrentSeasonStats)).Save() + person.AthenaProfile.Attributes.GetAttributeByKey("xp").SetValue(fortnite.DataClient.SnowSeason.GetRelativeSeasonXP(person.CurrentSeasonStats)).Save() + person.AthenaProfile.Attributes.GetAttributeByKey("book_purchased").SetValue(person.CurrentSeasonStats.BookPurchased).Save() + person.AthenaProfile.Attributes.GetAttributeByKey("book_level").SetValue(fortnite.DataClient.SnowSeason.GetBookLevel(person.CurrentSeasonStats)).Save() + person.AthenaProfile.Attributes.GetAttributeByKey("book_xp").SetValue(fortnite.DataClient.SnowSeason.GetRelativeBookXP(person.CurrentSeasonStats)).Save() return nil } \ No newline at end of file diff --git a/handlers/common.go b/handlers/common.go index 8744a53..13cd8cf 100644 --- a/handlers/common.go +++ b/handlers/common.go @@ -3,6 +3,7 @@ package handlers import ( "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/storage" "github.com/gofiber/fiber/v2" ) @@ -27,7 +28,19 @@ func PostGameAccess(c *fiber.Ctx) error { } func GetFortniteReceipts(c *fiber.Ctx) error { - return c.Status(200).JSON([]string{}) + person := c.Locals("person").(*p.Person) + receipts := []aid.JSON{} + + person.Receipts.RangeReceipts(func(key string, value *p.Receipt) bool { + if value.State == "OK" { + return true + } + + receipts = append(receipts, value.GenerateFortniteReceiptEntry()) + return true + }) + + return c.Status(200).JSON(receipts) } func GetMatchmakingSession(c *fiber.Ctx) error { @@ -69,4 +82,15 @@ func GetRegion(c *fiber.Ctx) error { }, "subdivisions": []aid.JSON{}, }) +} + +func SendJSONResponseFromAsset(c *fiber.Ctx, asset string) error { + bytes := storage.Asset(asset) + if bytes == nil { + return c.Status(404).JSON(aid.JSON{}) + } + + stringBytes := string(*bytes) + c.Set("Content-Type", "application/json") + return c.Status(200).SendString(stringBytes) } \ No newline at end of file diff --git a/handlers/discord.go b/handlers/discord.go index 503225d..76fdcf1 100644 --- a/handlers/discord.go +++ b/handlers/discord.go @@ -16,7 +16,7 @@ import ( func GetDiscordOAuthURL(c *fiber.Ctx) error { code := c.Query("code") if code == "" { - return c.Status(200).SendString("https://discord.com/oauth2/authorize?client_id="+ aid.Config.Discord.ID +"&redirect_uri="+ url.QueryEscape("http://" + aid.Config.API.Host + aid.Config.API.Port +"/snow/discord") + "&response_type=code&scope=identify") + return c.Status(200).SendString("https://discord.com/oauth2/authorize?client_id="+ aid.Config.Discord.ID +"&redirect_uri="+ url.QueryEscape(aid.Config.Discord.CallbackURL +"/snow/discord") + "&response_type=code&scope=identify") } client := &http.Client{} @@ -26,7 +26,7 @@ func GetDiscordOAuthURL(c *fiber.Ctx) error { "client_secret": {aid.Config.Discord.Secret}, "grant_type": {"authorization_code"}, "code": {code}, - "redirect_uri": {"http://" + aid.Config.API.Host + aid.Config.API.Port +"/snow/discord"}, + "redirect_uri": {aid.Config.Discord.CallbackURL +"/snow/discord"}, }) if err != nil { return c.Status(500).JSON(aid.JSON{"error":err.Error()}) diff --git a/handlers/discovery.go b/handlers/discovery.go index e3d312a..01c2236 100644 --- a/handlers/discovery.go +++ b/handlers/discovery.go @@ -7,157 +7,36 @@ import ( "github.com/gofiber/fiber/v2" ) -func createContentPanel(title string, id string) aid.JSON { - return aid.JSON{ - "NumPages": 1, - "AnalyticsId": id, - "PanelType": "AnalyticsList", - "AnalyticsListName": title, - "CuratedListOfLinkCodes": []aid.JSON{}, - "ModelName": "", - "PageSize": 7, - "PlatformBlacklist": []aid.JSON{}, - "PanelName": id, - "MetricInterval": "", - "SkippedEntriesCount": 0, - "SkippedEntriesPercent": 0, - "SplicedEntries": []aid.JSON{}, - "PlatformWhitelist": []aid.JSON{}, - "EntrySkippingMethod": "None", - "PanelDisplayName": aid.JSON{ - "Category": "Game", - "NativeCulture": "", - "Namespace": "CreativeDiscoverySurface_Frontend", - "LocalizedStrings": []aid.JSON{{ - "key": "en", - "value": title, - }}, - "bIsMinimalPatch": false, - "NativeString": title, - "Key": "", - }, - "PlayHistoryType": "RecentlyPlayed", - "bLowestToHighest": false, - "PanelLinkCodeBlacklist": []aid.JSON{}, - "PanelLinkCodeWhitelist": []aid.JSON{}, - "FeatureTags": []aid.JSON{}, - "MetricName": "", - } -} - -func createPlaylist(mnemonic string, image string) aid.JSON { - return aid.JSON{ - "linkData": aid.JSON{ - "namespace": "fn", - "mnemonic": mnemonic, - "linkType": "BR:Playlist", - "active": true, - "disabled": false, - "version": 1, - "moderationStatus": "Unmoderated", - "accountId": "epic", - "creatorName": "Epic", - "descriptionTags": []string{}, - "metadata": aid.JSON{ - "image_url": image, - "matchmaking": aid.JSON{ - "override_playlist": mnemonic, - }, - }, - }, - "lastVisited": nil, - "linkCode": mnemonic, - "isFavorite": false, - } -} - -func PostDiscovery(c *fiber.Ctx) error { - results := []aid.JSON{} - results = append(results, createPlaylist("Playlist_DefaultSolo", "https://bucket.retrac.site/55737fa15677cd57fab9e7f4499d62f89cfde320.png")) - - return c.Status(200).JSON(aid.JSON{ - "Panels": []aid.JSON{ - { - "PanelName": "1", - "Pages": []aid.JSON{{ - "results": results, - "hasMore": false, - }}, - }, - }, - "TestCohorts": []string{ - "playlists", - }, - "ModeSets": aid.JSON{}, - }) -} - -func PostAssets(c *fiber.Ctx) error { - var body struct { - DAD_CosmeticItemUserOptions int `json:"DAD_CosmeticItemUserOptions"` - FortCreativeDiscoverySurface int `json:"FortCreativeDiscoverySurface"` - FortPlaylistAthena int `json:"FortPlaylistAthena"` - } - - err := c.BodyParser(&body) - if err != nil { - return c.Status(400).JSON(aid.JSON{"error":err.Error()}) - } - - testCohort := aid.JSON{ - "AnalyticsId": "0", - "CohortSelector": "PlayerDeterministic", - "PlatformBlacklist": []aid.JSON{}, - "ContentPanels": []aid.JSON{ - createContentPanel("Featured", "1"), - }, - "PlatformWhitelist": []aid.JSON{}, - "SelectionChance": 0.1, - "TestName": "playlists", - } - - return c.Status(200).JSON(aid.JSON{ - "FortCreativeDiscoverySurface": aid.JSON{ - "meta": aid.JSON{ - "promotion": 1, - }, - "assets": aid.JSON{ - "CreativeDiscoverySurface_Frontend": aid.JSON{ - "meta": aid.JSON{ - "revision": 1, - "headRevision": 1, - "promotion": 1, - "revisedAt": "0000-00-00T00:00:00.000Z", - "promotedAt": "0000-00-00T00:00:00.000Z", - }, - "assetData": aid.JSON{ - "AnalyticsId": "t412", - "TestCohorts": []aid.JSON{ - testCohort, - }, - "GlobalLinkCodeBlacklist": []aid.JSON{}, - "SurfaceName": "CreativeDiscoverySurface_Frontend", - "TestName": "20.10_4/11/2022_hero_combat_popularConsole", - "primaryAssetId": "FortCreativeDiscoverySurface:CreativeDiscoverySurface_Frontend", - "GlobalLinkCodeWhitelist": []aid.JSON{}, - }, - }, - }, - }, - }) -} - func GetContentPages(c *fiber.Ctx) error { seasonString := strconv.Itoa(aid.Config.Fortnite.Season) - playlists := []aid.JSON{} - // for playlist := range fortnite.PlaylistImages { - // playlists = append(playlists, aid.JSON{ - // "image": "http://" +aid.Config.API.Host + aid.Config.API.Port + "/snow/image/" + playlist + ".png?cache="+strconv.Itoa(rand.Intn(9999)), - // "playlist_name": playlist, - // "hidden": false, - // }) - // } + playlists := []aid.JSON{ + { + "image": "https://cdn.snows.rocks/squads.png", + "playlist_name": "Playlist_DefaultSquad", + "hidden": false, + }, + { + "image": "https://cdn.snows.rocks/duos.png", + "playlist_name": "Playlist_DefaultDuo", + "hidden": false, + }, + { + "image": "https://cdn.snows.rocks/solo.png", + "playlist_name": "Playlist_DefaultSolo", + "hidden": false, + }, + { + "image": "https://cdn.snows.rocks/arena_solo.png", + "playlist_name": "Playlist_ShowdownAlt_Solo", + "hidden": false, + }, + { + "image": "https://cdn.snows.rocks/arena_duos.png", + "playlist_name": "Playlist_ShowdownAlt_Duos", + "hidden": false, + }, + } backgrounds := []aid.JSON{} switch aid.Config.Fortnite.Season { @@ -182,6 +61,17 @@ func GetContentPages(c *fiber.Ctx) error { "spotlight": false, "hidden": true, "messagetype": "normal", + "image": "https://cdn.snows.rocks/loading_stw.png", + }, + }, + "saveTheWorld": aid.JSON{ + "message": aid.JSON{ + "title": "Co-op PvE", + "body": "Cooperative PvE storm-fighting adventure!", + "spotlight": false, + "hidden": true, + "messagetype": "normal", + "image": "https://cdn.snows.rocks/loading_stw.png", }, }, "battleRoyale": aid.JSON{ @@ -191,6 +81,7 @@ func GetContentPages(c *fiber.Ctx) error { "spotlight": false, "hidden": true, "messagetype": "normal", + "image": "https://cdn.snows.rocks/loading_br.png", }, }, "creative": aid.JSON{ @@ -282,6 +173,23 @@ func GetContentPages(c *fiber.Ctx) error { "frontend_matchmaking_header_text": "ECS Qualifiers", "lastModified": "0000-00-00T00:00:00.000Z", }, + "tournamentinformation": aid.JSON{ + "tournament_info": aid.JSON{ + "tournaments": []aid.JSON{ + { + "tournament_display_id": "SnowArenaSolo", + "playlist_tile_image": "https://cdn.snows.rocks/arena_solo.png", + "title_line_2" : "ARENA", + }, + { + "tournament_display_id": "SnowArenaDuos", + "playlist_tile_image": "https://cdn.snows.rocks/arena_duos.png", + "title_line_2" : "ARENA", + }, + }, + }, + "lastModified": "0000-00-00T00:00:00.000Z", + }, "lastModified": "0000-00-00T00:00:00.000Z", }) } \ No newline at end of file diff --git a/handlers/events.go b/handlers/events.go new file mode 100644 index 0000000..f379b8c --- /dev/null +++ b/handlers/events.go @@ -0,0 +1,65 @@ +// structs from https://github.com/FabianFG/Fortnite-Api/ + +package handlers + +import ( + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/fortnite" + "github.com/ectrc/snow/person" + "github.com/gofiber/fiber/v2" +) + +var ( + hypeTokens = map[int]string{ + 0: "ARENA_S8_Division1", + 25: "ARENA_S8_Division2", + 75: "ARENA_S8_Division3", + 125: "ARENA_S8_Division4", + 175: "ARENA_S8_Division5", + 225: "ARENA_S8_Division6", + 300: "ARENA_S8_Division7", + } +) + +func GetEvents(c *fiber.Ctx) error { + person := c.Locals("person").(*person.Person) + + events := []aid.JSON{} + templates := []aid.JSON{} + tokens := []string{} + + for _, event := range fortnite.ArenaEvents { + events = append(events, event.GenerateFortniteEvent()) + + for _, window := range event.Windows { + templates = append(templates, window.Template.GenerateFortniteEventTemplate()) + } + } + + for limit, token := range hypeTokens { + if person.CurrentSeasonStats.Hype >= limit { + tokens = []string{token} + } + } + + return c.Status(200).JSON(aid.JSON{ + "player": aid.JSON{ + "gameId": "Fortnite", + "accountId": person.ID, + "tokens": tokens, + "teams": aid.JSON{}, + "pendingPayouts": []string{}, + "pendingPenalties": aid.JSON{}, + "persistentScores": aid.JSON{ + "Hype": person.CurrentSeasonStats.Hype, + }, + "groupIdentity": aid.JSON{}, + }, + "events": events, + "templates": templates, + }) +} + +func GetEventsBulkHistory(c *fiber.Ctx) error { + return c.Status(200).JSON([]aid.JSON{}) +} \ No newline at end of file diff --git a/handlers/friends.go b/handlers/friends.go index b5458f1..99f26de 100644 --- a/handlers/friends.go +++ b/handlers/friends.go @@ -14,6 +14,10 @@ func GetFriendList(c *fiber.Ctx) error { result := []aid.JSON{} person.Relationships.Range(func(key string, value *p.Relationship) bool { + if value.Towards == nil || value.From == nil { + return true + } + switch value.Direction { case p.RelationshipInboundDirection: result = append(result, value.GenerateFortniteFriendEntry(p.GenerateTypeTowardsPerson)) diff --git a/handlers/lightswitch.go b/handlers/lightswitch.go index 2d94343..8ee5ee0 100644 --- a/handlers/lightswitch.go +++ b/handlers/lightswitch.go @@ -94,7 +94,7 @@ func GetFortniteTimeline(c *fiber.Ctx) error { "seasonNumber": season, "seasonTemplateId": "AthenaSeason:AthenaSeason" + strings.Split(build, ".")[0], "seasonBegin": time.Now().Add(-time.Hour * 24 * 7).Format("2006-01-02T15:04:05.000Z"), - "seasonEnd": time.Now().Add(time.Hour * 24 * 7).Format("2006-01-02T15:04:05.000Z"), + "seasonEnd": time.Now().Add(time.Hour * 24 * 65).Format("2006-01-02T15:04:05.000Z"), "seasonDisplayedEnd": time.Now().Add(time.Hour * 24 * 7).Format("2006-01-02T15:04:05.000Z"), "activeStorefronts": []aid.JSON{}, "dailyStoreEnd": aid.TimeEndOfDay(), diff --git a/handlers/purchases.go b/handlers/purchases.go new file mode 100644 index 0000000..7ea285f --- /dev/null +++ b/handlers/purchases.go @@ -0,0 +1,177 @@ +package handlers + +import ( + "strings" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/person" + p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/shop" + "github.com/ectrc/snow/storage" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" +) + +func GetHtmlPurchasePage(c *fiber.Ctx) error { + c.Set("X-UEL", "DEFAULT") + c.Set("X-Download-Options", "noopen") + c.Set("X-DNS-Prefetch-Control", "off") + c.Set("x-epic-correlation-id", uuid.New().String()) + c.Set("X-Frame-Options", "SAMEORIGIN") + + var cookies struct { + Token string `cookie:"EPIC_BEARER_TOKEN"` + } + + if err := c.CookieParser(&cookies); err != nil { + return c.SendStatus(401) + } + + if cookies.Token == "" { + return c.SendStatus(401) + } + + person, err := aid.GetSnowFromToken(cookies.Token) + if err != nil { + return c.SendStatus(401) + } + c.Locals("person", person) + + fileBytes := storage.Asset("purchase.html") + if fileBytes == nil { + return c.SendStatus(404) + } + + c.Set("content-type", "text/html") + return c.SendString(string(*fileBytes)) +} + +func GetPurchaseAsset(c *fiber.Ctx) error { + asset := c.Query("asset") + + type_ := strings.Split(asset, ".") + fileBytes := storage.Asset(asset) + if fileBytes == nil { + return c.SendStatus(404) + } + + c.Set("content-type", "text/" + type_[1]) + return c.SendString(string(*fileBytes)) +} + +func GetPurchaseOffer(c *fiber.Ctx) error { + player := c.Locals("person").(*person.Person) + offerId := c.Query("offerId") + if offerId == "" { + return c.SendStatus(400) + } + + store := shop.GetShop() + offerRaw, type_ := store.GetOfferByID(offerId) + if offerRaw == nil { + return c.SendStatus(404) + } + + response := aid.JSON{ + "user": aid.JSON{ + "displayName": player.DisplayName, + }, + } + + switch type_ { + case shop.StorefrontCatalogOfferEnumCurrency: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeCurrency) + response["offer"] = aid.JSON{ + "id": offer.GetOfferID(), + "price": aid.FormatPrice(int(offer.Price.LocalPrice)), + "name": offer.Diplay.Title, + "imageUrl": offer.Meta.FeaturedImageURL, + "type": "currency", + } + case shop.StorefrontCatalogOfferEnumStarterKit: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeStarterKit) + response["offer"] = aid.JSON{ + "id": offer.GetOfferID(), + "price": aid.FormatPrice(int(offer.Price.LocalPrice)), + "name": offer.Diplay.Title, + "imageUrl": offer.Meta.FeaturedImageURL, + "type": "starterpack", + } + default: + break + } + + return c.Status(200).JSON(response) +} + +func PostPurchaseOffer(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + + var body struct { + OfferId string `json:"offerId" binding:"required"` + Type string `json:"type" binding:"required"` // "currency" or "starterpack" + } + + aid.PrintJSON(body) + + if err := c.BodyParser(&body); err != nil { + return c.SendStatus(400) + } + + lookup := map[string]func(*fiber.Ctx, *p.Person, string) error{ + "currency": purchaseCurrency, + "starterpack": purchaseStarterPack, + } + + if handler, ok := lookup[body.Type]; ok { + return handler(c, person, body.OfferId) + } + + return c.SendStatus(400) +} + +func purchaseCurrency(c *fiber.Ctx, person *p.Person, offerId string) error { + offerRaw, type_ := shop.GetShop().GetOfferByID(offerId) + if offerRaw == nil { + return c.Status(404).JSON(aid.ErrorNotFound) + } + if type_ != shop.StorefrontCatalogOfferEnumCurrency { + return c.Status(400).JSON(aid.ErrorBadRequest) + } + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeCurrency) + + receipt := p.NewReceipt(offerId, int(offer.Price.BasePrice)) + for _, grant := range offer.Rewards { + item := p.NewItem(grant.TemplateID, grant.Quantity) + item.ProfileType = string(grant.ProfileType) + receipt.AddLoot(item) + } + person.Receipts.AddReceipt(receipt).Save() + + return c.Status(200).JSON(aid.JSON{ + "receipt": receipt.GenerateUnrealReceiptEntry(), + }) +} + +func purchaseStarterPack(c *fiber.Ctx, person *p.Person, offerId string) error { + offerRaw, type_ := shop.GetShop().GetOfferByID(offerId) + if offerRaw == nil { + return c.Status(404).JSON(aid.ErrorNotFound) + } + if type_ != shop.StorefrontCatalogOfferEnumStarterKit { + return c.Status(400).JSON(aid.ErrorBadRequest) + } + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeStarterKit) + + receipt := p.NewReceipt(offerId, int(offer.Price.BasePrice)) + for _, grant := range offer.Rewards { + item := p.NewItem(grant.TemplateID, grant.Quantity) + item.ProfileType = string(grant.ProfileType) + receipt.AddLoot(item) + } + person.Receipts.AddReceipt(receipt).Save() + + return c.Status(200).JSON(aid.JSON{ + "receipt": receipt.GenerateUnrealReceiptEntry(), + }) +} \ No newline at end of file diff --git a/handlers/snow.go b/handlers/snow.go index 3b5c1ef..ee0ea87 100644 --- a/handlers/snow.go +++ b/handlers/snow.go @@ -1,9 +1,13 @@ package handlers import ( + "time" + "github.com/ectrc/snow/aid" "github.com/ectrc/snow/fortnite" p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/shop" + "github.com/ectrc/snow/socket" "github.com/gofiber/fiber/v2" ) @@ -42,17 +46,52 @@ func GetSnowParties(c *fiber.Ctx) error { } func GetSnowShop(c *fiber.Ctx) error { - shop := fortnite.NewRandomFortniteCatalog() + shop := shop.GetShop() return c.JSON(shop.GenerateFortniteCatalogResponse()) } -// +func PostSnowLog(c *fiber.Ctx) error { + var body struct { + JSON aid.JSON `json:"json"` + URL string `json:"url"` + } + + if err := c.BodyParser(&body); err != nil { + return c.Status(400).JSON(err.Error()) + } + + aid.PrintJSON(body.JSON) + return c.JSON(body) +} func GetPlayer(c *fiber.Ctx) error { person := c.Locals("person").(*p.Person) - return c.Status(200).JSON(person.Snapshot()) + return c.Status(200).JSON(aid.JSON{ + "snapshot": person.Snapshot(), + "season": aid.JSON{ + "level": fortnite.DataClient.SnowSeason.GetSeasonLevel(person.CurrentSeasonStats), + "xp": fortnite.DataClient.SnowSeason.GetRelativeSeasonXP(person.CurrentSeasonStats), + "bookLevel": fortnite.DataClient.SnowSeason.GetBookLevel(person.CurrentSeasonStats), + "bookXp": fortnite.DataClient.SnowSeason.GetRelativeBookXP(person.CurrentSeasonStats), + }, + }) } func GetPlayerOkay(c *fiber.Ctx) error { return c.Status(200).SendString("okay") +} + +func PostPlayerCreateCode(c *fiber.Ctx) error { + person := c.Locals("person").(*p.Person) + code := person.ID + "=" + time.Now().Format("2006-01-02T15:04:05.999Z") + encrypted, sig := aid.KeyPair.EncryptAndSignB64([]byte(code)) + return c.Status(200).SendString(encrypted + "." + sig) +} + +func GetLauncherStatus(c *fiber.Ctx) error { + return c.Status(200).JSON(aid.JSON{ + "CurrentSeason": aid.Config.Fortnite.Season, + "CurrentBuild": aid.Config.Fortnite.Build, + "PlayersOnline": aid.FormatNumber(socket.JabberSockets.Len()), + }) } \ No newline at end of file diff --git a/handlers/storefront.go b/handlers/storefront.go index 11e7fb9..f06da41 100644 --- a/handlers/storefront.go +++ b/handlers/storefront.go @@ -1,18 +1,16 @@ package handlers import ( - "strings" - "github.com/goccy/go-json" "github.com/ectrc/snow/aid" - "github.com/ectrc/snow/fortnite" + "github.com/ectrc/snow/shop" "github.com/ectrc/snow/storage" "github.com/gofiber/fiber/v2" ) func GetStorefrontCatalog(c *fiber.Ctx) error { - shop := fortnite.NewRandomFortniteCatalog() + shop := shop.GetShop() return c.Status(200).JSON(shop.GenerateFortniteCatalogResponse()) } @@ -27,7 +25,7 @@ func GetStorefrontKeychain(c *fiber.Ctx) error { } func GetStorefrontCatalogBulkOffers(c *fiber.Ctx) error { - shop := fortnite.NewRandomFortniteCatalog() + store := shop.GetShop() appStoreIdBytes := c.Request().URI().QueryArgs().PeekMulti("id") appStoreIds := make([]string, len(appStoreIdBytes)) @@ -37,21 +35,21 @@ func GetStorefrontCatalogBulkOffers(c *fiber.Ctx) error { response := aid.JSON{} for _, id := range appStoreIds { - offer := shop.FindCurrencyOfferById(strings.ReplaceAll(id, "app-", "")) - if offer == nil { + offerRaw, type_ := store.GetOfferByID(id) + if offerRaw == nil { continue } - response[id] = offer.GenerateFortniteCatalogBulkOfferResponse() - } - - for _, id := range appStoreIds { - offer := shop.FindStarterPackById(strings.ReplaceAll(id, "app-", "")) - if offer == nil { - continue + switch type_ { + case shop.StorefrontCatalogOfferEnumCurrency: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeCurrency) + response[id] = offer.GenerateFortniteBulkOffersResponse() + case shop.StorefrontCatalogOfferEnumStarterKit: + offer := offerRaw.(*shop.StorefrontCatalogOfferTypeStarterKit) + response[id] = offer.GenerateFortniteBulkOffersResponse() + default: + break } - - response[id] = offer.GenerateFortniteCatalogBulkOfferResponse() } return c.Status(200).JSON(response) diff --git a/main.go b/main.go index a37aa8c..870f51b 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/ectrc/snow/fortnite" "github.com/ectrc/snow/handlers" "github.com/ectrc/snow/person" + "github.com/ectrc/snow/shop" "github.com/ectrc/snow/storage" "github.com/goccy/go-json" @@ -20,7 +21,7 @@ import ( var configFile []byte func init() { - aid.LoadConfig(configFile) + aid.LoadConfig(configFile) var device storage.Storage switch aid.Config.Database.Type { case "postgres": @@ -45,13 +46,20 @@ func init() { func init() { discord.IntialiseClient() fortnite.PreloadCosmetics() - fortnite.NewRandomFortniteCatalog() + fortnite.PreloadEvents() + shop.GetShop() for _, username := range aid.Config.Accounts.Gods { found := person.FindByDisplay(username) if found == nil { found = fortnite.NewFortnitePersonWithId(username, username, aid.Config.Fortnite.Everything) } + found.Discord = &storage.DB_DiscordPerson{ + ID: found.ID, + PersonID: found.ID, + Username: username, + } + found.Save() found.AddPermission(person.PermissionAllWithRoles) aid.Print("(snow) max account " + username + " loaded") @@ -76,20 +84,16 @@ func main() { }) r.Use(aid.FiberLogger()) - r.Use(aid.FiberLimiter(100)) + r.Use(aid.FiberLimiter(1000)) r.Use(aid.FiberCors()) r.Get("/region", handlers.GetRegion) r.Get("/content/api/pages/fortnite-game", handlers.GetContentPages) r.Get("/waitingroom/api/waitingroom", handlers.GetWaitingRoomStatus) r.Get("/affiliate/api/public/affiliates/slug/:slug", handlers.GetAffiliate) - - r.Get("/api/v1/search/:accountId", handlers.GetPersonSearch) - r.Post("/api/v1/assets/Fortnite/:versionId/:assetName", handlers.PostAssets) - r.Get("/profile/privacy_settings", handlers.MiddlewareFortnite, handlers.GetPrivacySettings) r.Put("/profile/play_region", handlers.AnyNoContent) - + r.Get("/api/v1/search/:accountId", handlers.GetPersonSearch) r.Get("/", handlers.RedirectSocket) r.Get("/socket", handlers.MiddlewareWebsocket, websocket.New(handlers.WebsocketConnection)) @@ -103,7 +107,7 @@ func main() { account.Delete("/oauth/sessions/kill", handlers.DeleteToken) fortnite := r.Group("/fortnite/api") - fortnite.Get("/receipts/v1/account/:accountId/receipts", handlers.GetFortniteReceipts) + fortnite.Get("/receipts/v1/account/:accountId/receipts", handlers.MiddlewareFortnite, handlers.GetFortniteReceipts) fortnite.Get("/v2/versioncheck/:version", handlers.GetFortniteVersion) fortnite.Get("/calendar/v1/timeline", handlers.GetFortniteTimeline) @@ -134,11 +138,15 @@ func main() { friends.Post("/:version/:accountId/friends/:wanted", handlers.PostCreateFriend) friends.Delete("/:version/:accountId/friends/:wanted", handlers.DeleteFriend) + events := r.Group("/api/v1/events/Fortnite") + events.Use(handlers.MiddlewareFortnite) + events.Get("/download/:accountId", handlers.GetEvents) + events.Get("/:eventId/history/:accountId", handlers.GetEventsBulkHistory) + game := fortnite.Group("/game/v2") game.Get("/enabled_features", handlers.GetGameEnabledFeatures) game.Post("/tryPlayOnPlatform/account/:accountId", handlers.PostGamePlatform) game.Post("/grant_access/:accountId", handlers.PostGameAccess) - game.Post("/creative/discovery/surface/:accountId", handlers.PostDiscovery) game.Post("/profileToken/verify/:accountId", handlers.AnyNoContent) profile := game.Group("/profile/:accountId") @@ -150,6 +158,12 @@ func main() { lightswitch.Use(handlers.MiddlewareFortnite) lightswitch.Get("/service/bulk/status", handlers.GetLightswitchBulkStatus) + purchasing := r.Group("/purchase") + purchasing.Get("/", handlers.GetHtmlPurchasePage) + purchasing.Get("/offer", handlers.MiddlewareFortnite, handlers.GetPurchaseOffer) + purchasing.Post("/offer", handlers.MiddlewareFortnite, handlers.PostPurchaseOffer) + purchasing.Get("/assets", handlers.GetPurchaseAsset) + party := r.Group("/party/api/v1/Fortnite") party.Use(handlers.MiddlewareFortnite) party.Get("/user/:accountId", handlers.GetPartiesForUser) @@ -169,13 +183,19 @@ func main() { party.Post("/members/:friendId/intentions/:accountId", handlers.PostPartyCreateIntention) snow := r.Group("/snow") + snow.Post("/log", handlers.PostSnowLog) + discord := snow.Group("/discord") discord.Get("/", handlers.GetDiscordOAuthURL) + launcher := snow.Group("/launcher") + launcher.Get("/", handlers.GetLauncherStatus) + player := snow.Group("/player") player.Use(handlers.MiddlewareWeb) player.Get("/", handlers.GetPlayer) player.Get("/okay", handlers.GetPlayerOkay) + player.Post("/code", handlers.PostPlayerCreateCode) debug := snow.Group("/") debug.Use(handlers.MiddlewareOnlyDebug) @@ -191,10 +211,10 @@ 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() - } + // if aid.Config.Fortnite.Season <= 2 { + // t := handlers.NewServer() + // go t.Listen() + // } err := r.Listen("0.0.0.0" + aid.Config.API.Port) if err != nil { diff --git a/person/attribute.go b/person/attribute.go index 1f8250a..f633825 100644 --- a/person/attribute.go +++ b/person/attribute.go @@ -56,6 +56,11 @@ func (a *Attribute) Save() { storage.Repo.SaveAttribute(a.ToDatabase(a.ProfileID)) } +func (a *Attribute) SetValue(value interface{}) *Attribute { + a.ValueJSON = aid.JSONStringify(value) + return a +} + func AttributeConvertToSlice[T any](attribute *Attribute) []T { valuesRaw := aid.JSONParse(attribute.ValueJSON).([]interface{}) values := make([]T, len(valuesRaw)) @@ -67,5 +72,5 @@ func AttributeConvertToSlice[T any](attribute *Attribute) []T { } func AttributeConvert[T any](attribute *Attribute) T { - return aid.JSONParse(attribute.ValueJSON).(T) + return aid.JSONParseG[T](attribute.ValueJSON) } \ No newline at end of file diff --git a/person/gift.go b/person/gift.go index 32b190d..99b97bd 100644 --- a/person/gift.go +++ b/person/gift.go @@ -10,7 +10,7 @@ import ( type Gift struct { ID string - ProfileID string + ProfileID string TemplateID string Quantity int FromID string diff --git a/person/item.go b/person/item.go index 11027ac..f125a81 100644 --- a/person/item.go +++ b/person/item.go @@ -1,6 +1,8 @@ package person import ( + "strings" + "github.com/ectrc/snow/aid" "github.com/ectrc/snow/storage" "github.com/google/uuid" @@ -82,16 +84,29 @@ func FromDatabasePurchaseLoot(item *storage.DB_PurchaseLoot) *Item { } } -func (i *Item) GenerateFortniteItemEntry() aid.JSON { - attributes := aid.JSON{ - "variants": i.GenerateFortniteItemVariantChannels(), - "favorite": i.Favorite, - "item_seen": i.HasSeen, +func FromDatabaseReceiptLoot(item *storage.DB_ReceiptLoot) *Item { + return &Item{ + ID: item.ID, + TemplateID: item.TemplateID, + Quantity: item.Quantity, + Favorite: false, + HasSeen: false, + Variants: []*VariantChannel{}, + ProfileType: item.ProfileType, } +} - if i.TemplateID == "Currency:MtxPurchased" { +func (i *Item) GenerateFortniteItemEntry() aid.JSON { + attributes := aid.JSON{} + + switch strings.Split(i.TemplateID, ":")[0] { + case "Currency": + attributes["platform"] = "Shared" + default: attributes = aid.JSON{ - "platform": "Shared", + "variants": i.GenerateFortniteItemVariantChannels(), + "favorite": i.Favorite, + "item_seen": i.HasSeen, } } @@ -137,6 +152,10 @@ func (i *Item) DeleteLoot() { storage.Repo.DeleteLoot(i.ID) } +func (i *Item) DeleteReceiptLoot() { + storage.Repo.DeleteReceiptLoot(i.ID) +} + func (i *Item) NewChannel(channel string, owned []string, active string) *VariantChannel { return &VariantChannel{ ID: uuid.New().String(), @@ -149,7 +168,6 @@ func (i *Item) NewChannel(channel string, owned []string, active string) *Varian func (i *Item) AddChannel(channel *VariantChannel) { i.Variants = append(i.Variants, channel) - //storage.Repo.SaveItemVariant(i.ID, channel) } func (i *Item) RemoveChannel(channel *VariantChannel) { @@ -211,6 +229,10 @@ func (i *Item) Save() { return } + for _, variant := range i.Variants { + variant.Save() + } + storage.Repo.SaveItem(i.ToDatabase(i.ProfileID)) } @@ -234,8 +256,17 @@ func (i *Item) ToPurchaseLootDatabase(purchaseId string) *storage.DB_PurchaseLoo } } +func (i *Item) ToReceiptLootDatabase(receiptId string) *storage.DB_ReceiptLoot { + return &storage.DB_ReceiptLoot{ + ID: i.ID, + ReceiptID: receiptId, + ProfileType: i.ProfileType, + TemplateID: i.TemplateID, + Quantity: i.Quantity, + } +} + func (i *Item) SaveLoot(giftId string) { - //storage.Repo.SaveLoot(i.ToLootDatabase(giftId)) } func (i *Item) Snapshot() ItemSnapshot { diff --git a/person/person.go b/person/person.go index e5ebc85..30cfe4f 100644 --- a/person/person.go +++ b/person/person.go @@ -1,6 +1,8 @@ package person import ( + "fmt" + "strings" "time" "github.com/ectrc/snow/aid" @@ -19,7 +21,10 @@ type Person struct { Profile0Profile *Profile CollectionsProfile *Profile CreativeProfile *Profile + CurrentSeasonStats *SeasonStats + AllSeasonsStats aid.GenericSyncMap[SeasonStats] Discord *storage.DB_DiscordPerson + Receipts *ReceiptMutex BanHistory aid.GenericSyncMap[storage.DB_BanStatus] Relationships aid.GenericSyncMap[Relationship] Parties aid.GenericSyncMap[Party] @@ -28,8 +33,9 @@ type Person struct { } func NewPerson() *Person { + id := uuid.New().String() return &Person{ - ID: uuid.New().String(), + ID: id, DisplayName: uuid.New().String(), Permissions: 0, RefundTickets: 3, @@ -39,6 +45,8 @@ func NewPerson() *Person { Profile0Profile: NewProfile("profile0"), CollectionsProfile: NewProfile("collections"), CreativeProfile: NewProfile("creative"), + Receipts: NewReceiptMutex(id), + AllSeasonsStats: aid.GenericSyncMap[SeasonStats]{}, BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, Relationships: aid.GenericSyncMap[Relationship]{}, Parties: aid.GenericSyncMap[Party]{}, @@ -59,6 +67,8 @@ func NewPersonWithCustomID(id string) *Person { Profile0Profile: NewProfile("profile0"), CollectionsProfile: NewProfile("collections"), CreativeProfile: NewProfile("creative"), + Receipts: NewReceiptMutex(id), + AllSeasonsStats: aid.GenericSyncMap[SeasonStats]{}, BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, Relationships: aid.GenericSyncMap[Relationship]{}, Parties: aid.GenericSyncMap[Party]{}, @@ -164,6 +174,7 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per profile0 := NewProfile("profile0") collectionsProfile := NewProfile("collections") creativeProfile := NewProfile("creative") + receipts := NewReceiptMutex(databasePerson.ID) for _, profile := range databasePerson.Profiles { if profile.Type == "athena" { @@ -197,11 +208,14 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per } } + for _, receipt := range databasePerson.Receipts { + receipts.AddReceipt(FromDatabaseReceipt(&receipt)) + } + person := &Person{ ID: databasePerson.ID, DisplayName: databasePerson.DisplayName, Permissions: Permission(databasePerson.Permissions), - BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, AthenaProfile: athenaProfile, CommonCoreProfile: commonCoreProfile, CommonPublicProfile: commonPublicProfile, @@ -210,6 +224,9 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per CreativeProfile: creativeProfile, Discord: &databasePerson.Discord, RefundTickets: databasePerson.RefundTickets, + Receipts: receipts, + AllSeasonsStats: aid.GenericSyncMap[SeasonStats]{}, + BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, Relationships: aid.GenericSyncMap[Relationship]{}, Parties: aid.GenericSyncMap[Party]{}, Invites: aid.GenericSyncMap[PartyInvite]{}, @@ -220,6 +237,21 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per person.BanHistory.Set(ban.ID, &ban) } + for _, stat := range databasePerson.Stats { + person.AllSeasonsStats.Set(fmt.Sprint(stat.Season), FromDatabaseSeasonStats(stat)) + + if stat.Season == aid.Config.Fortnite.Season { + person.CurrentSeasonStats = FromDatabaseSeasonStats(stat) + } + } + + if person.CurrentSeasonStats == nil { + person.CurrentSeasonStats = NewSeasonStats(aid.Config.Fortnite.Season) + person.CurrentSeasonStats.PersonID = person.ID + person.AllSeasonsStats.Set(fmt.Sprint(aid.Config.Fortnite.Season), person.CurrentSeasonStats) + person.CurrentSeasonStats.Save() + } + if !shallow { person.LoadRelationships() } @@ -340,10 +372,6 @@ func (p *Person) RemovePermission(permission Permission) { } func (p *Person) HasPermission(permission Permission) bool { - // if permission == PermissionAll && permission != PermissionOwner { - // return p.Permissions == PermissionAll - // } - return p.Permissions & permission != 0 } @@ -352,8 +380,9 @@ func (p *Person) ToDatabase() *storage.DB_Person { ID: p.ID, DisplayName: p.DisplayName, Permissions: int64(p.Permissions), - BanHistory: []storage.DB_BanStatus{}, RefundTickets: p.RefundTickets, + Receipts: []storage.DB_Receipt{}, + BanHistory: []storage.DB_BanStatus{}, Profiles: []storage.DB_Profile{}, Stats: []storage.DB_SeasonStat{}, Discord: storage.DB_DiscordPerson{}, @@ -377,6 +406,16 @@ func (p *Person) ToDatabase() *storage.DB_Person { return true }) + p.Receipts.RangeReceipts(func(key string, receipt *Receipt) bool { + dbPerson.Receipts = append(dbPerson.Receipts, *receipt.ToDatabase()) + return true + }) + + p.AllSeasonsStats.Range(func(key string, stat *SeasonStats) bool { + dbPerson.Stats = append(dbPerson.Stats, *stat.ToDatabase(p.ID)) + return true + }) + for profileType, profile := range profilesToConvert { dbProfile := storage.DB_Profile{ ID: profile.ID, @@ -426,8 +465,9 @@ func (p *Person) ToDatabaseShallow() *storage.DB_Person { ID: p.ID, DisplayName: p.DisplayName, Permissions: int64(p.Permissions), - BanHistory: []storage.DB_BanStatus{}, RefundTickets: p.RefundTickets, + Receipts: []storage.DB_Receipt{}, + BanHistory: []storage.DB_BanStatus{}, Profiles: []storage.DB_Profile{}, Stats: []storage.DB_SeasonStat{}, Discord: storage.DB_DiscordPerson{}, @@ -442,6 +482,16 @@ func (p *Person) ToDatabaseShallow() *storage.DB_Person { return true }) + p.Receipts.RangeReceipts(func(key string, receipt *Receipt) bool { + dbPerson.Receipts = append(dbPerson.Receipts, *receipt.ToDatabase()) + return true + }) + + p.AllSeasonsStats.Range(func(key string, stat *SeasonStats) bool { + dbPerson.Stats = append(dbPerson.Stats, *stat.ToDatabase(p.ID)) + return true + }) + return &dbPerson } @@ -457,7 +507,10 @@ func (p *Person) Snapshot() *PersonSnapshot { Profile0Profile: *p.Profile0Profile.Snapshot(), CollectionsProfile: *p.CollectionsProfile.Snapshot(), CreativeProfile: *p.CreativeProfile.Snapshot(), + CurrentSeasonStats: *p.CurrentSeasonStats, + AllSeasonsStats: []SeasonStats{}, BanHistory: []storage.DB_BanStatus{}, + Receipts: []storage.DB_Receipt{}, Discord: *p.Discord, Relationships: *p.Relationships.Snapshot(), Parties: *p.Parties.Snapshot(), @@ -470,6 +523,16 @@ func (p *Person) Snapshot() *PersonSnapshot { return true }) + p.Receipts.RangeReceipts(func(key string, receipt *Receipt) bool { + snapshot.Receipts = append(snapshot.Receipts, *receipt.ToDatabase()) + return true + }) + + p.AllSeasonsStats.Range(func(key string, stat *SeasonStats) bool { + snapshot.AllSeasonsStats = append(snapshot.AllSeasonsStats, *stat) + return true + }) + return snapshot } @@ -493,4 +556,72 @@ func (p *Person) SetPurchaseHistoryAttribute() { "purchases": purchases, }) purchaseAttribute.Save() +} + +func (p *Person) SetInAppPurchasesAttribute() { + receipts := []string{} + fulfillmentCounts := map[string]int{} + + p.Receipts.RangeReceipts(func(key string, r *Receipt) bool { + pureOfferId := strings.ReplaceAll(r.OfferID, "app-", "") + receipts = append(receipts, r.ID) + fulfillmentCounts[pureOfferId]++ + return true + }) + + inAppPurchaseAttribute := p.CommonCoreProfile.Attributes.GetAttributeByKey("in_app_purchases") + inAppPurchaseAttribute.ValueJSON = aid.JSONStringify(aid.JSON{ + "ignoredReceipts": []string{}, + "refreshTimers": aid.JSON{}, + "receipts": receipts, + "fulfillmentCounts": fulfillmentCounts, + }) + inAppPurchaseAttribute.Save() +} + +func (p *Person) SyncVBucks(sourceProfileType string) { + antiSourceLookup := map[string]string{ + "profile0": "common_core", + "common_core": "profile0", + } + sourceProfile := p.GetProfileFromType(sourceProfileType) + antiSourceProfile := p.GetProfileFromType(antiSourceLookup[sourceProfileType]) + if sourceProfile == nil || antiSourceProfile == nil { + return + } + + sourceCurrency := sourceProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + antiSourceCurrency := antiSourceProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if sourceCurrency == nil || antiSourceCurrency == nil { + return + } + + antiSourceCurrency.Quantity = sourceCurrency.Quantity + antiSourceCurrency.Save() +} + +func (p *Person) TakeAndSyncVbucks(quant int) { + currency := p.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if currency == nil { + aid.Print("currency not found") + return + } + + currency.Quantity -= quant + currency.Save() + + p.SyncVBucks("common_core") +} + +func (p *Person) GiveAndSyncVbucks(quant int) { + currency := p.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if currency == nil { + aid.Print("currency not found") + return + } + + currency.Quantity += quant + currency.Save() + + p.SyncVBucks("common_core") } \ No newline at end of file diff --git a/person/profile.go b/person/profile.go index b2d7b82..65d117e 100644 --- a/person/profile.go +++ b/person/profile.go @@ -18,6 +18,7 @@ type Profile struct { Attributes *AttributeMutex Loadouts *LoadoutMutex Purchases *PurchaseMutex + VariantTokens *VariantTokenMutex Type string Revision int Changes []interface{} @@ -34,6 +35,7 @@ func NewProfile(profile string) *Profile { Attributes: NewAttributeMutex(&storage.DB_Profile{ID: id, Type: profile}), Loadouts: NewLoadoutMutex(&storage.DB_Profile{ID: id, Type: profile}), Purchases: NewPurchaseMutex(&storage.DB_Profile{ID: id, Type: profile}), + VariantTokens: NewVariantTokenMutex(&storage.DB_Profile{ID: id, Type: profile}), Type: profile, Revision: 0, Changes: []interface{}{}, @@ -47,6 +49,7 @@ func FromDatabaseProfile(profile *storage.DB_Profile) *Profile { attributes := NewAttributeMutex(profile) loadouts := NewLoadoutMutex(profile) purchases := NewPurchaseMutex(profile) + variantTokens := NewVariantTokenMutex(profile) for _, item := range profile.Items { items.AddItem(FromDatabaseItem(&item)) @@ -56,6 +59,10 @@ func FromDatabaseProfile(profile *storage.DB_Profile) *Profile { gifts.AddGift(FromDatabaseGift(&gift)) } + for _, variantToken := range profile.VariantTokens { + variantTokens.AddVariantToken(FromDatabaseVariantToken(&variantToken)) + } + for _, quest := range profile.Quests { quests.AddQuest(FromDatabaseQuest(&quest)) } @@ -87,6 +94,7 @@ func FromDatabaseProfile(profile *storage.DB_Profile) *Profile { Attributes: attributes, Loadouts: loadouts, Purchases: purchases, + VariantTokens: variantTokens, Type: profile.Type, Revision: profile.Revision, Changes: []interface{}{}, @@ -112,6 +120,11 @@ func (p *Profile) GenerateFortniteProfileEntry() aid.JSON { return true }) + p.VariantTokens.RangeVariantTokens(func(id string, variantToken *VariantToken) bool { + items[id] = variantToken.GenerateFortniteVariantTokenEntry() + return true + }) + p.Attributes.RangeAttributes(func(id string, attribute *Attribute) bool { attributes[attribute.Key] = aid.JSONParse(attribute.ValueJSON) return true @@ -146,6 +159,7 @@ func (p *Profile) Snapshot() *ProfileSnapshot { quests := map[string]Quest{} attributes := map[string]Attribute{} loadouts := map[string]Loadout{} + variantTokens := map[string]VariantToken{} p.Items.RangeItems(func(id string, item *Item) bool { items[id] = item.Snapshot() @@ -172,6 +186,11 @@ func (p *Profile) Snapshot() *ProfileSnapshot { return true }) + p.VariantTokens.RangeVariantTokens(func(id string, variantToken *VariantToken) bool { + variantTokens[id] = *variantToken + return true + }) + return &ProfileSnapshot{ ID: p.ID, Items: items, @@ -212,10 +231,12 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { 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") + if loadout != nil { + slotType := loadout.GetSlotFromItemTemplateID(item.TemplateID) + slotValue := loadout.GetItemFromSlot(slotType) + if slotValue != nil && slotValue.ID == item.ID { + p.CreateLoadoutChangedChange(loadout, slotType + "ID") + } } } case "Quests": @@ -231,6 +252,14 @@ func (p *Profile) Diff(b *ProfileSnapshot) []diff.Change { p.CreateGiftAddedChange(p.Gifts.GetGift(change.Path[1])) } + if change.Type == "delete" && change.Path[2] == "ID" { + p.CreateItemRemovedChange(change.Path[1]) + } + case "VariantTokens": + if change.Type == "create" && change.Path[2] == "ID" { + p.CreateVariantTokenAddedChange(p.VariantTokens.GetVariantToken(change.Path[1])) + } + if change.Type == "delete" && change.Path[2] == "ID" { p.CreateItemRemovedChange(change.Path[1]) } @@ -310,6 +339,19 @@ func (p *Profile) CreateGiftAddedChange(gift *Gift) { }) } +func (p *Profile) CreateVariantTokenAddedChange(variantToken *VariantToken) { + if variantToken == nil { + fmt.Println("error getting variant token from profile", variantToken.ID) + return + } + + p.Changes = append(p.Changes, ItemAdded{ + ChangeType: "itemAdded", + ItemId: variantToken.ID, + Item: variantToken.GenerateFortniteVariantTokenEntry(), + }) +} + func (p *Profile) CreateQuestAddedChange(quest *Quest) { if quest == nil { fmt.Println("error getting quest from profile", quest.ID) @@ -443,6 +485,7 @@ func (p *Profile) ToDatabase() *storage.DB_Profile { Type: p.Type, Items: []storage.DB_Item{}, Gifts: []storage.DB_Gift{}, + VariantTokens: []storage.DB_VariantToken{}, Quests: []storage.DB_Quest{}, Loadouts: []storage.DB_Loadout{}, Purchases: []storage.DB_Purchase{}, @@ -450,16 +493,21 @@ func (p *Profile) ToDatabase() *storage.DB_Profile { Revision: p.Revision, } - // p.Items.RangeItems(func(id string, item *Item) bool { - // dbProfile.Items = append(dbProfile.Items, *item.ToDatabase(dbProfile.PersonID)) - // return true - // }) + p.Items.RangeItems(func(id string, item *Item) bool { + dbProfile.Items = append(dbProfile.Items, *item.ToDatabase(dbProfile.PersonID)) + return true + }) // slow p.Gifts.RangeGifts(func(id string, gift *Gift) bool { dbProfile.Gifts = append(dbProfile.Gifts, *gift.ToDatabase(dbProfile.PersonID)) return true }) + p.VariantTokens.RangeVariantTokens(func(id string, variantToken *VariantToken) bool { + dbProfile.VariantTokens = append(dbProfile.VariantTokens, *variantToken.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 diff --git a/person/receipt.go b/person/receipt.go new file mode 100644 index 0000000..43ebb15 --- /dev/null +++ b/person/receipt.go @@ -0,0 +1,115 @@ +package person + +import ( + "time" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/storage" + "github.com/google/uuid" +) + +type Receipt struct { + ID string + PersonID string + OfferID string + PurchaseDate int64 + TotalPaid int + State string + Loot []*Item +} + +func NewReceipt(offerID string, totalPaid int) *Receipt { + return &Receipt{ + ID: uuid.New().String(), + OfferID: offerID, + PurchaseDate: time.Now().Unix(), + TotalPaid: totalPaid, + Loot: []*Item{}, + State: "PENDING", + } +} + +func FromDatabaseReceipt(receipt *storage.DB_Receipt) *Receipt { + loot := []*Item{} + + for _, item := range receipt.Loot { + loot = append(loot, FromDatabaseReceiptLoot(&item)) + } + + return &Receipt{ + ID: receipt.ID, + PersonID: receipt.PersonID, + OfferID: receipt.OfferID, + PurchaseDate: receipt.PurchaseDate, + TotalPaid: receipt.TotalPaid, + State: receipt.State, + Loot: loot, + } +} + +func (r *Receipt) GenerateUnrealReceiptEntry() aid.JSON { + return aid.JSON{ + "TransactionId": r.ID, + "TransactionState": string(r.State), + "Offers": []aid.JSON{{ + "OfferNamespace": "fn", + "OfferId": r.OfferID, + "Items": []aid.JSON{{ + "EntitlementId": r.ID, + "EntitlementName": "", + "ItemId": r.OfferID, + "ItemNamespace": "fn", + }}, + }}, + "grantedVoucher": aid.JSON{}, + } +} + +func (r *Receipt) GenerateFortniteReceiptEntry() aid.JSON { + return aid.JSON{ + "receiptId": r.ID, + "appStoreId": r.OfferID, + "receiptInfo": r.State, + } +} + +func (r *Receipt) AddLoot(item *Item) { + r.Loot = append(r.Loot, item) +} + +func (r *Receipt) SetState(state string) { + r.State = state +} + +func (r *Receipt) Delete() { + for _, item := range r.Loot { + item.DeleteReceiptLoot() + } + + storage.Repo.DeleteReceipt(r.ID) +} + +func (r *Receipt) Save() { + for _, item := range r.Loot { + storage.Repo.SaveReceiptLoot(item.ToReceiptLootDatabase(r.ID)) + } + storage.Repo.SaveReceipt(r.ToDatabase()) +} + +func (r *Receipt) ToDatabase() *storage.DB_Receipt { + loot := []storage.DB_ReceiptLoot{} + + for _, item := range r.Loot { + loot = append(loot, *item.ToReceiptLootDatabase(r.ID)) + } + + return &storage.DB_Receipt{ + ID: r.ID, + PersonID: r.PersonID, + OfferID: r.OfferID, + PurchaseDate: r.PurchaseDate, + TotalPaid: r.TotalPaid, + State: r.State, + Loot: loot, + } +} \ No newline at end of file diff --git a/person/season.go b/person/season.go new file mode 100644 index 0000000..d8174b6 --- /dev/null +++ b/person/season.go @@ -0,0 +1,55 @@ +package person + +import ( + "github.com/ectrc/snow/storage" + "github.com/google/uuid" +) + +type SeasonStats struct { + ID string + PersonID string + Season int + SeasonXP int + BookXP int + BookPurchased bool + Hype int +} + +func NewSeasonStats(season int) *SeasonStats { + return &SeasonStats{ + ID: uuid.New().String(), + Season: season, + } +} + +func (s *SeasonStats) ToDatabase(personId string) *storage.DB_SeasonStat { + return &storage.DB_SeasonStat{ + ID: s.ID, + PersonID: personId, + Season: s.Season, + SeasonXP: s.SeasonXP, + BookXP: s.BookXP, + BookPurchased: s.BookPurchased, + Hype: s.Hype, + } +} + +func (s *SeasonStats) Save() { + storage.Repo.SaveSeasonStats(s.ToDatabase(s.PersonID)) +} + +func (s *SeasonStats) Delete() { + storage.Repo.DeleteSeasonStats(s.ID) +} + +func FromDatabaseSeasonStats(db storage.DB_SeasonStat) *SeasonStats { + return &SeasonStats{ + ID: db.ID, + PersonID: db.PersonID, + Season: db.Season, + SeasonXP: db.SeasonXP, + BookXP: db.BookXP, + BookPurchased: db.BookPurchased, + Hype: db.Hype, + } +} \ No newline at end of file diff --git a/person/snapshot.go b/person/snapshot.go index 48117e8..cb765bf 100644 --- a/person/snapshot.go +++ b/person/snapshot.go @@ -13,6 +13,9 @@ type PersonSnapshot struct { Profile0Profile ProfileSnapshot CollectionsProfile ProfileSnapshot CreativeProfile ProfileSnapshot + CurrentSeasonStats SeasonStats + AllSeasonsStats []SeasonStats + Receipts []storage.DB_Receipt BanHistory []storage.DB_BanStatus Discord storage.DB_DiscordPerson Relationships map[string]*Relationship @@ -25,6 +28,7 @@ type ProfileSnapshot struct { ID string Items map[string]ItemSnapshot Gifts map[string]GiftSnapshot + Variants map[string]VariantChannel Quests map[string]Quest Attributes map[string]Attribute Loadouts map[string]Loadout diff --git a/person/sync.go b/person/sync.go index b9ba971..45eb30d 100644 --- a/person/sync.go +++ b/person/sync.go @@ -364,4 +364,105 @@ func (m *PurchaseMutex) CountRefunded() int { return true }) return count +} + +type ReceiptMutex struct { + sync.Map + PersonID string +} + +func NewReceiptMutex(personID string) *ReceiptMutex { + return &ReceiptMutex{ + PersonID: personID, + } +} + +func (m *ReceiptMutex) AddReceipt(receipt *Receipt) *Receipt { + receipt.PersonID = m.PersonID + m.Store(receipt.ID, receipt) + // storage.Repo.SaveReceipt(receipt.ToDatabase()) + return receipt +} + +func (m *ReceiptMutex) DeleteReceipt(id string) { + receipt := m.GetReceipt(id) + if receipt == nil { + return + } + + m.Delete(id) + receipt.Delete() +} + +func (m *ReceiptMutex) GetReceipt(id string) *Receipt { + receipt, ok := m.Load(id) + if !ok { + return nil + } + + return receipt.(*Receipt) +} + +func (m *ReceiptMutex) RangeReceipts(f func(key string, value *Receipt) bool) { + m.Range(func(key, value interface{}) bool { + return f(key.(string), value.(*Receipt)) + }) +} + +func (m *ReceiptMutex) Count() int { + count := 0 + m.Range(func(key, value interface{}) bool { + count++ + return true + }) + return count +} + +type VariantTokenMutex struct { + sync.Map + ProfileID string + ProfileType string +} + +func NewVariantTokenMutex(profile *storage.DB_Profile) *VariantTokenMutex { + return &VariantTokenMutex{ + ProfileID: profile.ID, + ProfileType: profile.Type, + } +} + +func (m *VariantTokenMutex) AddVariantToken(token *VariantToken) *VariantToken { + token.ProfileID = m.ProfileID + m.Store(token.ID, token) + // storage.Repo.SaveVariantToken(token.ToDatabase(m.ProfileID)) + return token +} + +func (m *VariantTokenMutex) DeleteVariantToken(id string) { + m.Delete(id) + storage.Repo.DeleteVariantToken(id) +} + +func (m *VariantTokenMutex) GetVariantToken(id string) *VariantToken { + token, ok := m.Load(id) + if !ok { + return nil + } + + return token.(*VariantToken) +} + +func (m *VariantTokenMutex) RangeVariantTokens(f func(key string, value *VariantToken) bool) { + m.Range(func(key, value interface{}) bool { + return f(key.(string), value.(*VariantToken)) + }) +} + +func (m *VariantTokenMutex) Count() int { + count := 0 + m.Range(func(key, value interface{}) bool { + count++ + return true + }) + return count } \ No newline at end of file diff --git a/person/variant.go b/person/variant.go new file mode 100644 index 0000000..2ffd7fa --- /dev/null +++ b/person/variant.go @@ -0,0 +1,130 @@ +package person + +import ( + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/storage" + "github.com/google/uuid" +) + +type VariantToken struct { + ID string + ProfileID string + TemplateID string + Name string + AutoEquipOnGrant bool + CreateGiftboxOnGrant bool + MarkItemUnseenOnGrant bool + VariantGrants []*VariantTokenGrant +} + +func NewVariantToken(profileID, templateID string) *VariantToken { + return &VariantToken{ + ID: uuid.New().String(), + ProfileID: profileID, + TemplateID: templateID, + } +} + +func (v *VariantToken) AddVariantGrant(channel, value string) { + vtGrant := &VariantTokenGrant{ + ID: uuid.New().String(), + VariantTokenID: v.ID, + Channel: channel, + Value: value, + } + + v.VariantGrants = append(v.VariantGrants, vtGrant) +} + +func FromDatabaseVariantToken(token *storage.DB_VariantToken) *VariantToken { + variantGrants := []*VariantTokenGrant{} + + for _, grant := range token.VariantGrants { + variantGrants = append(variantGrants, FromDatabaseVariantTokenGrant(&grant)) + } + + return &VariantToken{ + ID: token.ID, + ProfileID: token.ProfileID, + TemplateID: token.TemplateID, + Name: token.Name, + AutoEquipOnGrant: token.AutoEquipOnGrant, + CreateGiftboxOnGrant: token.CreateGiftboxOnGrant, + MarkItemUnseenOnGrant: token.MarkItemUnseenOnGrant, + VariantGrants: variantGrants, + } +} + +func (v *VariantToken) GenerateFortniteVariantTokenEntry() aid.JSON { + return aid.JSON{ + "templateId": v.TemplateID, + "attributes": aid.JSON{ + "auto_equip_variant": v.AutoEquipOnGrant, + "create_giftbox": v.CreateGiftboxOnGrant, + "mark_item_unseen": v.MarkItemUnseenOnGrant, + "variant_name": v.Name, + }, + "quantity": 1, + } +} + +func (v *VariantToken) ToDatabase(profileID string) *storage.DB_VariantToken { + variantGrants := []storage.DB_VariantTokenGrant{} + + for _, grant := range v.VariantGrants { + variantGrants = append(variantGrants, *grant.ToDatabase()) + } + + return &storage.DB_VariantToken{ + ID: v.ID, + ProfileID: profileID, + TemplateID: v.TemplateID, + Name: v.Name, + AutoEquipOnGrant: v.AutoEquipOnGrant, + CreateGiftboxOnGrant: v.CreateGiftboxOnGrant, + MarkItemUnseenOnGrant: v.MarkItemUnseenOnGrant, + VariantGrants: variantGrants, + } +} + +func (v *VariantToken) Save() { + storage.Repo.SaveVariantToken(v.ToDatabase(v.ProfileID)) +} + +func (v *VariantToken) Delete() { + storage.Repo.DeleteVariantToken(v.ID) +} + +type VariantTokenGrant struct { + ID string + VariantTokenID string + Channel string + Value string +} + +func NewVariantTokenGrant(vtID, channel, value string) *VariantTokenGrant { + return &VariantTokenGrant{ + ID: uuid.New().String(), + VariantTokenID: vtID, + Channel: channel, + Value: value, + } +} + +func FromDatabaseVariantTokenGrant(grant *storage.DB_VariantTokenGrant) *VariantTokenGrant { + return &VariantTokenGrant{ + ID: grant.ID, + VariantTokenID: grant.VariantTokenID, + Channel: grant.Channel, + Value: grant.Value, + } +} + +func (v *VariantTokenGrant) ToDatabase() *storage.DB_VariantTokenGrant { + return &storage.DB_VariantTokenGrant{ + ID: v.ID, + VariantTokenID: v.VariantTokenID, + Channel: v.Channel, + Value: v.Value, + } +} \ No newline at end of file diff --git a/readme.md b/readme.md index 7c86304..77f156d 100644 --- a/readme.md +++ b/readme.md @@ -4,18 +4,14 @@ > Performance first, feature-rich universal Fortnite private server backend written in Go. -Snow will no longer be updated. I have reached a point where I would need a game server to make it worth adding new features, e.g. Leaderboards, Challenges etc. In the future I may continue but for the time being no updates will occur. If you would like to contribute I will still review each request! - ## Overview - **Single File** It will embed all of the external files inside of one executable! This allows the backend to be ran anywhere with no setup _(after initial config)_! - **Blazingly Fast** Written in Go and built upon Fast HTTP, it is extremely fast and can handle any profile action in milliseconds with its caching system. - **Automatic Profile Changes** Automatically keeps track of profile changes exactly so any external changes are displayed in-game on the next action. -- **Universal Database** It is possible to add new database types to satisfy your needs. Currently, it only supports `postgresql`. ## What's up next? -- 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. @@ -25,22 +21,23 @@ And once battle royale is completed ... ## Feature List +- **Battle Pass** Claim a free battle pass and level it up with challenges or buy tiers with V-Bucks. +- **Store Purchasing** Buy V-Bucks and Starter Packs right from the in-game store! +- **Item Refunding** Of previous shop purchases, will use a refund ticket if refunded in time. +- **Automatic Item Shop** Will automatically update the item shop for the day, for all builds. +- **Support A Creator 5%** Use any display name and each purchase will give them 5% of the vbucks spent. - **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. -- **V-Bucks Purchasing** Buy V-Bucks and Starter Packs right from the in-game store! - **Client Settings Storage** Uses amazon buckets to store client settings. - **Giftable Bundles** Players can recieve bundles, e.g. Twitch Prime, and gift them to friends. -- **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 ## Supported MCP Actions -`QueryProfile`, `ClientQuestLogin`, `MarkItemSeen`, `SetItemFavoriteStatusBatch`, `EquipBattleRoyaleCustomization`, `SetBattleRoyaleBanner`, `SetCosmeticLockerSlot`, `SetCosmeticLockerBanner`, `SetCosmeticLockerName`, `CopyCosmeticLoadout`, `DeleteCosmeticLoadout`, `PurchaseCatalogEntry`, `GiftCatalogEntry`, `RemoveGiftBox`, `RefundMtxPurchase`, `SetAffiliateName`, `SetReceiveGiftsEnabled` +`QueryProfile`, `ClientQuestLogin`, `MarkItemSeen`, `SetItemFavoriteStatusBatch`, `EquipBattleRoyaleCustomization`, `SetBattleRoyaleBanner`, `SetCosmeticLockerSlot`, `SetCosmeticLockerBanner`, `SetCosmeticLockerName`, `CopyCosmeticLoadout`, `DeleteCosmeticLoadout`, `PurchaseCatalogEntry`, `GiftCatalogEntry`, `RemoveGiftBox`, `RefundMtxPurchase`, `SetAffiliateName`, `SetReceiveGiftsEnabled`, `VerifyRealMoneyPurchase` ## Support diff --git a/shop/book.go b/shop/book.go new file mode 100644 index 0000000..0870c8f --- /dev/null +++ b/shop/book.go @@ -0,0 +1,130 @@ +package shop + +import ( + "fmt" + + "github.com/ectrc/snow/aid" +) + +type StorefrontCatalogOfferMetaTypeBattlePass struct { + TileSize string + SectionID string + DisplayAssetPath string + NewDisplayAssetPath string + Priority int + OnlyOnce bool +} + +type StorefrontCatalogOfferTypeBattlePass struct { + OfferID string + OfferType StorefrontCatalogOfferEnum + Rewards []*StorefrontCatalogOfferGrant + Price *StorefrontCatalogOfferPriceMtxCurrency + Diplay *OfferDisplay + Categories []string + Meta *StorefrontCatalogOfferMetaTypeBattlePass +} + +func NewBattlePassCatalogOffer() *StorefrontCatalogOfferTypeBattlePass { + return &StorefrontCatalogOfferTypeBattlePass{ + OfferID: aid.RandomString(32), + OfferType: StorefrontCatalogOfferEnumBattlePass, + Rewards: make([]*StorefrontCatalogOfferGrant, 0), + Price: &StorefrontCatalogOfferPriceMtxCurrency{}, + Diplay: &OfferDisplay{}, + Categories: make([]string, 0), + Meta: &StorefrontCatalogOfferMetaTypeBattlePass{}, + } +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GetOffer() *StorefrontCatalogOfferTypeBattlePass { + return o +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GetOfferID() string { + return o.OfferID +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GetOfferType() StorefrontCatalogOfferEnum { + return o.OfferType +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GetOfferPrice() *StorefrontCatalogOfferPriceMtxCurrency { + return o.Price +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GetRewards() []*StorefrontCatalogOfferGrant { + return o.Rewards +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GenerateFortniteCatalogOfferResponse() aid.JSON { + return aid.JSON{ + "offerId": o.OfferID, + "offerType": "StaticPrice", + "devName": fmt.Sprintf("[BOOK] %s", o.Diplay.ShortDescription), + "itemGrants": []string{}, + "requirements": aid.Ternary[[]aid.JSON](o.Meta.OnlyOnce, []aid.JSON{{ + "requirementType": "DenyOnFulfillment", + "requiredId": o.GetOfferID(), + "minQuantity": 1, + }}, []aid.JSON{}), + "fulfillmentIds": []string{o.OfferID}, + "categories": o.Categories, + "metaInfo": []aid.JSON{ + { + "Key": "TileSize", + "Value": o.Meta.TileSize, + }, + { + "Key": "SectionId", + "Value": o.Meta.SectionID, + }, + { + "Key": "NewDisplayAssetPath", + "Value": o.Meta.NewDisplayAssetPath, + }, + { + "Key": "DisplayAssetPath", + "Value": o.Meta.DisplayAssetPath, + }, + }, + "meta": aid.JSON{ + "TileSize": o.Meta.TileSize, + "SectionId": o.Meta.SectionID, + "DisplayAssetPath": o.Meta.DisplayAssetPath, + "NewDisplayAssetPath": o.Meta.NewDisplayAssetPath, + }, + "giftInfo": aid.JSON{ + "bIsEnabled": false, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": []string{}, + "giftRecordIds": []string{}, + }, + "prices": []aid.JSON{{ + "currencyType": "MtxCurrency", + "currencySubType": "Currency", + "regularPrice": o.Price.OriginalPrice, + "dynamicRegularPrice": -1, + "finalPrice": o.Price.FinalPrice, + "basePrice": o.Price.OriginalPrice, + "saleType": o.Price.SaleType, + "saleExpiration": "9999-12-31T23:59:59.999Z", + }}, + "displayAssetPath": o.Meta.DisplayAssetPath, + "refundable": false, + "title": o.Diplay.Title, + "description": o.Diplay.Description, + "shortDescription": o.Diplay.ShortDescription, + "appStoreId": []string{}, + "dailyLimit": -1, + "weeklyLimit": -1, + "monthlyLimit": -1, + "sortPriority": o.Meta.Priority, + "catalogGroupPriority": 0, + "filterWeight": 0, + } +} + +func (o *StorefrontCatalogOfferTypeBattlePass) GenerateFortniteBulkOffersResponse() aid.JSON { + return aid.JSON{} +} \ No newline at end of file diff --git a/shop/catalog.go b/shop/catalog.go new file mode 100644 index 0000000..ef37adf --- /dev/null +++ b/shop/catalog.go @@ -0,0 +1,46 @@ +package shop + +import "github.com/ectrc/snow/aid" + +type StorefrontCatalog struct { + Sections []*StorefrontCatalogSection +} + +func NewStorefrontCatalog() *StorefrontCatalog { + return &StorefrontCatalog{ + Sections: make([]*StorefrontCatalogSection, 0), + } +} + +func (c *StorefrontCatalog) AddSection(section *StorefrontCatalogSection) { + c.Sections = append(c.Sections, section) +} +func (c *StorefrontCatalog) AddSections(sections ...*StorefrontCatalogSection) { + c.Sections = append(c.Sections, sections...) +} + +func (c *StorefrontCatalog) GetOfferByID(offerID string) (interface{}, StorefrontCatalogOfferEnum) { + for _, section := range c.Sections { + found, type_ := section.GetOfferByID(offerID) + if found != nil { + return found, type_ + } + } + + return nil, -1 +} + +func (c *StorefrontCatalog) GenerateFortniteCatalogResponse() aid.JSON { + sectionsResponse := []aid.JSON{} + + for _, section := range c.Sections { + sectionsResponse = append(sectionsResponse, section.GenerateFortniteCatalogSectionResponse()) + } + + return aid.JSON{ + "storefronts": sectionsResponse, + "refreshIntervalHrs": 24, + "dailyPurchaseHrs": 24, + "expiration": "9999-12-31T23:59:59.999Z", + } +} \ No newline at end of file diff --git a/shop/item.go b/shop/item.go new file mode 100644 index 0000000..7469f02 --- /dev/null +++ b/shop/item.go @@ -0,0 +1,151 @@ +package shop + +import ( + "fmt" + + "github.com/ectrc/snow/aid" +) + +type StorefrontCatalogOfferMetaTypeItem struct { + TileSize string + SectionID string + DisplayAssetPath string + NewDisplayAssetPath string + BannerOverride string + Giftable bool + Refundable bool +} + +type StorefrontCatalogOfferTypeItem struct { + OfferID string + OfferType StorefrontCatalogOfferEnum + Rewards []*StorefrontCatalogOfferGrant + Price *StorefrontCatalogOfferPriceMtxCurrency + Diplay *OfferDisplay + Categories []string + Meta *StorefrontCatalogOfferMetaTypeItem +} + +func NewItemCatalogOffer() *StorefrontCatalogOfferTypeItem { + return &StorefrontCatalogOfferTypeItem{ + OfferID: aid.RandomString(32), + OfferType: StorefrontCatalogOfferEnumItem, + Rewards: make([]*StorefrontCatalogOfferGrant, 0), + Price: &StorefrontCatalogOfferPriceMtxCurrency{}, + Diplay: &OfferDisplay{}, + Categories: make([]string, 0), + Meta: &StorefrontCatalogOfferMetaTypeItem{}, + } +} + +func (o *StorefrontCatalogOfferTypeItem) GetOffer() *StorefrontCatalogOfferTypeItem { + return o +} + +func (o *StorefrontCatalogOfferTypeItem) GetOfferID() string { + return o.OfferID +} + +func (o *StorefrontCatalogOfferTypeItem) GetOfferType() StorefrontCatalogOfferEnum { + return o.OfferType +} + +func (o *StorefrontCatalogOfferTypeItem) GetOfferPrice() *StorefrontCatalogOfferPriceMtxCurrency { + return o.Price +} + +func (o *StorefrontCatalogOfferTypeItem) GetRewards() []*StorefrontCatalogOfferGrant { + return o.Rewards +} + +func (o *StorefrontCatalogOfferTypeItem) GenerateFortniteCatalogOfferResponse() aid.JSON { + itemGrantResponse := []aid.JSON{} + purchaseRequirementsResponse := []aid.JSON{} + developerNameResponse := "[ITEM]" + + for _, reward := range o.Rewards { + itemGrantResponse = append(itemGrantResponse, aid.JSON{ + "templateId": reward.TemplateID, + "quantity": reward.Quantity, + }) + + purchaseRequirementsResponse = append(purchaseRequirementsResponse, aid.JSON{ + "requirementType": "DenyOnItemOwnership", + "requiredId": reward.TemplateID, + "minQuantity": 1, + }) + + developerNameResponse += fmt.Sprintf(" %dx %s", reward.Quantity, reward.TemplateID) + } + + return aid.JSON{ + "offerId": o.OfferID, + "offerType": "StaticPrice", + "devName": fmt.Sprintf("%s for %d MtxCurrency", developerNameResponse, o.Price.OriginalPrice), + "itemGrants": itemGrantResponse, + "requirements": purchaseRequirementsResponse, + "categories": o.Categories, + "metaInfo": []aid.JSON{ + { + "Key": "TileSize", + "Value": o.Meta.TileSize, + }, + { + "Key": "SectionId", + "Value": o.Meta.SectionID, + }, + { + "Key": "NewDisplayAssetPath", + "Value": o.Meta.NewDisplayAssetPath, + }, + { + "Key": "DisplayAssetPath", + "Value": o.Meta.DisplayAssetPath, + }, + { + "Key": "BannerOverride", + "Value": o.Meta.BannerOverride, + }, + }, + "meta": aid.JSON{ + "TileSize": o.Meta.TileSize, + "SectionId": o.Meta.SectionID, + "DisplayAssetPath": o.Meta.DisplayAssetPath, + "NewDisplayAssetPath": o.Meta.NewDisplayAssetPath, + "BannerOverride": o.Meta.BannerOverride, + }, + "giftInfo": aid.JSON{ + "bIsEnabled": o.Meta.Giftable, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": purchaseRequirementsResponse, + "giftRecordIds": []string{}, + }, + "prices": []aid.JSON{{ + "currencyType": "MtxCurrency", + "currencySubType": "Currency", + "regularPrice": o.Price.OriginalPrice, + "dynamicRegularPrice": -1, + "finalPrice": o.Price.FinalPrice, + "basePrice": o.Price.OriginalPrice, + "saleExpiration": "9999-12-31T23:59:59.999Z", + }}, + "bannerOverride": o.Meta.BannerOverride, + "displayAssetPath": o.Meta.DisplayAssetPath, + "refundable": o.Meta.Refundable, + "title": o.Diplay.Title, + "description": o.Diplay.Description, + "shortDescription": o.Diplay.ShortDescription, + "appStoreId": []string{}, + "fulfillmentIds": []string{}, + "dailyLimit": -1, + "weeklyLimit": -1, + "monthlyLimit": -1, + "sortPriority": 0, + "catalogGroupPriority": 0, + "filterWeight": 0, + } +} + +func (o *StorefrontCatalogOfferTypeItem) GenerateFortniteBulkOffersResponse() aid.JSON { + return aid.JSON{} +} \ No newline at end of file diff --git a/shop/kits.go b/shop/kits.go new file mode 100644 index 0000000..aa6f4d8 --- /dev/null +++ b/shop/kits.go @@ -0,0 +1,148 @@ +package shop + +import ( + "fmt" + "strings" + + "github.com/ectrc/snow/aid" +) + +type StorefrontCatalogOfferMetaTypeStarterKit struct { + TileSize string + DisplayAssetPath string + NewDisplayAssetPath string + OriginalOffer int + ExtraBonus int + FeaturedImageURL string + Priority int + ReleaseSeason int +} + +type StorefrontCatalogOfferTypeStarterKit struct { + OfferType StorefrontCatalogOfferEnum + Rewards []*StorefrontCatalogOfferGrant + Price *StorefrontCatalogOfferPriceRealMoney + Diplay *OfferDisplay + Categories []string + Meta *StorefrontCatalogOfferMetaTypeStarterKit +} + +func NewStarterKitCatalogOffer() *StorefrontCatalogOfferTypeStarterKit { + return &StorefrontCatalogOfferTypeStarterKit{ + OfferType: StorefrontCatalogOfferEnumStarterKit, + Rewards: make([]*StorefrontCatalogOfferGrant, 0), + Price: &StorefrontCatalogOfferPriceRealMoney{}, + Diplay: &OfferDisplay{}, + Categories: make([]string, 0), + Meta: &StorefrontCatalogOfferMetaTypeStarterKit{}, + } +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GetOffer() *StorefrontCatalogOfferTypeStarterKit { + return o +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GetOfferID() string { + return fmt.Sprintf("kit://%s", strings.ReplaceAll(o.Diplay.Title, " ", "")) +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GetOfferType() StorefrontCatalogOfferEnum { + return o.OfferType +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GetOfferPrice() *StorefrontCatalogOfferPriceRealMoney { + return o.Price +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GetRewards() []*StorefrontCatalogOfferGrant { + return o.Rewards +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GenerateFortniteCatalogOfferResponse() aid.JSON { + return aid.JSON{ + "offerId": o.GetOfferID(), + "offerType": "StaticPrice", + "devName": fmt.Sprintf("[STARTER KIT] %s", o.Diplay.Title), + "itemGrants": []aid.JSON{}, + "requirements": []aid.JSON{{ + "requirementType": "DenyOnFulfillment", + "requiredId": o.GetOfferID(), + "minQuantity": 1, + }}, + "categories": o.Categories, + "metaInfo": []aid.JSON{ + { + "Key": "TileSize", + "Value": o.Meta.TileSize, + }, + { + "Key": "NewDisplayAssetPath", + "Value": o.Meta.NewDisplayAssetPath, + }, + { + "Key": "DisplayAssetPath", + "Value": o.Meta.DisplayAssetPath, + }, + { + "key": "MtxQuantity", + "value": o.Meta.OriginalOffer, + }, + { + "key": "MtxBonus", + "value": o.Meta.ExtraBonus, + }, + }, + "meta": aid.JSON{ + "TileSize": o.Meta.TileSize, + "DisplayAssetPath": o.Meta.DisplayAssetPath, + "NewDisplayAssetPath": o.Meta.NewDisplayAssetPath, + "MtxQuantity": o.Meta.OriginalOffer, + "MtxBonus": o.Meta.ExtraBonus, + }, + "giftInfo": aid.JSON{ + "bIsEnabled": false, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": []aid.JSON{}, + "giftRecordIds": []string{}, + }, + "prices": []aid.JSON{{ + "currencyType": "RealMoney", + "currencySubType": "", + "regularPrice": -1, + "dynamicRegularPrice": -1, + "finalPrice": -1, + "basePrice": -1, + "saleExpiration": "9999-12-31T23:59:59.999Z", + }}, + "displayAssetPath": o.Meta.DisplayAssetPath, + "refundable": false, + "title": o.Diplay.Title, + "description": o.Diplay.Description, + "shortDescription": o.Diplay.ShortDescription, + "appStoreId": []string{ + "", + o.GetOfferID(), + }, + "dailyLimit": -1, + "weeklyLimit": -1, + "monthlyLimit": -1, + "sortPriority": 0, + "catalogGroupPriority": 0, + "filterWeight": 0, + } +} + +func (o *StorefrontCatalogOfferTypeStarterKit) GenerateFortniteBulkOffersResponse() aid.JSON { + return aid.JSON{ + "id": o.GetOfferID(), + "title": o.Diplay.Title, + "shortDescription": o.Diplay.ShortDescription, + "longDescription": o.Diplay.LongDescription, + "creationDate": "0000-00-00T00:00:00.000Z", + "price": o.Price.LocalPrice, + "currentPrice": o.Price.LocalPrice, + "currencyCode": "USD", + "basePrice": o.Price.BasePrice, + "basePriceCurrencyCode": "GBP", + } +} \ No newline at end of file diff --git a/shop/money.go b/shop/money.go new file mode 100644 index 0000000..65723db --- /dev/null +++ b/shop/money.go @@ -0,0 +1,157 @@ +package shop + +import ( + "fmt" + "strings" + + "github.com/ectrc/snow/aid" +) + +type StorefrontCatalogOfferMetaTypeCurrency struct { + IconSize string + DisplayAssetPath string + NewDisplayAssetPath string + BannerOverride string + CurrencyAnalyticsName string + OriginalOffer int + ExtraBonus int + FeaturedImageURL string + Priority int +} + +type StorefrontCatalogOfferTypeCurrency struct { + OfferType StorefrontCatalogOfferEnum + Rewards []*StorefrontCatalogOfferGrant + Price *StorefrontCatalogOfferPriceRealMoney + Diplay *OfferDisplay + Categories []string + Meta *StorefrontCatalogOfferMetaTypeCurrency +} + +func NewCurrencyCatalogOffer() *StorefrontCatalogOfferTypeCurrency { + return &StorefrontCatalogOfferTypeCurrency{ + OfferType: StorefrontCatalogOfferEnumItem, + Rewards: make([]*StorefrontCatalogOfferGrant, 0), + Price: &StorefrontCatalogOfferPriceRealMoney{}, + Diplay: &OfferDisplay{}, + Categories: make([]string, 0), + Meta: &StorefrontCatalogOfferMetaTypeCurrency{}, + } +} + +func (o *StorefrontCatalogOfferTypeCurrency) GetOffer() *StorefrontCatalogOfferTypeCurrency { + return o +} + +func (o *StorefrontCatalogOfferTypeCurrency) GetOfferID() string { + return fmt.Sprintf("money://%s", strings.ReplaceAll(o.Diplay.Title, " ", "")) +} + +func (o *StorefrontCatalogOfferTypeCurrency) GetOfferType() StorefrontCatalogOfferEnum { + return o.OfferType +} + +func (o *StorefrontCatalogOfferTypeCurrency) GetOfferPrice() *StorefrontCatalogOfferPriceRealMoney { + return o.Price +} + +func (o *StorefrontCatalogOfferTypeCurrency) GetRewards() []*StorefrontCatalogOfferGrant { + return o.Rewards +} + +func (o *StorefrontCatalogOfferTypeCurrency) GenerateFortniteCatalogOfferResponse() aid.JSON { + return aid.JSON{ + "offerId": o.GetOfferID(), + "offerType": "StaticPrice", + "devName": fmt.Sprintf("[CURRENCY] %s", o.Diplay.Title), + "itemGrants": []aid.JSON{}, + "requirements": []aid.JSON{}, + "fulfillmentIds": []string{o.GetOfferID()}, + "categories": o.Categories, + "metaInfo": []aid.JSON{ + { + "key": "IconSize", + "value": o.Meta.IconSize, + }, + { + "Key": "NewDisplayAssetPath", + "Value": o.Meta.NewDisplayAssetPath, + }, + { + "Key": "DisplayAssetPath", + "Value": o.Meta.DisplayAssetPath, + }, + { + "Key": "BannerOverride", + "Value": o.Meta.BannerOverride, + }, + { + "Key": "CurrencyAnalyticsName", + "Value": o.Meta.CurrencyAnalyticsName, + }, + { + "key": "MtxQuantity", + "value": o.Meta.OriginalOffer, + }, + { + "key": "MtxBonus", + "value": o.Meta.ExtraBonus, + }, + }, + "meta": aid.JSON{ + "IconSize": o.Meta.IconSize, + "DisplayAssetPath": o.Meta.DisplayAssetPath, + "NewDisplayAssetPath": o.Meta.NewDisplayAssetPath, + "BannerOverride": o.Meta.BannerOverride, + "CurrencyAnalyticsName": o.Meta.CurrencyAnalyticsName, + "MtxQuantity": o.Meta.OriginalOffer, + "MtxBonus": o.Meta.ExtraBonus, + }, + "giftInfo": aid.JSON{ + "bIsEnabled": false, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": []aid.JSON{}, + "giftRecordIds": []string{}, + }, + "prices": []aid.JSON{{ + "currencyType": "RealMoney", + "currencySubType": "", + "regularPrice": -1, + "dynamicRegularPrice": -1, + "finalPrice": -1, + "basePrice": -1, + "saleExpiration": "9999-12-31T23:59:59.999Z", + }}, + "bannerOverride": o.Meta.BannerOverride, + "displayAssetPath": o.Meta.DisplayAssetPath, + "refundable": false, + "title": o.Diplay.Title, + "description": o.Diplay.Description, + "shortDescription": o.Diplay.ShortDescription, + "appStoreId": []string{ + "", + o.GetOfferID(), + }, + "dailyLimit": -1, + "weeklyLimit": -1, + "monthlyLimit": -1, + "sortPriority": o.Meta.Priority, + "catalogGroupPriority": 0, + "filterWeight": 0, + } +} + +func (o *StorefrontCatalogOfferTypeCurrency) GenerateFortniteBulkOffersResponse() aid.JSON { + return aid.JSON{ + "id": o.GetOfferID(), + "title": o.Diplay.Title, + "shortDescription": o.Diplay.ShortDescription, + "longDescription": o.Diplay.LongDescription, + "creationDate": "0000-00-00T00:00:00.000Z", + "price": o.Price.LocalPrice, + "currentPrice": o.Price.LocalPrice, + "currencyCode": "USD", + "basePrice": o.Price.BasePrice, + "basePriceCurrencyCode": "GBP", + } +} \ No newline at end of file diff --git a/shop/section.go b/shop/section.go new file mode 100644 index 0000000..a98a8ec --- /dev/null +++ b/shop/section.go @@ -0,0 +1,92 @@ +package shop + +import "github.com/ectrc/snow/aid" + +func NewStorefrontCatalogSection(name string, type_ StorefrontCatalogOfferEnum) *StorefrontCatalogSection { + return &StorefrontCatalogSection{ + Name: name, + SectionType: type_, + Offers: make([]interface{}, 0), + } +} + +func (s *StorefrontCatalogSection) GenerateFortniteCatalogSectionResponse() aid.JSON { + catalogEntiresResponse := []aid.JSON{} + + for _, entry := range s.Offers { + switch s.SectionType { + case StorefrontCatalogOfferEnumItem: + s := entry.(*StorefrontCatalogOfferTypeItem) + catalogEntiresResponse = append(catalogEntiresResponse, s.GenerateFortniteCatalogOfferResponse()) + case StorefrontCatalogOfferEnumCurrency: + s := entry.(*StorefrontCatalogOfferTypeCurrency) + catalogEntiresResponse = append(catalogEntiresResponse, s.GenerateFortniteCatalogOfferResponse()) + case StorefrontCatalogOfferEnumStarterKit: + s := entry.(*StorefrontCatalogOfferTypeStarterKit) + catalogEntiresResponse = append(catalogEntiresResponse, s.GenerateFortniteCatalogOfferResponse()) + case StorefrontCatalogOfferEnumBattlePass: + s := entry.(*StorefrontCatalogOfferTypeBattlePass) + catalogEntiresResponse = append(catalogEntiresResponse, s.GenerateFortniteCatalogOfferResponse()) + } + } + + return aid.JSON{ + "name": s.Name, + "catalogEntries": catalogEntiresResponse, + } +} + +func (s *StorefrontCatalogSection) GetGroupedOffersLength() int { + if s.SectionType != StorefrontCatalogOfferEnumItem { + return len(s.Offers) + } + + newOffers := []*StorefrontCatalogOfferTypeItem{} + for _, offer := range s.Offers { + newOffers = append(newOffers, offer.(*StorefrontCatalogOfferTypeItem)) + } + + groupedOffers := map[string][]*StorefrontCatalogOfferTypeItem{} + for _, offer := range newOffers { + if _, ok := groupedOffers[offer.Categories[0]]; !ok { + groupedOffers[offer.Categories[0]] = []*StorefrontCatalogOfferTypeItem{} + } + + groupedOffers[offer.Categories[0]] = append(groupedOffers[offer.Categories[0]], offer) + } + + return len(groupedOffers) +} + +func (s *StorefrontCatalogSection) AddOffer(offer interface{}) { + s.Offers = append(s.Offers, offer) +} + +func (s *StorefrontCatalogSection) GetOfferByID(offerID string) (interface{}, StorefrontCatalogOfferEnum) { + for _, offer := range s.Offers { + switch s.SectionType { + case StorefrontCatalogOfferEnumItem: + o := offer.(*StorefrontCatalogOfferTypeItem) + if o.GetOfferID() == offerID { + return o, StorefrontCatalogOfferEnumItem + } + case StorefrontCatalogOfferEnumCurrency: + o := offer.(*StorefrontCatalogOfferTypeCurrency) + if o.GetOfferID() == offerID { + return o, StorefrontCatalogOfferEnumCurrency + } + case StorefrontCatalogOfferEnumStarterKit: + o := offer.(*StorefrontCatalogOfferTypeStarterKit) + if o.GetOfferID() == offerID { + return o, StorefrontCatalogOfferEnumStarterKit + } + case StorefrontCatalogOfferEnumBattlePass: + o := offer.(*StorefrontCatalogOfferTypeBattlePass) + if o.GetOfferID() == offerID { + return o, StorefrontCatalogOfferEnumBattlePass + } + } + } + + return nil, -1 +} \ No newline at end of file diff --git a/shop/shop.go b/shop/shop.go new file mode 100644 index 0000000..0eb4c61 --- /dev/null +++ b/shop/shop.go @@ -0,0 +1,206 @@ +package shop + +import ( + "fmt" + "math/rand" + "regexp" + "strings" + + "github.com/ectrc/snow/aid" + "github.com/ectrc/snow/fortnite" +) + +func GetShop() *StorefrontCatalog { + aid.SetRandom(rand.New(rand.NewSource(int64(aid.Config.Fortnite.ShopSeed) + aid.CurrentDayUnix()))) + shop := NewStorefrontCatalog() + + dailySection := NewStorefrontCatalogSection("BRDailyStorefront", StorefrontCatalogOfferEnumItem) + weeklySection := NewStorefrontCatalogSection("BRWeeklyStorefront", StorefrontCatalogOfferEnumItem) + moneySection := NewStorefrontCatalogSection("CurrencyStorefront", StorefrontCatalogOfferEnumCurrency) + kitSection := NewStorefrontCatalogSection("BRStarterKits", StorefrontCatalogOfferEnumStarterKit) + bookSection := NewStorefrontCatalogSection(fmt.Sprintf("BRSeason%d", aid.Config.Fortnite.Season), StorefrontCatalogOfferEnumBattlePass) + shop.AddSections(bookSection, dailySection, weeklySection, moneySection, kitSection) + + bookDefaultOffer := newBookOffer(aid.Ternary[string](fortnite.DataClient.SnowSeason.DefaultOfferID != "", fortnite.DataClient.SnowSeason.DefaultOfferID, "book://"+ aid.Hash([]byte(aid.RandomString(32)))), 950, 0, &StorefrontCatalogOfferGrant{TemplateID: "Snow:BattlePass", Quantity: 1, ProfileType: "athena"}) + bookDefaultOffer.Diplay.Title = "Battle Pass" + bookDefaultOffer.Diplay.ShortDescription = "Claim your Season 8 Battle Pass!" + bookDefaultOffer.Diplay.Description = "Fortnite Season 8\n\nInstantly get these items valued at over 3,500 V-Bucks.\n • Blackheart Progressive Outfit\n • Hybrid Progressive Outfit\n • 50% Bonus Season Match XP\n • 10% Bonus Season Friend Match XP\n • Extra Weekly Challenges\n\nPlay to level up your Battle Pass, unlocking over 100 rewards (typically takes 75 to 150 hours of play).\n • Sidewinder and 4 more Outfits\n • 1,300 V-Bucks\n • 7 Emotes\n • 6 Wraps\n • 2 Pets\n • 5 Harvesting Tools\n • 4 Gliders\n • 4 Back Blings\n • 5 Contrails\n • 14 Sprays\n • 3 Music Tracks\n • 1 Toy\n • 20 Loading Screens\n • and so much more!\nWant it all faster? You can use V-Bucks to buy tiers any time!" + bookDefaultOffer.Meta.DisplayAssetPath = fmt.Sprintf("/Game/Catalog/DisplayAssets/DA_BR_Season%d_BattlePass.DA_BR_Season%d_BattlePass", aid.Config.Fortnite.Season, aid.Config.Fortnite.Season) + bookDefaultOffer.Meta.Priority = 1 + bookSection.AddOffer(bookDefaultOffer) + + bookBundleOffer := newBookOffer(aid.Ternary[string](fortnite.DataClient.SnowSeason.BundleOfferID != "", fortnite.DataClient.SnowSeason.BundleOfferID, "book://"+ aid.Hash([]byte(aid.RandomString(32)))), 4700, 1850, []*StorefrontCatalogOfferGrant{ + {TemplateID: "Snow:BattlePass", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "AccountResource:AthenaBattleStar", Quantity: 250, ProfileType: "athena"}, + }...) + bookBundleOffer.Diplay.Title = "Battle Bundle" + bookBundleOffer.Diplay.ShortDescription = "Claim your Season 8 Battle Pass + 25 Tiers!" + bookBundleOffer.Diplay.Description = "Fortnite Season 8\n\nInstantly get these items valued at over 10,000 V-Bucks.\n • Blackheart Progressive Outfit\n • Hybrid Progressive Outfit\n • Sidewinder Outfit\n • Tropical Camo Wrap\n • Woodsy Pet\n • Sky Serpents Glider\n • Cobra Back Bling\n • Flying Standard Contrail\n • 300 V-Bucks\n • 1 Music Track\n • 70% Bonus Season Match XP\n • 20% Bonus Season Friend Match XP\n • Extra Weekly Challenges\n • and more!\n\nPlay to level up your Battle Pass, unlocking over 75 rewards (typically takes 75 to 150 hours of play).\n • 4 more Outfits\n • 1,000 V-Bucks\n • 6 Emotes\n • 5 Wraps\n • 3 Gliders\n • 3 Back Blings\n • 4 Harvesting Tools\n • 4 Contrails\n • 1 Pet\n • 12 Sprays\n • 2 Music Tracks\n • and so much more!\nWant it all faster? You can use V-Bucks to buy tiers any time!" + bookBundleOffer.Meta.DisplayAssetPath = fmt.Sprintf("/Game/Catalog/DisplayAssets/DA_BR_Season%d_BattlePassWithLevels.DA_BR_Season%d_BattlePassWithLevels", aid.Config.Fortnite.Season, aid.Config.Fortnite.Season) + bookBundleOffer.Meta.Priority = 0 + bookSection.AddOffer(bookBundleOffer) + + bookLevelOffer := newBookOffer(aid.Ternary[string](fortnite.DataClient.SnowSeason.TierOfferID != "", fortnite.DataClient.SnowSeason.TierOfferID, "book://"+ aid.Hash([]byte(aid.RandomString(32)))), 150, 150, &StorefrontCatalogOfferGrant{TemplateID: "AccountResource:AthenaBattleStar", Quantity: 10, ProfileType: "athena"}) + bookSection.AddOffer(bookLevelOffer) + + for len(dailySection.Offers) <= fortnite.DataClient.GetStorefrontDailyItemCount(aid.Config.Fortnite.Season) { + offer := newItemOffer(fortnite.GetRandomItemWithDisplayAssetOfNotType("AthenaCharacter"), true, true) + offer.Meta.SectionID = "Daily" + dailySection.AddOffer(offer) + } + + for weeklySection.GetGroupedOffersLength() < fortnite.DataClient.GetStorefrontWeeklySetCount(aid.Config.Fortnite.Season) { + set := fortnite.GetRandomSet() + for _, item := range set.Items { + offer := newItemOffer(item, true, true) + offer.Meta.SectionID = "Featured" + offer.Categories = append(offer.Categories, set.BackendName) + weeklySection.AddOffer(offer) + } + } + + xp := NewItemCatalogOffer() + xp.OfferID = "item://AthenaSeasonalXP" + xp.Meta.TileSize = "Small" + xp.Meta.Giftable = false + xp.Meta.Refundable = false + xp.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_FoundersPack_4_5.DA_FoundersPack_4_5" + xp.Rewards = append(xp.Rewards, &StorefrontCatalogOfferGrant{ + TemplateID: "AccountResource:AthenaSeasonalXP", + Quantity: 100000, + ProfileType: "athena", + }) + xp.Price.PriceType = StorefrontCatalogOfferPriceTypeMtxCurrency + xp.Price.SaleType = StorefrontCatalogOfferPriceSaleTypeNone + xp.Price.OriginalPrice = 1000 + xp.Price.FinalPrice = 1000 + weeklySection.AddOffer(xp) + + moneySection.AddOffer(newMoneyOffer(1000, 0, "https://cdn1.epicgames.com/offer/fn/EGS_VBucks_1000_1200x1600-c8a13f66ba88744d5216f884855e2a4d", 3)) + moneySection.AddOffer(newMoneyOffer(2800, 300, "https://cdn1.epicgames.com/offer/fn/EGS_VBucks_2800_1200x1600-055112a56c0fb d65989470ece7c653f", 2)) + moneySection.AddOffer(newMoneyOffer(7500, 1500, "https://cdn1.epicgames.com/offer/fn/EGS_VBucks_5000_1200x1600-8ea53bb4ea3d75821153075df8e3ca95", 1)) + moneySection.AddOffer(newMoneyOffer(13500, 3500, "https://cdn1.epicgames.com/offer/fn/EGS_VBucks_13500_1200x1600-39489a289769bc6c1d14f4a8b53b48f4", 0)) + + lagunaKit := newKitOffer("The Laguna Pack", 499, 8, []*StorefrontCatalogOfferGrant{ + {TemplateID: "AthenaCharacter:CID_367_Athena_Commando_F_Tropical", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "AthenaBackpack:BID_231_TropicalFemale", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "AthenaItemWrap:Wrap_033_TropicalGirl", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "Currency:MtxPurchased", Quantity: 600, ProfileType: "common_core"}, + }...) + lagunaKit.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_367_Athena_Commando_F_Tropical.DA_Featured_CID_367_Athena_Commando_F_Tropical" + lagunaKit.Meta.FeaturedImageURL = "https://fortnite-api.com/images/cosmetics/br/CID_367_Athena_Commando_F_Tropical/icon.png" + kitSection.AddOffer(lagunaKit) + + ikonikKit := newKitOffer("Ikonik Pack", 3999, 8, []*StorefrontCatalogOfferGrant{ + {TemplateID: "AthenaCharacter:CID_313_Athena_Commando_M_KpopFashion", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "AthenaDance:EID_KPopDance03", Quantity: 1, ProfileType: "athena"}, + {TemplateID: "Currency:MtxPurchased", Quantity: 600, ProfileType: "common_core"}, + }...) + ikonikKit.Meta.DisplayAssetPath = "/Game/Catalog/DisplayAssets/DA_Featured_CID_313_Athena_Commando_M_KpopFashion.DA_Featured_CID_313_Athena_Commando_M_KpopFashion" + ikonikKit.Meta.FeaturedImageURL = "https://fortnite-api.com/images/cosmetics/br/CID_313_Athena_Commando_M_KpopFashion/icon.png" + kitSection.AddOffer(ikonikKit) + + return shop +} + +func newItemOffer(item *fortnite.APICosmeticDefinition, addAssets, giftable bool) *StorefrontCatalogOfferTypeItem { + displayAsset := regexp.MustCompile(`[^/]+$`).FindString(item.DisplayAssetPath) + + offer := NewItemCatalogOffer() + offer.Meta.TileSize = aid.Ternary[string](item.Type.BackendValue == "AthenaCharacter", "Small", "Normal") + offer.Meta.Giftable = giftable + offer.Meta.Refundable = true + if addAssets { + offer.Meta.DisplayAssetPath = aid.Ternary[string](displayAsset != "", "/Game/Catalog/DisplayAssets/" + displayAsset + "." + displayAsset, "") + offer.Meta.NewDisplayAssetPath = aid.Ternary[string](item.NewDisplayAssetPath != "", "/Game/Catalog/NewDisplayAssets/" + item.NewDisplayAssetPath + "." + item.NewDisplayAssetPath, "") + } + + offer.Rewards = append(offer.Rewards, &StorefrontCatalogOfferGrant{ + TemplateID: item.Type.BackendValue + ":" + item.ID, + Quantity: 1, + ProfileType: "athena", + }) + + offer.Price.PriceType = StorefrontCatalogOfferPriceTypeMtxCurrency + offer.Price.SaleType = StorefrontCatalogOfferPriceSaleTypeNone + offer.Price.OriginalPrice = fortnite.DataClient.GetStorefrontCosmeticOfferPrice(item.Rarity.BackendValue, item.Type.BackendValue) + offer.Price.FinalPrice = offer.Price.OriginalPrice + + offer.OfferID = fmt.Sprintf("item://%s", aid.Hash([]byte(offer.OfferID))) + + return offer +} + +func newMoneyOffer(real, bonus int, imgUrl string, position int) *StorefrontCatalogOfferTypeCurrency { + format := aid.FormatNumber(real) + offer := NewCurrencyCatalogOffer() + + offer.Meta.IconSize = "Small" + offer.Meta.CurrencyAnalyticsName = fmt.Sprintf("MtxPack%d", real) + offer.Meta.OriginalOffer = real + offer.Meta.ExtraBonus = bonus + offer.Meta.DisplayAssetPath = fmt.Sprintf("/Game/Catalog/DisplayAssets/DA_MtxPack%d.DA_MtxPack%d", real, real) + offer.Meta.FeaturedImageURL = imgUrl + offer.Meta.Priority = position + + offer.Diplay.Title = fmt.Sprintf("%s V-Bucks", format) + offer.Diplay.Description = fmt.Sprintf("Buy %s Fortnite V-Bucks, the in-game currency that can be spent in Fortnite Battle Royale and Creative modes. You can purchase new customization items like Outfits, Gliders, Pickaxes, Emotes, Wraps and the latest season's Battle Pass! Gliders and Contrails may not be used in Save the World mode.", format) + offer.Diplay.LongDescription = fmt.Sprintf("Buy %s Fortnite V-Bucks, the in-game currency that can be spent in Fortnite Battle Royale and Creative modes. You can purchase new customization items like Outfits, Gliders, Pickaxes, Emotes, Wraps and the latest season's Battle Pass! Gliders and Contrails may not be used in Save the World mode.\n\nAll V-Bucks purchased on the Epic Games Store are not redeemable or usable on Nintendo Switch™.", format) + + offer.Price.PriceType = StorefrontCatalogOfferPriceTypeRealMoney + offer.Price.BasePrice = float64(fortnite.DataClient.GetStorefrontCurrencyOfferPrice("GBP", real)) + offer.Price.LocalPrice = float64(fortnite.DataClient.GetStorefrontCurrencyOfferPrice("USD", real)) + + offer.Rewards = append(offer.Rewards, &StorefrontCatalogOfferGrant{ + TemplateID: "Currency:MtxPurchased", + Quantity: real, + ProfileType: "common_core", + }) + + return offer +} + +func newKitOffer(title string, basePrice, season int, rewards ...*StorefrontCatalogOfferGrant) *StorefrontCatalogOfferTypeStarterKit { + description := fmt.Sprintf("Jump into Fortnite Battle Royale with the %s. Includes:\n\n- 600 V-Bucks", strings.ReplaceAll(title, "The ", "")) + for _, reward := range rewards { + item := fortnite.DataClient.FortniteItems[strings.Split(reward.TemplateID, ":")[1]] + if item != nil { + description += fmt.Sprintf("\n- %s %s", item.Name, item.Type.DisplayValue) + } + } + + offer := NewStarterKitCatalogOffer() + + offer.Meta.ReleaseSeason = season + offer.Meta.OriginalOffer = 600 + offer.Meta.ExtraBonus = 100 + + offer.Diplay.Title = title + offer.Diplay.Description = description + offer.Diplay.LongDescription = fmt.Sprintf("%s\n\nV-Bucks are an in-game currency that can be spent in both the Battle Royale PvP mode and the Save the World PvE campaign. In Battle Royale, you can use V-bucks to purchase new customization items like outfits, emotes, pickaxes, gliders, and more! In Save the World you can purchase Llama Pinata card packs that contain weapon, trap and gadget schematics as well as new Heroes and more! \n\nNote: Items do not transfer between the Battle Royale mode and the Save the World campaign.", description) + + offer.Price.PriceType = StorefrontCatalogOfferPriceTypeRealMoney + offer.Price.BasePrice = float64(fortnite.DataClient.GetStorefrontLocalizedOfferPrice("GBP", basePrice)) + offer.Price.LocalPrice = float64(fortnite.DataClient.GetStorefrontLocalizedOfferPrice("USD", basePrice)) + + offer.Rewards = rewards + + return offer +} + +func newBookOffer(customId string, ogPrice, finalprice int, rewards ...*StorefrontCatalogOfferGrant) *StorefrontCatalogOfferTypeBattlePass { + offer := NewBattlePassCatalogOffer() + offer.OfferID = customId + + offer.Meta.TileSize = "Normal" + offer.Meta.SectionID = "BattlePass" + + offer.Price.PriceType = StorefrontCatalogOfferPriceTypeMtxCurrency + offer.Price.SaleType = StorefrontCatalogOfferPriceSaleTypeStrikethrough + offer.Price.OriginalPrice = ogPrice + offer.Price.FinalPrice = finalprice + + offer.Rewards = rewards + + return offer +} \ No newline at end of file diff --git a/shop/types.go b/shop/types.go new file mode 100644 index 0000000..fca430a --- /dev/null +++ b/shop/types.go @@ -0,0 +1,73 @@ +package shop + +import "github.com/ectrc/snow/aid" + +type ShopGrantProfileType string +const ShopGrantProfileTypeAthena ShopGrantProfileType = "athena" +const ShopGrantProfileTypeCommonCore ShopGrantProfileType = "common_core" + +type StorefrontCatalogOfferGrant struct { + TemplateID string + Quantity int + ProfileType ShopGrantProfileType +} + +type StorefrontCatalogOfferPriceType string +const StorefrontCatalogOfferPriceTypeMtxCurrency StorefrontCatalogOfferPriceType = "MtxCurrency" +const StorefrontCatalogOfferPriceTypeRealMoney StorefrontCatalogOfferPriceType = "RealMoney" + +type StorefrontCatalogOfferPriceSaleType string +const StorefrontCatalogOfferPriceSaleTypeNone StorefrontCatalogOfferPriceSaleType = "" +const StorefrontCatalogOfferPriceSaleTypeAmountOff StorefrontCatalogOfferPriceSaleType = "AmountOff" +const StorefrontCatalogOfferPriceSaleTypeStrikethrough StorefrontCatalogOfferPriceSaleType = "Strikethrough" + +type StorefrontCatalogOfferPriceMtxCurrency struct { + PriceType StorefrontCatalogOfferPriceType + SaleType StorefrontCatalogOfferPriceSaleType + OriginalPrice int + FinalPrice int +} + +type StorefrontCatalogOfferPriceRealMoney struct { + PriceType StorefrontCatalogOfferPriceType + SaleType StorefrontCatalogOfferPriceSaleType + BasePrice float64 + LocalPrice float64 +} + +type OfferDisplay struct { + Title string + Description string + ShortDescription string + LongDescription string +} + +type StorefrontCatalogOfferEnum int +const StorefrontCatalogOfferEnumItem StorefrontCatalogOfferEnum = 0 +const StorefrontCatalogOfferEnumCurrency StorefrontCatalogOfferEnum = 1 +const StorefrontCatalogOfferEnumStarterKit StorefrontCatalogOfferEnum = 2 +const StorefrontCatalogOfferEnumBattlePass StorefrontCatalogOfferEnum = 3 + +type StorefrontCatalogOfferGeneric interface { + StorefrontCatalogOfferTypeItem | StorefrontCatalogOfferTypeCurrency | StorefrontCatalogOfferTypeStarterKit | StorefrontCatalogOfferTypeBattlePass +} + +type StorefrontCatalogOffer[T StorefrontCatalogOfferGeneric] interface { + GetOffer() *T + GetOfferID() string + GetOfferType() StorefrontCatalogOfferEnum + GetRewards() []*StorefrontCatalogOfferGrant + GenerateFortniteCatalogOfferResponse() aid.JSON + GenerateFortniteBulkOffersResponse() aid.JSON +} + +var storefrontCatalogOfferPriceMultiplier = map[string]float64{ + "USD": 1.2503128911, + "GBP": 1.0, +} + +type StorefrontCatalogSection struct { + SectionType StorefrontCatalogOfferEnum + Name string + Offers []interface{} // *StorefrontCatalogOfferTypeItem | *StorefrontCatalogOfferTypeCurrency | *StorefrontCatalogOfferTypeStarterKit | *StorefrontCatalogOfferTypeBattlePass +} \ No newline at end of file diff --git a/socket/events.go b/socket/events.go index bfcf3a0..2f8e92e 100644 --- a/socket/events.go +++ b/socket/events.go @@ -14,7 +14,11 @@ func EmitGiftReceived(person *person.Person) { } s.JabberSendMessageToPerson(aid.JSON{ - "payload": aid.JSON{}, + "payload": aid.JSON{ + "gifts": []aid.JSON{{ + "Wahgsdhjgasjkd": "Wahgsdhjgasjkd", + }}, + }, "type": "com.epicgames.gift.received", "timestamp": time.Now().Format("2006-01-02T15:04:05.999Z"), }) diff --git a/socket/jabber.go b/socket/jabber.go index 946c39a..0a84d04 100644 --- a/socket/jabber.go +++ b/socket/jabber.go @@ -3,6 +3,7 @@ package socket import ( "fmt" "reflect" + "time" "github.com/beevik/etree" "github.com/ectrc/snow/aid" @@ -29,7 +30,7 @@ func HandleNewJabberSocket(identifier string) { if !ok { return } - defer JabberSockets.Delete(socket.ID) + defer socket.Remove() for { _, message, failed := socket.Connection.ReadMessage() @@ -57,9 +58,6 @@ func JabberSocketOnMessage(socket *Socket[JabberData], message []byte) { } func jabberStreamHandler(socket *Socket[JabberData], parsed *etree.Document) error { - socket.Write([]byte(``)) - // socket.Write([]byte(``)) - // socket.Write([]byte(``)) return nil } @@ -106,7 +104,9 @@ func jabberIqSetHandler(socket *Socket[JabberData], parsed *etree.Document) erro } func jabberIqGetHandler(socket *Socket[JabberData], parsed *etree.Document) error { - socket.Write([]byte(``)) + socket.Write([]byte(` + + `+ jabberSocket.Data.LastPresence +` `)) +} + +func init() { + go func() { + timer := time.NewTicker(5 * time.Second) + + for { + <-timer.C + + JabberSockets.Range(func(key string, value *Socket[JabberData]) bool { + value.Write([]byte(` + + `)) + return true + }) + } + }() } \ No newline at end of file diff --git a/socket/socket.go b/socket/socket.go index 947ca2f..c501e2e 100644 --- a/socket/socket.go +++ b/socket/socket.go @@ -1,6 +1,7 @@ package socket import ( + "reflect" "sync" "github.com/ectrc/snow/aid" @@ -31,7 +32,20 @@ func (s *Socket[T]) Write(payload []byte) { s.M.Lock() defer s.M.Unlock() - s.Connection.WriteMessage(websocket.TextMessage, payload) + err := s.Connection.WriteMessage(websocket.TextMessage, payload) + if err != nil { + s.Remove() + } +} + +func (s *Socket[T]) Remove() { + reflectType := reflect.TypeOf(s.Data).String() + switch reflectType { + case "*socket.JabberData": + JabberSockets.Delete(s.ID) + case "*socket.MatchmakerData": + MatchmakerSockets.Delete(s.ID) + } } func newSocket[T JabberData | MatchmakerData](conn WebSocket, data ...T) *Socket[T] { diff --git a/storage/amazon.go b/storage/amazon.go index d6a51da..133d9ee 100644 --- a/storage/amazon.go +++ b/storage/amazon.go @@ -62,7 +62,7 @@ func (a *AmazonClient) GetAllUserFiles() ([]string, error) { func (a *AmazonClient) CreateUserFile(fileName string, data []byte) error { _, err := a.client.PutObject(context.TODO(), &s3.PutObjectInput{ Bucket: aws.String(a.ClientSettingsBucket), - Key: aws.String(fileName), + Key: aws.String("client/"+fileName), Body: bytes.NewReader(data), }) if err != nil { @@ -75,7 +75,7 @@ func (a *AmazonClient) CreateUserFile(fileName string, data []byte) error { func (a *AmazonClient) GetUserFile(fileName string) ([]byte, error) { getObjectOutput, err := a.client.GetObject(context.TODO(), &s3.GetObjectInput{ Bucket: aws.String(a.ClientSettingsBucket), - Key: aws.String(fileName), + Key: aws.String("client/"+fileName), }) if err != nil { return nil, err diff --git a/storage/embeds.go b/storage/embeds.go index bd2d409..36883f5 100644 --- a/storage/embeds.go +++ b/storage/embeds.go @@ -13,6 +13,10 @@ var ( Assets embed.FS ) +type snowFS struct { + embed.FS +} + func Asset(file string) (*[]byte) { data, err := Assets.ReadFile("mem/" + strings.ToLower(file)) if err != nil { diff --git a/storage/hotfix.go b/storage/hotfix.go index 5d0a061..6830f4f 100644 --- a/storage/hotfix.go +++ b/storage/hotfix.go @@ -16,10 +16,6 @@ func GetDefaultEngine() []byte { realPort := fmt.Sprintf("%d", portNumber) str := ` -[OnlineSubsystemMcp.OnlinePaymentServiceMcp Fortnite] -Domain="launcher-website-prod.ak.epicgames.com" -BasePath="/logout?redirectUrl=https%3A%2F%2Fwww.unrealengine.com%2Fid%2Flogout%3FclientId%3Dxyza7891KKDWlczTxsyy7H3ExYgsNT4Y%26responseType%3Dcode%26redirectUrl%3Dhttps%253A%252F%252Ftesting-site.neonitedev.live%252Fid%252Flogin%253FredirectUrl%253Dhttps%253A%252F%252Ftesting-site.neonitedev.live%252Fpurchase%252Facquire&path=" - [XMPP] bEnableWebsockets=true @@ -36,42 +32,59 @@ FortMatchmakingV2.EnableContentBeacon=0 NumTestsPerRegion=5 PingTimeout=3.0 +[/Script/Qos.QosRegionManager] +NumTestsPerRegion=5 +PingTimeout=3.0 +!RegionDefinitions=ClearArray ++RegionDefinitions=(DisplayName=NSLOCTEXT("MMRegion", "Europe", "Europe"), RegionId="EU", bEnabled=true, bVisible=true, bAutoAssignable=true) ++RegionDefinitions=(DisplayName=NSLOCTEXT("MMRegion", "North America", "North America"), RegionId="NA", bEnabled=true, bVisible=true, bAutoAssignable=true) ++RegionDefinitions=(DisplayName=NSLOCTEXT("MMRegion", "Oceania", "Oceania"), RegionId="OCE", bEnabled=true, bVisible=true, bAutoAssignable=true) +!DatacenterDefinitions=ClearArray ++DatacenterDefinitions=(Id="DE", RegionId="EU", bEnabled=true, Servers[0]=(Address="142.132.145.234", Port=22222)) ++DatacenterDefinitions=(Id="VA", RegionId="NA", bEnabled=true, Servers[0]=(Address="69.10.34.38", Port=22222)) ++DatacenterDefinitions=(Id="SYD", RegionId="OCE", bEnabled=true, Servers[0]=(Address="139.99.209.91", Port=22222)) +!Datacenters=ClearArray ++Datacenters=(DisplayName=NSLOCTEXT("MMRegion", "Europe", "Europe"), RegionId="EU", bEnabled=true, bVisible=true, bBeta=false, Servers[0]=(Address="142.132.145.234", Port=22222)) ++Datacenters=(DisplayName=NSLOCTEXT("MMRegion", "North America", "North America"), RegionId="NA", bEnabled=true, bVisible=true, bBeta=false, Servers[0]=(Address="69.10.34.38", Port=22222)) ++Datacenters=(DisplayName=NSLOCTEXT("MMRegion", "Oceania", "Oceania"), RegionId="OCE", bEnabled=true, bVisible=true, bBeta=false, Servers[0]=(Address="139.99.209.91", Port=22222)) + [LwsWebSocket] bDisableCertValidation=true bDisableDomainWhitelist=true +[/Script/Engine.NetworkSettings] +n.VerifyPeer=false + [WinHttpWebSocket] +bDisableCertValidation=true bDisableDomainWhitelist=true` if aid.Config.Fortnite.Season <= 2 { str += ` - [OnlineSubsystemMcp.Xmpp] bUsePlainTextAuth=true bUseSSL=false Protocol=tcp -ServerAddr="`+ aid.Config.API.Host + `" +ServerAddr="`+ aid.Config.API.XMPP.Host + aid.Config.API.XMPP.Port + `" ServerPort=`+ realPort + ` [OnlineSubsystemMcp.Xmpp Prod] bUsePlainTextAuth=true bUseSSL=false Protocol=tcp -ServerAddr="`+ aid.Config.API.Host + `" +ServerAddr="`+ aid.Config.API.XMPP.Host + aid.Config.API.XMPP.Port + `" ServerPort=`+ realPort } else { str += ` [OnlineSubsystemMcp.Xmpp] -bUsePlainTextAuth=true bUseSSL=false Protocol=ws -ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?SNOW_SOCKET_CONNECTION" +ServerAddr="ws://`+ aid.Config.API.XMPP.Host + aid.Config.API.XMPP.Port +`/?SNOW_SOCKET_CONNECTION" [OnlineSubsystemMcp.Xmpp Prod] -bUsePlainTextAuth=true bUseSSL=false Protocol=ws -ServerAddr="ws://`+ aid.Config.API.Host + aid.Config.API.Port +`/?SNOW_SOCKET_CONNECTION"` +ServerAddr="ws://`+ aid.Config.API.XMPP.Host + aid.Config.API.XMPP.Port +`/?SNOW_SOCKET_CONNECTION"` } return []byte(str) @@ -93,12 +106,23 @@ bShouldCheckIfPlatformAllowed=false [EpicPurchaseFlow] bUsePaymentWeb=false -CI="http://localhost:5173/purchase" -GameDev="http://localhost:5173/purchase" -Stage="http://127.0.0.1:5173/purchase" -Prod="http://127.0.0.1:5173/purchase" +CI="http://127.0.0.1:3000/purchase" +GameDev="http://127.0.0.1:3000/purchase" +Stage="http://127.0.0.1:3000/purchase" +Prod="http://127.0.0.1:3000/purchase" UEPlatform="FNGame" +[/Script/FortniteGame.FortTextHotfixConfig] ++TextReplacements=(Category=Game, bIsMinimalPatch=True, Namespace="", Key="68ADE44C49B20BFF78677799BE68B0EE", NativeString="FORTNITEMARES", LocalizedStrings=(("en","BOOST PERKS"))) ++TextReplacements=(Category=Game, bIsMinimalPatch=True, Namespace="", Key="BE6B17BD456F3F13EEB2998AF91DC717", NativeString="THANKS FOR PLAYING!", LocalizedStrings=(("en","THANKS FOR SUPPORTING SNOW!"))) + +[/Script/FortniteGame.FortGameInstance] +!FrontEndPlaylistData=ClearArray ++FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSolo, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=True, bVisibleWhenDisabled=True, bDisplayAsNew=False, CategoryIndex=0, bDisplayAsLimitedTime=False, DisplayPriority=0)) ++FrontEndPlaylistData=(PlaylistName=Playlist_DefaultDuo, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=True, bVisibleWhenDisabled=True, bDisplayAsNew=False, CategoryIndex=0, bDisplayAsLimitedTime=False, DisplayPriority=1)) ++FrontEndPlaylistData=(PlaylistName=Playlist_DefaultSquad, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=True, bVisibleWhenDisabled=True, bDisplayAsNew=False, CategoryIndex=0, bDisplayAsLimitedTime=False, DisplayPriority=2)) ++FrontEndPlaylistData=(PlaylistName=Playlist_ShowdownAlt_Solo, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=False, bVisibleWhenDisabled=True, bDisplayAsNew=False, CategoryIndex=1, bDisplayAsLimitedTime=False, DisplayPriority=0)) ++FrontEndPlaylistData=(PlaylistName=Playlist_ShowdownAlt_Duos, PlaylistAccess=(bEnabled=True, bIsDefaultPlaylist=False, bVisibleWhenDisabled=True, bDisplayAsNew=False, CategoryIndex=1, bDisplayAsLimitedTime=False, DisplayPriority=1)) `)} func GetDefaultRuntime() []byte {return []byte(` @@ -108,6 +132,11 @@ func GetDefaultRuntime() []byte {return []byte(` ;+DisabledFrontendNavigationTabs=(TabName="Showdown",TabState=EFortRuntimeOptionTabState::Hidden) ;+DisabledFrontendNavigationTabs=(TabName="AthenaStore",TabState=EFortRuntimeOptionTabState::Hidden) +[/Script/FortniteGame.FortRuntimeOptions] +bForceBRMode=True +bSkipSubgameSelect=True +bEnableInGameMatchmaking=True + bEnableGlobalChat=true bDisableGifting=false bDisableGiftingPC=false diff --git a/storage/mem/controller.js b/storage/mem/controller.js new file mode 100644 index 0000000..470ed59 --- /dev/null +++ b/storage/mem/controller.js @@ -0,0 +1,98 @@ +/** + * + * @typedef {Object} PurchaseFlow + * @property {(reason: string) => Promise} requestclose + * @property {(receipt: {}) => Promise} receipt + * @property {(browserId: string, url: string) => Promise} launchvalidatedexternalbrowserurl + * @property {(url: string) => Promise} launchexternalbrowserurl + * @property {(browserId: string) => Promise} getexternalbrowserpath + * @property {(browserId: string) => Promise} getexternalbrowsername + * @property {(url: string) => Promise} getdefaultexternalbrowserid + * + * @typedef {Object} Engine + * @property {PurchaseFlow} purchaseflow + * + * @typedef {Object} Offer + * @property {{ + * displayName: string + * }} user + * @property {{ + * id: string + * name: string + * price: number + * imageUrl: string + * type: string + * }} offer + */ + +function getCookie(name) { + var value = "; " + document.cookie; + var parts = value.split("; " + name + "="); + if (parts.length === 2) return parts.pop().split(";").shift(); +} + +/** + * @param {Engine} engine + * @returns + */ +const main = async (engine) => { + const close = document.getElementById("close"); + const purchase = document.getElementById("purchaseOfferButton"); + const pf = engine.purchaseflow ? engine.purchaseflow : null; + if (!pf) return; + + const offerId = new URLSearchParams(window.location.search).get("offers"); + const offerResponse = await axios.get( + `http://127.0.0.1:3000/purchase/offer?offerId=${offerId}`, + { + headers: { + Authorization: getCookie("EPIC_BEARER_TOKEN"), + }, + } + ); + if (offerResponse.status !== 200) return pf.requestclose("LoadFailure"); + const offer = offerResponse.data; + + const image = document.getElementById("orderImage"); + image && (image.style.backgroundImage = `url(${offer.offer.imageUrl})`); + + const title = document.getElementById("orderName"); + title && (title.innerText = offer.offer.name); + + const price = document.getElementById("orderPrice"); + price && (price.innerText = "$" + offer.offer.price); + + const totalPrice = document.getElementById("orderTotalPrice"); + totalPrice && (totalPrice.innerText = "$" + offer.offer.price); + + const SubtotalPrice = document.getElementById("orderSubtotalPrice"); + SubtotalPrice && (SubtotalPrice.innerText = "$" + offer.offer.price); + + const displayName = document.getElementById("displayName"); + displayName && (displayName.innerText = offer.user.displayName); + + close && close.addEventListener("click", () => pf.requestclose("Escape")); + purchase && purchase.addEventListener("click", () => buy(pf, offer)); +}; + +/** + * @param {PurchaseFlow} pf + * @param {Offer} offer + */ +const buy = async (pf, offer) => { + const purchase = await axios.post( + `http://127.0.0.1:3000/purchase/offer`, + { + offerId: offer.offer.id, + type: offer.offer.type, + }, + { + headers: { + Authorization: getCookie("EPIC_BEARER_TOKEN"), + }, + } + ); + if (purchase.status !== 200) return pf.requestclose("PurchaseFailure"); + await pf.receipt(purchase.data.receipt); + await pf.requestclose("WasSuccessful"); +}; diff --git a/storage/mem/purchase.html b/storage/mem/purchase.html new file mode 100644 index 0000000..7e1aa04 --- /dev/null +++ b/storage/mem/purchase.html @@ -0,0 +1,510 @@ + + + + + + + + + Purchase Flow + + + + +
+
+ +
+
+
+

CHECKOUT

+
+
+

+
+
+
+

REVIEW AND PLACE ORDER

+

YOUR PAYMENT METHODS

+ +
+

+ Snow does not store your payment information. Your payment + information is stored securely by Sellix. +

+
+
+ + + +
+
+

ORDER SUMMARY

+ +
+ +
+
+
+

+

+
+
+ +
+
+

Price

+

+
+
+

VAT included where applicable

+
+
+
+

Total

+

+
+
+ +
+
+ + + +
+

+ Earn 50 V-Bucks with this purchase. Rewards are available + for use 14 days after purchase +

+
+ + +
+ +
+
+ + + + diff --git a/storage/postgres.go b/storage/postgres.go index 5db1243..0f71d39 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -49,6 +49,10 @@ func (s *PostgresStorage) MigrateAll() { s.Migrate(&DB_DiscordPerson{}, "Discords") s.Migrate(&DB_BanStatus{}, "Bans") s.Migrate(&DB_SeasonStat{}, "Stats") + s.Migrate(&DB_Receipt{}, "Receipts") + s.Migrate(&DB_ReceiptLoot{}, "ReceiptLoot") + s.Migrate(&DB_VariantToken{}, "VariantTokens") + s.Migrate(&DB_VariantTokenGrant{}, "VariantTokenGrants") } func (s *PostgresStorage) DropTables() { @@ -66,8 +70,12 @@ func (s *PostgresStorage) PreloadPerson() (tx *gorm.DB) { Preload("Profiles.Gifts"). Preload("Profiles.Gifts.Loot"). Preload("Profiles.Quests"). + Preload("Profiles.VariantTokens"). + Preload("Profiles.VariantTokens.VariantGrants"). Preload("Profiles.Purchases"). Preload("Profiles.Purchases.Loot"). + Preload("Receipts"). + Preload("Receipts.Loot"). Preload("Discord"). Preload("BanHistory"). Preload("Stats") @@ -220,6 +228,22 @@ func (s *PostgresStorage) DeleteGift(giftId string) { s.Postgres.Delete(&DB_Gift{}, "id = ?", giftId) } +func (s *PostgresStorage) SaveVariantToken(variantToken *DB_VariantToken) { + s.Postgres.Save(variantToken) +} + +func (s *PostgresStorage) DeleteVariantToken(variantTokenId string) { + s.Postgres.Delete(&DB_VariantToken{}, "id = ?", variantTokenId) +} + +func (s *PostgresStorage) SaveVariantTokenGrant(variantTokenGrant *DB_VariantTokenGrant) { + s.Postgres.Save(variantTokenGrant) +} + +func (s *PostgresStorage) DeleteVariantTokenGrant(variantTokenGrantId string) { + s.Postgres.Delete(&DB_VariantTokenGrant{}, "id = ?", variantTokenGrantId) +} + func (s *PostgresStorage) SaveAttribute(attribute *DB_Attribute) { s.Postgres.Save(attribute) } @@ -258,4 +282,28 @@ func (s *PostgresStorage) SaveBanStatus(banStatus *DB_BanStatus) { func (s *PostgresStorage) DeleteBanStatus(banStatusId string) { s.Postgres.Delete(&DB_BanStatus{}, "id = ?", banStatusId) +} + +func (s *PostgresStorage) SaveReceipt(receipt *DB_Receipt) { + s.Postgres.Save(receipt) +} + +func (s *PostgresStorage) DeleteReceipt(receiptId string) { + s.Postgres.Delete(&DB_Receipt{}, "id = ?", receiptId) +} + +func (s *PostgresStorage) SaveReceiptLoot(receiptLoot *DB_ReceiptLoot) { + s.Postgres.Save(receiptLoot) +} + +func (s *PostgresStorage) DeleteReceiptLoot(receiptLootId string) { + s.Postgres.Delete(&DB_ReceiptLoot{}, "id = ?", receiptLootId) +} + +func (s *PostgresStorage) SaveSeasonStats(seasonStats *DB_SeasonStat) { + s.Postgres.Save(seasonStats) +} + +func (s *PostgresStorage) DeleteSeasonStats(seasonId string) { + s.Postgres.Delete(&DB_SeasonStat{}, "id = ?", seasonId) } \ No newline at end of file diff --git a/storage/storage.go b/storage/storage.go index 2dae24d..f8fbe72 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -43,6 +43,11 @@ type Storage interface { SaveGift(gift *DB_Gift) DeleteGift(giftId string) + SaveVariantToken(variantToken *DB_VariantToken) + SaveVariantTokenGrant(variantTokenGrant *DB_VariantTokenGrant) + DeleteVariantToken(variantTokenId string) + DeleteVariantTokenGrant(variantTokenGrantId string) + SaveAttribute(attribute *DB_Attribute) DeleteAttribute(attributeId string) @@ -57,6 +62,14 @@ type Storage interface { SaveBanStatus(ban *DB_BanStatus) DeleteBanStatus(banId string) + + SaveReceipt(receipt *DB_Receipt) + SaveReceiptLoot(receiptLoot *DB_ReceiptLoot) + DeleteReceipt(receiptId string) + DeleteReceiptLoot(receiptLootId string) + + SaveSeasonStats(season *DB_SeasonStat) + DeleteSeasonStats(seasonId string) } type Repository struct { @@ -198,6 +211,22 @@ func (r *Repository) DeleteGift(giftId string) { r.Storage.DeleteGift(giftId) } +func (r *Repository) SaveVariantToken(variantToken *DB_VariantToken) { + r.Storage.SaveVariantToken(variantToken) +} + +func (r *Repository) SaveVariantTokenGrant(variantTokenGrant *DB_VariantTokenGrant) { + r.Storage.SaveVariantTokenGrant(variantTokenGrant) +} + +func (r *Repository) DeleteVariantToken(variantTokenId string) { + r.Storage.DeleteVariantToken(variantTokenId) +} + +func (r *Repository) DeleteVariantTokenGrant(variantTokenGrantId string) { + r.Storage.DeleteVariantTokenGrant(variantTokenGrantId) +} + func (r *Repository) SaveAttribute(attribute *DB_Attribute) { r.Storage.SaveAttribute(attribute) } @@ -236,4 +265,28 @@ func (r *Repository) SaveBanStatus(ban *DB_BanStatus) { func (r *Repository) DeleteBanStatus(banId string) { r.Storage.DeleteBanStatus(banId) +} + +func (r *Repository) SaveReceipt(receipt *DB_Receipt) { + r.Storage.SaveReceipt(receipt) +} + +func (r *Repository) SaveReceiptLoot(receiptLoot *DB_ReceiptLoot) { + r.Storage.SaveReceiptLoot(receiptLoot) +} + +func (r *Repository) DeleteReceipt(receiptId string) { + r.Storage.DeleteReceipt(receiptId) +} + +func (r *Repository) DeleteReceiptLoot(receiptLootId string) { + r.Storage.DeleteReceiptLoot(receiptLootId) +} + +func (r *Repository) SaveSeasonStats(season *DB_SeasonStat) { + r.Storage.SaveSeasonStats(season) +} + +func (r *Repository) DeleteSeasonStats(seasonId string) { + r.Storage.DeleteSeasonStats(seasonId) } \ No newline at end of file diff --git a/storage/tables.go b/storage/tables.go index 6bdd066..dd09d56 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -15,6 +15,7 @@ type DB_Person struct { DisplayName string RefundTickets int Permissions int64 + Receipts []DB_Receipt `gorm:"foreignkey:PersonID"` Profiles []DB_Profile `gorm:"foreignkey:PersonID"` Stats []DB_SeasonStat `gorm:"foreignkey:PersonID"` Discord DB_DiscordPerson `gorm:"foreignkey:PersonID"` @@ -35,6 +36,32 @@ func (DB_Relationship) TableName() string { return "Relationships" } +type DB_Receipt struct { + ID string `gorm:"primary_key"` + PersonID string `gorm:"index"` + OfferID string + PurchaseDate int64 + TotalPaid int + State string + Loot []DB_ReceiptLoot `gorm:"foreignkey:ReceiptID"` +} + +func (DB_Receipt) TableName() string { + return "Receipts" +} + +type DB_ReceiptLoot struct { + ID string `gorm:"primary_key"` + ReceiptID string `gorm:"index"` + TemplateID string + Quantity int + ProfileType string +} + +func (DB_ReceiptLoot) TableName() string { + return "ReceiptLoot" +} + type DB_Profile struct { ID string `gorm:"primary_key"` PersonID string `gorm:"index"` @@ -44,6 +71,7 @@ type DB_Profile struct { Attributes []DB_Attribute `gorm:"foreignkey:ProfileID"` Loadouts []DB_Loadout `gorm:"foreignkey:ProfileID"` Purchases []DB_Purchase `gorm:"foreignkey:ProfileID"` + VariantTokens []DB_VariantToken `gorm:"foreignkey:ProfileID"` Type string Revision int } @@ -182,6 +210,32 @@ func (DB_GiftLoot) TableName() string { return "GiftLoot" } +type DB_VariantToken struct { + ID string `gorm:"primary_key"` + ProfileID string `gorm:"index"` + TemplateID string + Name string + AutoEquipOnGrant bool + CreateGiftboxOnGrant bool + MarkItemUnseenOnGrant bool + VariantGrants []DB_VariantTokenGrant `gorm:"foreignkey:VariantTokenID"` +} + +func (DB_VariantToken) TableName() string { + return "VariantTokens" +} + +type DB_VariantTokenGrant struct { + ID string `gorm:"primary_key"` + VariantTokenID string `gorm:"index"` + Channel string + Value string +} + +func (DB_VariantTokenGrant) TableName() string { + return "VariantTokenGrants" +} + type DB_DiscordPerson struct { ID string `gorm:"primary_key"` PersonID string @@ -199,11 +253,11 @@ func (DB_DiscordPerson) TableName() string { type DB_SeasonStat struct { ID string `gorm:"primary_key"` PersonID string - Build string + Season int SeasonXP int - SeasonalLevel int - SeasonalTier int - BattleStars int + BookXP int + BookPurchased bool + Hype int } func (DB_SeasonStat) TableName() string {