feat: update to latest;

new shop system
more config options
arena & hype
per season stats
battle pass
better variant system
complete vbuck & starter pack store
fix bugs related to deleting account
update launcher endpoints
fixed gift loot not deleting
This commit is contained in:
Eccentric 2024-03-10 18:16:42 +00:00
parent aaeacb663a
commit 250e85732d
58 changed files with 4523 additions and 1435 deletions

View File

@ -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
}

View File

@ -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")

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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{

View File

@ -55,7 +55,7 @@ func IntialiseClient() {
err := StaticClient.Client.Open()
if err != nil {
panic(err)
aid.Print("(discord) failed to connect; will be disabled")
}
}

416
fortnite/arena.go Normal file
View File

@ -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
}

View File

@ -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)
})

182
fortnite/granting.go Normal file
View File

@ -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
}

View File

@ -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()

331
fortnite/season.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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"`
}

159
fortnite/typings.go Normal file
View File

@ -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
}

4
go.sum
View File

@ -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=

View File

@ -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"))
}

View File

@ -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, &notifications); 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
}

View File

@ -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)
}

View File

@ -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()})

View File

@ -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",
})
}

65
handlers/events.go Normal file
View File

@ -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{})
}

View File

@ -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))

View File

@ -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(),

177
handlers/purchases.go Normal file
View File

@ -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(),
})
}

View File

@ -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()),
})
}

View File

@ -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)

48
main.go
View File

@ -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 {

View File

@ -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)
}

View File

@ -10,7 +10,7 @@ import (
type Gift struct {
ID string
ProfileID string
ProfileID string
TemplateID string
Quantity int
FromID string

View File

@ -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 {

View File

@ -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")
}

View File

@ -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

115
person/receipt.go Normal file
View File

@ -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,
}
}

55
person/season.go Normal file
View File

@ -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,
}
}

View File

@ -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

View File

@ -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
}

130
person/variant.go Normal file
View File

@ -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,
}
}

View File

@ -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

130
shop/book.go Normal file
View File

@ -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{}
}

46
shop/catalog.go Normal file
View File

@ -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",
}
}

151
shop/item.go Normal file
View File

@ -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{}
}

148
shop/kits.go Normal file
View File

@ -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",
}
}

157
shop/money.go Normal file
View File

@ -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",
}
}

92
shop/section.go Normal file
View File

@ -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
}

206
shop/shop.go Normal file
View File

@ -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 <Bold>valued at over 3,500 V-Bucks</>.\n • <ItemName>Blackheart</> Progressive Outfit\n • <ItemName>Hybrid</> Progressive Outfit\n • <Bold>50% Bonus</> Season Match XP\n • <Bold>10% Bonus</> Season Friend Match XP\n • <Bold>Extra Weekly Challenges</>\n\nPlay to level up your Battle Pass, unlocking <Bold>over 100 rewards</> (typically takes 75 to 150 hours of play).\n • <ItemName>Sidewinder</> and <Bold>4 more Outfits</>\n • <Bold>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 <Bold>valued at over 10,000 V-Bucks</>.\n • <ItemName>Blackheart</> Progressive Outfit\n • <ItemName>Hybrid</> Progressive Outfit\n • <ItemName>Sidewinder</> Outfit\n • <ItemName>Tropical Camo</> Wrap\n • <ItemName>Woodsy</> Pet\n • <ItemName>Sky Serpents</> Glider\n • <ItemName>Cobra</> Back Bling\n • <ItemName>Flying Standard</> Contrail\n • 300 V-Bucks\n • 1 Music Track\n • <Bold>70% Bonus</> Season Match XP\n • <Bold>20% Bonus</> Season Friend Match XP\n • <Bold>Extra Weekly Challenges</>\n • and more!\n\nPlay to level up your Battle Pass, unlocking <Bold>over 75 rewards</> (typically takes 75 to 150 hours of play).\n • <Bold>4 more Outfits</>\n • <Bold>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
}

73
shop/types.go Normal file
View File

@ -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
}

View File

@ -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"),
})

View File

