diff --git a/main.go b/main.go index e2a9751..ab3a19b 100644 --- a/main.go +++ b/main.go @@ -44,48 +44,48 @@ func init() { func init() { if DROP_TABLES { user := person.NewPerson() - snapshot := user.AthenaProfile.Snapshot() - - quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1") { - quest.AddObjective("quest_objective_eliminateplayers", 0) - quest.AddObjective("quest_objective_top1", 0) - quest.AddObjective("quest_objective_place_top10", 0) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Currency:MtxPurchased", 100)) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:CampaignAccess", 1)) + + quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1") + { + quest.AddObjective("quest_objective_eliminateplayers", 0) + quest.AddObjective("quest_objective_top1", 0) + quest.AddObjective("quest_objective_place_top10", 0) - quest.UpdateObjectiveCount("quest_objective_eliminateplayers", 10) - quest.UpdateObjectiveCount("quest_objective_place_top10", -3) + quest.UpdateObjectiveCount("quest_objective_eliminateplayers", 10) + quest.UpdateObjectiveCount("quest_objective_place_top10", -3) - quest.RemoveObjective("quest_objective_top1") + quest.RemoveObjective("quest_objective_top1") + } + user.AthenaProfile.Quests.AddQuest(quest) + + giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!") + { + giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena")) + } + user.CommonCoreProfile.Gifts.AddGift(giftBox) } - user.AthenaProfile.Quests.AddQuest(quest) - - giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!") - { - giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena")) - } - user.CommonCoreProfile.Gifts.AddGift(giftBox) - - currency := person.NewItem("Currency:MtxPurchased", 100) - user.AthenaProfile.Items.AddItem(currency) - user.Save() - user.AthenaProfile.Diff(snapshot) + + snapshot := user.CommonCoreProfile.Snapshot() + { + vbucks := user.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + vbucks.Quantity = 200 + vbucks.Favorite = true + + user.CommonCoreProfile.Items.DeleteItem(user.CommonCoreProfile.Items.GetItemByTemplateID("Token:CampaignAccess").ID) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:ReceiveMtxCurrency", 1)) + } + user.CommonCoreProfile.Diff(snapshot) + + aid.PrintJSON(user.CommonCoreProfile.Changes) } go storage.Cache.CacheKiller() } func main() { - persons := person.AllFromDatabase() - - for _, p := range persons { - p.AthenaProfile.Items.RangeItems(func(id string, item *person.Item) bool { - aid.PrintJSON(item) - return true - }) - - aid.PrintJSON(p.Snapshot()) - } - - aid.WaitForExit() + // aid.WaitForExit() } \ No newline at end of file diff --git a/person/item.go b/person/item.go index 8fb8696..07596e5 100644 --- a/person/item.go +++ b/person/item.go @@ -1,6 +1,7 @@ package person import ( + "github.com/ectrc/snow/aid" "github.com/ectrc/snow/storage" "github.com/google/uuid" ) @@ -68,6 +69,41 @@ func FromDatabaseLoot(item *storage.DB_Loot) *Item { } } +func (i *Item) GenerateFortniteItemEntry() aid.JSON { + varaints := []aid.JSON{} + + for _, variant := range i.Variants { + varaints = append(varaints, aid.JSON{ + "channel": variant.Channel, + "owned": variant.Owned, + "active": variant.Active, + }) + } + + return aid.JSON{ + "templateId": i.TemplateID, + "attributes": aid.JSON{ + "variants": varaints, + "favorite": i.Favorite, + "item_seen": i.HasSeen, + }, + "quantity": i.Quantity, + } +} + +func (i *Item) GetAttribute(attribute string) interface{} { + switch attribute { + case "Favorite": + return i.Favorite + case "HasSeen": + return i.HasSeen + case "Variants": + return i.Variants + } + + return nil +} + func (i *Item) Delete() { //storage.Repo.DeleteItem(i.ID) i.Quantity = 0 diff --git a/person/person.go b/person/person.go index 3100f57..46d262c 100644 --- a/person/person.go +++ b/person/person.go @@ -21,7 +21,7 @@ type Option struct { func NewPerson() *Person { return &Person{ ID: uuid.New().String(), - DisplayName: "Hello, Bully!", + DisplayName: uuid.New().String(), AthenaProfile: NewProfile("athena"), CommonCoreProfile: NewProfile("common_core"), Loadout: NewLoadout(), diff --git a/person/profile.go b/person/profile.go index 720d62a..b04edaf 100644 --- a/person/profile.go +++ b/person/profile.go @@ -3,6 +3,7 @@ package person import ( "fmt" + "github.com/ectrc/snow/aid" "github.com/ectrc/snow/storage" "github.com/google/uuid" "github.com/r3labs/diff/v3" @@ -15,7 +16,8 @@ type Profile struct { Quests *QuestMutex Attributes *AttributeMutex Type string - Changes []diff.Change + Revision int + Changes []interface{} } func NewProfile(profile string) *Profile { @@ -25,7 +27,8 @@ func NewProfile(profile string) *Profile { Gifts: NewGiftMutex(), Quests: NewQuestMutex(), Attributes: NewAttributeMutex(), - Type: profile, + Type: profile, + Revision: 0, } } @@ -64,6 +67,7 @@ func FromDatabaseProfile(profile *storage.DB_Profile) *Profile { Quests: quests, Attributes: attributes, Type: profile.Type, + Revision: profile.Revision, } } @@ -92,8 +96,8 @@ func (p *Profile) Snapshot() *ProfileSnapshot { return true }) - p.Attributes.RangeAttributes(func(key string, value *Attribute) bool { - attributes[key] = *value + p.Attributes.RangeAttributes(func(key string, attribute *Attribute) bool { + attributes[key] = *attribute return true }) @@ -103,15 +107,97 @@ func (p *Profile) Snapshot() *ProfileSnapshot { Gifts: gifts, Quests: quests, Attributes: attributes, + Type: p.Type, + Revision: p.Revision, } } func (p *Profile) Diff(snapshot *ProfileSnapshot) []diff.Change { - changes, _ := diff.Diff(p.Snapshot(), snapshot) - p.Changes = changes + changes, err := diff.Diff(snapshot, p.Snapshot()) + if err != nil { + fmt.Printf("error diffing profile: %v\n", err) + return nil + } + + aid.PrintJSON(changes) + + for _, change := range changes { + switch change.Path[0] { + case "Items": + if change.Type == "create" && change.Path[2] == "ID" { + p.CreateItemAddedChange(p.Items.GetItem(change.Path[1])) + } + + if change.Type == "delete" && change.Path[2] == "ID" { + p.CreateItemRemovedChange(change.Path[1]) + } + + if change.Type == "update" && change.Path[2] == "Quantity" { + p.CreateItemQuantityChangedChange(p.Items.GetItem(change.Path[1])) + } + + if change.Type == "update" && change.Path[2] != "Quantity" { + p.CreateItemAttributeChangedChange(p.Items.GetItem(change.Path[1]), change.Path[2]) + } + } + } + return changes } +func (p *Profile) CreateItemAddedChange(item *Item) { + if item == nil { + fmt.Println("error getting item from profile", item.ID) + return + } + + p.Changes = append(p.Changes, ItemAdded{ + ChangeType: "itemAdded", + ItemId: item.ID, + Item: item.GenerateFortniteItemEntry(), + }) +} + +func (p *Profile) CreateItemRemovedChange(itemId string) { + p.Changes = append(p.Changes, ItemRemoved{ + ChangeType: "itemRemoved", + ItemId: itemId, + }) +} + +func (p *Profile) CreateItemQuantityChangedChange(item *Item) { + if item == nil { + fmt.Println("error getting item from profile", item.ID) + return + } + + p.Changes = append(p.Changes, ItemQuantityChanged{ + ChangeType: "itemQuantityChanged", + ItemId: item.ID, + Quantity: item.Quantity, + }) +} + +func (p *Profile) CreateItemAttributeChangedChange(item *Item, attribute string) { + if item == nil { + fmt.Println("error getting item from profile", item.ID) + return + } + + lookup := map[string]string{ + "Favorite": "favorite", + "HasSeen": "item_seen", + "Variants": "variants", + } + + p.Changes = append(p.Changes, ItemAttributeChanged{ + ChangeType: "itemAttributeChanged", + ItemId: item.ID, + AttributeName: lookup[attribute], + AttributeValue: item.GetAttribute(attribute), + }) +} + type Loadout struct { ID string Character string @@ -134,8 +220,8 @@ func NewLoadout() *Loadout { Backpack: "", Pickaxe: "", Glider: "", - Dances: []string{"", "", "", "", "", ""}, - ItemWraps: []string{"", "", "", "", "", "", ""}, + Dances: make([]string, 6), + ItemWraps: make([]string, 7), LoadingScreen: "", SkyDiveContrail: "", MusicPack: "", @@ -144,20 +230,20 @@ func NewLoadout() *Loadout { } } -func FromDatabaseLoadout(loadout *storage.DB_Loadout) *Loadout { +func FromDatabaseLoadout(l *storage.DB_Loadout) *Loadout { return &Loadout{ - ID: loadout.ID, - Character: loadout.Character, - Backpack: loadout.Backpack, - Pickaxe: loadout.Pickaxe, - Glider: loadout.Glider, - Dances: loadout.Dances, - ItemWraps: loadout.ItemWraps, - LoadingScreen: loadout.LoadingScreen, - SkyDiveContrail: loadout.SkyDiveContrail, - MusicPack: loadout.MusicPack, - BannerIcon: loadout.BannerIcon, - BannerColor: loadout.BannerColor, + ID: l.ID, + Character: l.Character, + Backpack: l.Backpack, + Pickaxe: l.Pickaxe, + Glider: l.Glider, + Dances: l.Dances, + ItemWraps: l.ItemWraps, + LoadingScreen: l.LoadingScreen, + SkyDiveContrail: l.SkyDiveContrail, + MusicPack: l.MusicPack, + BannerIcon: l.BannerIcon, + BannerColor: l.BannerColor, } } diff --git a/person/snapshot.go b/person/snapshot.go index b0cc658..1fde77b 100644 --- a/person/snapshot.go +++ b/person/snapshot.go @@ -14,6 +14,8 @@ type ProfileSnapshot struct { Gifts map[string]GiftSnapshot Quests map[string]Quest Attributes map[string]Attribute + Revision int + Type string } type ItemSnapshot struct { diff --git a/person/sync.go b/person/sync.go index 64fbfc9..5618fb0 100644 --- a/person/sync.go +++ b/person/sync.go @@ -35,6 +35,21 @@ func (m *ItemMutex) GetItem(id string) *Item { return item.(*Item) } +func (m *ItemMutex) GetItemByTemplateID(templateID string) *Item { + var item *Item + + m.Range(func(key, value interface{}) bool { + if value.(*Item).TemplateID == templateID { + item = value.(*Item) + return false + } + + return true + }) + + return item +} + func (m *ItemMutex) RangeItems(f func(key string, value *Item) bool) { m.Range(func(key, value interface{}) bool { return f(key.(string), value.(*Item)) diff --git a/readme.md b/readme.md index 496c836..268ddcb 100644 --- a/readme.md +++ b/readme.md @@ -11,42 +11,57 @@ Performance first, universal Fortnite backend written in Go. ## Examples +### Person Structure + ```golang user := person.NewPerson() -snapshot := user.AthenaProfile.Snapshot() - -quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1") { - quest.AddObjective("quest_objective_eliminateplayers", 0) - quest.AddObjective("quest_objective_top1", 0) - quest.AddObjective("quest_objective_place_top10", 0) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Currency:MtxPurchased", 100)) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:CampaignAccess", 1)) - quest.UpdateObjectiveCount("quest_objective_eliminateplayers", 10) - quest.UpdateObjectiveCount("quest_objective_place_top10", -3) + quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1") + { + quest.AddObjective("quest_objective_eliminateplayers", 0) + quest.AddObjective("quest_objective_top1", 0) + quest.AddObjective("quest_objective_place_top10", 0) - quest.RemoveObjective("quest_objective_top1") + quest.UpdateObjectiveCount("quest_objective_eliminateplayers", 10) + quest.UpdateObjectiveCount("quest_objective_place_top10", -3) + + quest.RemoveObjective("quest_objective_top1") + } + user.AthenaProfile.Quests.AddQuest(quest) + + giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!") + { + giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena")) + } + user.CommonCoreProfile.Gifts.AddGift(giftBox) } -user.AthenaProfile.Quests.AddQuest(quest) - -giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!") -{ - giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena")) -} -user.CommonCoreProfile.Gifts.AddGift(giftBox) - -currency := person.NewItem("Currency:MtxPurchased", 100) -user.CommonCoreProfile.Items.AddItem(currency) - user.Save() -user.AthenaProfile.Diff(snapshot) +``` + +### Profile Changes + +```golang +snapshot := user.CommonCoreProfile.Snapshot() +{ + vbucks := user.CommonCoreProfile.Items.GetItemByTemplateID("Currency:MtxPurchased") + vbucks.Quantity = 200 + vbucks.Favorite = true + + user.CommonCoreProfile.Items.DeleteItem(user.CommonCoreProfile.Items.GetItemByTemplateID("Token:CampaignAccess").ID) + user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:ReceiveMtxCurrency", 1)) +} +user.CommonCoreProfile.Diff(snapshot) ``` ## What's next? -- Be able to convert my person structures into the required format for the game. This mainly targets the profiles and their changes. +- Be able to track my person profile structures changes, convert into the required responses for the game, and send back to the client. +- Implement the HTTP API for the game to communicate with the backend. This is the most important part of the project as it needs to handle thousands of requests per second. _Should I use Fiber?_ - Person Authentication for the game to determine if the person is valid or not. Fortnite uses JWT tokens for this which makes it easy to implement. - Embed game assets into the backend e.g. Game XP Curve, Quest Data etc. _This would mean a single binary that can be run anywhere without the need of external files._ -- Implement the HTTP API for the game to communicate with the backend. This is the most important part of the project as it needs to handle thousands of requests per second. _Should I use Fiber?_ - Interact with external Buckets to save player data externally. - A way to interact with persons outside of the game. This is mainly for a web app and other services to interact with the backend. - Game Server Communication. This would mean a websocket server that communicates with the game servers to send and receive data. diff --git a/storage/tables.go b/storage/tables.go index 91d22fa..30c95cf 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -45,6 +45,7 @@ type DB_Profile struct { Quests []DB_Quest `gorm:"foreignkey:ProfileID"` Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"` Type string + Revision int } func (DB_Profile) TableName() string {