@ -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(`<stream:stream id="`+socket.ID+`" from="prod.ol.epicgames.com" xml:lang="*" version="1.0" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client"/>`))
// socket.Write([]byte(`<open xmlns="urn:ietf:params:xml:ns:xmpp-framing" from="prod.ol.epicgames.com" version="1.0" id="`+ socket.ID +`" />`))
// socket.Write([]byte(`<stream:features xmlns:stream="http://etherx.jabber.org/streams" id="`+ socket.ID +`" />`))
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(`<iq xmlns="jabber:client" type="result" id="`+ parsed.Root().SelectAttr("id").Value +`" from="prod.ol.epicgames.com" to="`+ socket.Data.JabberID +`" />`))
socket.Write([]byte(`<iq xmlns="jabber:client" type="result" id="`+ parsed.Root().SelectAttr("id").Value +`" from="prod.ol.epicgames.com" to="`+ parsed.Root().SelectAttr("from").Value +`" >
<ping xmlns="urn:xmpp:ping"/>
</iq`))
socket.JabberNotifyFriends()
return nil
}
@ -207,8 +207,6 @@ func jabberMessageHandler(socket *Socket[JabberData], parsed *etree.Document) er
return nil
}
// TODO: IMPLEMENT WHISPERING
func jabberMessageChatHandler(socket *Socket[JabberData], parsed *etree.Document) error {
return nil
}
@ -267,4 +265,21 @@ func (s *Socket[T]) JabberNotifyFriends() {
jabberSocket.Write([]byte(`<presence xmlns="jabber:client" type="available" from="`+ jabberSocket.Data.JabberID +`" to="`+ jabberSocket.Data.JabberID +`">
<status>`+ jabberSocket.Data.LastPresence +`</status>
</presence>`))
}
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(`<iq id="_xmpp_auth1" type="get" from="prod.ol.epicgames.com" xmlns="jabber:client">
<ping xmlns="urn:xmpp:ping"/>
</iq>`))
return true
})
}
}()
}

View File

@ -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] {

View File

@ -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

View File

@ -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 {

View File

@ -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

98
storage/mem/controller.js Normal file
View File

@ -0,0 +1,98 @@
/**
*
* @typedef {Object} PurchaseFlow
* @property {(reason: string) => Promise<void>} requestclose
* @property {(receipt: {}) => Promise<void>} receipt
* @property {(browserId: string, url: string) => Promise<boolean>} launchvalidatedexternalbrowserurl
* @property {(url: string) => Promise<boolean>} launchexternalbrowserurl
* @property {(browserId: string) => Promise<string>} getexternalbrowserpath
* @property {(browserId: string) => Promise<string>} getexternalbrowsername
* @property {(url: string) => Promise<string>} 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");
};

510
storage/mem/purchase.html Normal file
View File

@ -0,0 +1,510 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="/purchase/assets/?asset=controller.js"></script>
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet"
/>
<title>Purchase Flow</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Open Sans", sans-serif;
font-optical-sizing: auto;
font-style: normal;
font-weight: 500;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
:root {
--epic-background: #1e1e1e;
--epic-summary: #262626;
--epic-summary-hover: #2e2e2e;
--epic-card: #242524;
--epic-highlight: #28a7da;
--epic-color-active: #f5f5f5;
--epic-color-semiactive: #a1a1a1;
--epic-color-inactive: #636363;
}
.icon {
width: 1.5rem;
height: 1.5rem;
}
.appContainer {
position: absolute;
top: 0;
left: 0;
display: flex;
width: 100vw;
height: 100vh;
color: var(--epic-color-active);
background-color: var(--epic-background);
}
.order {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.orderSummary {
position: relative;
display: flex;
flex-direction: column;
padding: 1rem;
width: 25rem;
min-width: 25rem;
height: 100%;
background-color: var(--epic-summary);
}
.orderSummaryHeader {
font-weight: 600;
margin-top: 3rem;
margin-bottom: 1rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.orderClose {
border: none;
outline: none;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 0.25rem;
cursor: pointer;
border-radius: 0.65rem;
background-color: var(--epic-summary);
transition: background-color 50ms;
}
.orderClose svg {
fill: var(--epic-color-inactive);
transition: fill 50ms;
}
.orderClose:hover {
background-color: var(--epic-card);
}
.orderClose:hover svg {
fill: var(--epic-color-active);
}
.orderImage {
/* aspect-ratio: 9/12; */
width: 8rem;
min-width: max-content;
height: 10rem;
min-height: max-content;
border-radius: 0.5rem;
image-rendering: optimizeSpeed;
/* background-image: url("https://fortnite-api.com/images/cosmetics/br/CID_384_Athena_Commando_M_StreetAssassin/icon.png"); */
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--epic-background);
}
.orderTitle {
display: flex;
flex-direction: row;
/* gap: 1rem; */
}
.orderTitleInformation {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 0.5rem;
}
.orderTitleName {
font-size: 1.25rem;
font-weight: 600;
line-height: 1.25rem;
}
.orderTitlePrice {
font-size: 1rem;
font-weight: 500;
color: var(--epic-color-semiactive);
}
.bigFatButButton {
margin-top: auto;
outline: none;
border: none;
padding: 1.2rem;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
color: var(--epic-color-active);
border-radius: 0.25rem;
background-color: #0078f2;
transition: filter 50ms;
}
.bigFatButButton:hover {
filter: brightness(0.9) contrast(1.5);
}
.priceBreakdown {
display: flex;
flex-direction: column;
/* gap: 0.15rem; */
margin-top: 0.5rem;
}
.priceBreakdown section {
display: flex;
justify-content: space-between;
margin-bottom: 0.25rem;
}
.priceBreakdown section p {
font-weight: 400;
font-size: 0.95rem;
color: var(--epic-color-semiactive);
}
.priceBreakdown .divider {
margin-top: 0.35rem;
margin-bottom: 0.35rem;
height: 1px;
width: 100%;
background-color: #636363;
}
.priceBreakdown section.bold p {
font-weight: 600;
color: var(--epic-color-active);
}
.specialBannerIcon {
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 1.5rem;
}
.specialBannerIcon svg {
width: 1.5rem;
height: 1.5rem;
fill: #242424;
}
.specialBanner {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0.35rem;
width: 100%;
border-radius: 0.25rem;
background-image: linear-gradient(to right, #ccec86, #ffdd76);
margin-top: 0.5rem;
}
.specialBanner p {
font-size: 0.785rem;
line-height: 1rem;
font-weight: 500;
color: #2c2d28;
}
.specialBanner p b {
font-weight: 700;
color: #242424;
}
.orderPaymentMethods {
display: flex;
flex-direction: column;
width: 55rem;
height: 100%;
padding: 1rem;
}
.orderStatusContainer {
display: flex;
flex-direction: row;
}
.orderStatus {
display: flex;
flex-direction: row;
align-items: center;
width: 20rem;
height: 3.5rem;
user-select: none;
cursor: pointer;
border-bottom: 0.15rem solid var(--epic-summary);
}
.orderStatus p {
font-size: 1rem;
font-weight: 600;
color: var(--epic-color-inactive);
}
.orderStatus:hover {
border-bottom: 0.15rem solid var(--epic-summary-hover);
}
.orderStatus.active {
border-bottom: 0.15rem solid var(--epic-highlight);
}
.orderStatus.active p {
color: var(--epic-color-active);
}
.orderStatus.fill {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
align-items: center;
width: unset;
cursor: default;
flex-grow: 1;
}
.orderStatus.fill:hover {
border-bottom: 0.15rem solid var(--epic-summary);
}
.orderStatusUserIcon {
width: 1.5rem;
height: 1.5rem;
fill: var(--epic-color-inactive);
}
.paymentMethodsContainer {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 1.5rem 0;
}
.paymentMethodsHeader {
font-size: 1rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: #ddd;
}
.paymentMethodsContainer .divider {
margin-top: 1.25rem;
height: 1px;
width: 100%;
background-color: #333333;
}
.paymentMethod {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 1rem;
padding: 0.8rem;
min-height: 3rem;
cursor: pointer;
border-radius: 0.5rem;
background-color: var(--epic-card);
transition: background-color 50ms;
}
.paymentMethod:hover {
background-color: var(--epic-summary-hover);
}
.paymentMethod.active {
cursor: default;
background-color: var(--epic-card);
}
.paymentMethodCard {
display: flex;
flex-direction: row;
align-items: end;
justify-content: flex-end;
padding: 0.5rem;
width: 12rem;
height: 7rem;
border-radius: 0.5rem;
background-color: #fff;
}
.paymentCardProvider {
width: 40%;
}
.paymentInformation {
display: flex;
flex-direction: column;
justify-content: center;
margin: 1rem;
}
p.paymentCardNumber {
font-size: 0.85rem;
font-weight: 500;
color: var(--epic-color-semiactive);
}
p.p {
font-size: 0.85rem;
font-weight: 400;
color: var(--epic-color-semiactive);
}
p.notice {
font-size: 0.75rem;
font-weight: 400;
color: var(--epic-color-semiactive);
margin-top: 1rem;
}
</style>
</head>
<body>
<div class="appContainer">
<div class="order">
<!-- <OrderPaymentMethods /> -->
<div class="orderPaymentMethods">
<div class="orderStatusContainer">
<div class="orderStatus active">
<p>CHECKOUT</p>
</div>
<div class="orderStatus fill">
<p class="orderStatusUsername" id="displayName"></p>
</div>
</div>
<div class="paymentMethodsContainer">
<h2 class="paymentMethodsHeader">REVIEW AND PLACE ORDER</h2>
<p class="p">YOUR PAYMENT METHODS</p>
<!-- <div class="paymentMethod active">
<div class="paymentMethodCard">
<img
src="https://logos-world.net/wp-content/uploads/2020/04/Visa-Logo.png"
alt=""
class="paymentCardProvider"
/>
</div>
<div class="paymentInformation">
<p class="paymentCardNumber">**** **** **** 0000</p>
<p class="paymentCard">Free Purchase!</p>
</div>
</div> -->
<div class="divider"></div>
<p class="notice">
Snow does not store your payment information. Your payment
information is stored securely by Sellix.
</p>
</div>
</div>
<!-- -->
<!-- <OrderSummary /> -->
<div class="orderSummary">
<header class="orderSummaryHeader">
<p>ORDER SUMMARY</p>
<button class="orderClose" id="close">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="icon"
>
<path
fill-rule="evenodd"
d="M5.47 5.47a.75.75 0 0 1 1.06 0L12 10.94l5.47-5.47a.75.75 0 1 1 1.06 1.06L13.06 12l5.47 5.47a.75.75 0 1 1-1.06 1.06L12 13.06l-5.47 5.47a.75.75 0 0 1-1.06-1.06L10.94 12 5.47 6.53a.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
</svg>
</button>
</header>
<div class="orderTitle">
<div class="orderImage" id="orderImage"></div>
<div class="orderTitleInformation">
<h4 class="orderTitleName" id="orderName"></h4>
<p class="orderTitlePrice" id="orderPrice"></p>
</div>
</div>
<div class="priceBreakdown">
<section>
<p>Price</p>
<p id="orderSubtotalPrice"></p>
</section>
<section>
<p>VAT included where applicable</p>
</section>
<div class="divider"></div>
<section class="bold">
<p>Total</p>
<p id="orderTotalPrice"></p>
</section>
</div>
<div class="specialBanner">
<div class="specialBannerIcon">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 9.134a1 1 0 010 1.732l-3.354 1.935-1.18 4.455a1 1 0 01-1.933 0L9.854 12.8 6.5 10.866a1 1 0 010-1.732l3.354-1.935 1.18-4.455A1 1 0 0112 2z"
clip-rule="evenodd"
/>
</svg>
</div>
<p>
Earn <b>50 V-Bucks</b> with this purchase. Rewards are available
for use 14 days after purchase
</p>
</div>
<button class="bigFatButButton" id="purchaseOfferButton">
PLACE ORDER
</button>
</div>
<!-- -->
</div>
</div>
<script>
const snow = {
log: async (json) =>
await axios.post("http://127.0.0.1:3000/snow/log", {
json: json,
url: window.location.href,
}),
};
const unrealEngineInjected = window.ue ? window.ue : null;
unrealEngineInjected && main(unrealEngineInjected); // asyncronous!!!
</script>
</body>
</html>

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {