Start to track profile changes.

This commit is contained in:
eccentric 2023-11-01 01:24:42 +00:00
parent 6b09cd5e82
commit 8e3cd016d9
8 changed files with 233 additions and 78 deletions

66
main.go
View File

@ -44,48 +44,48 @@ func init() {
func init() { func init() {
if DROP_TABLES { if DROP_TABLES {
user := person.NewPerson() 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) user.CommonCoreProfile.Items.AddItem(person.NewItem("Currency:MtxPurchased", 100))
quest.AddObjective("quest_objective_top1", 0) user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:CampaignAccess", 1))
quest.AddObjective("quest_objective_place_top10", 0)
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_eliminateplayers", 10)
quest.UpdateObjectiveCount("quest_objective_place_top10", -3) 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.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() go storage.Cache.CacheKiller()
} }
func main() { func main() {
persons := person.AllFromDatabase() // aid.WaitForExit()
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()
} }

View File

@ -1,6 +1,7 @@
package person package person
import ( import (
"github.com/ectrc/snow/aid"
"github.com/ectrc/snow/storage" "github.com/ectrc/snow/storage"
"github.com/google/uuid" "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() { func (i *Item) Delete() {
//storage.Repo.DeleteItem(i.ID) //storage.Repo.DeleteItem(i.ID)
i.Quantity = 0 i.Quantity = 0

View File

@ -21,7 +21,7 @@ type Option struct {
func NewPerson() *Person { func NewPerson() *Person {
return &Person{ return &Person{
ID: uuid.New().String(), ID: uuid.New().String(),
DisplayName: "Hello, Bully!", DisplayName: uuid.New().String(),
AthenaProfile: NewProfile("athena"), AthenaProfile: NewProfile("athena"),
CommonCoreProfile: NewProfile("common_core"), CommonCoreProfile: NewProfile("common_core"),
Loadout: NewLoadout(), Loadout: NewLoadout(),

View File

@ -3,6 +3,7 @@ package person
import ( import (
"fmt" "fmt"
"github.com/ectrc/snow/aid"
"github.com/ectrc/snow/storage" "github.com/ectrc/snow/storage"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/r3labs/diff/v3" "github.com/r3labs/diff/v3"
@ -15,7 +16,8 @@ type Profile struct {
Quests *QuestMutex Quests *QuestMutex
Attributes *AttributeMutex Attributes *AttributeMutex
Type string Type string
Changes []diff.Change Revision int
Changes []interface{}
} }
func NewProfile(profile string) *Profile { func NewProfile(profile string) *Profile {
@ -25,7 +27,8 @@ func NewProfile(profile string) *Profile {
Gifts: NewGiftMutex(), Gifts: NewGiftMutex(),
Quests: NewQuestMutex(), Quests: NewQuestMutex(),
Attributes: NewAttributeMutex(), Attributes: NewAttributeMutex(),
Type: profile, Type: profile,
Revision: 0,
} }
} }
@ -64,6 +67,7 @@ func FromDatabaseProfile(profile *storage.DB_Profile) *Profile {
Quests: quests, Quests: quests,
Attributes: attributes, Attributes: attributes,
Type: profile.Type, Type: profile.Type,
Revision: profile.Revision,
} }
} }
@ -92,8 +96,8 @@ func (p *Profile) Snapshot() *ProfileSnapshot {
return true return true
}) })
p.Attributes.RangeAttributes(func(key string, value *Attribute) bool { p.Attributes.RangeAttributes(func(key string, attribute *Attribute) bool {
attributes[key] = *value attributes[key] = *attribute
return true return true
}) })
@ -103,15 +107,97 @@ func (p *Profile) Snapshot() *ProfileSnapshot {
Gifts: gifts, Gifts: gifts,
Quests: quests, Quests: quests,
Attributes: attributes, Attributes: attributes,
Type: p.Type,
Revision: p.Revision,
} }
} }
func (p *Profile) Diff(snapshot *ProfileSnapshot) []diff.Change { func (p *Profile) Diff(snapshot *ProfileSnapshot) []diff.Change {
changes, _ := diff.Diff(p.Snapshot(), snapshot) changes, err := diff.Diff(snapshot, p.Snapshot())
p.Changes = changes 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 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 { type Loadout struct {
ID string ID string
Character string Character string
@ -134,8 +220,8 @@ func NewLoadout() *Loadout {
Backpack: "", Backpack: "",
Pickaxe: "", Pickaxe: "",
Glider: "", Glider: "",
Dances: []string{"", "", "", "", "", ""}, Dances: make([]string, 6),
ItemWraps: []string{"", "", "", "", "", "", ""}, ItemWraps: make([]string, 7),
LoadingScreen: "", LoadingScreen: "",
SkyDiveContrail: "", SkyDiveContrail: "",
MusicPack: "", MusicPack: "",
@ -144,20 +230,20 @@ func NewLoadout() *Loadout {
} }
} }
func FromDatabaseLoadout(loadout *storage.DB_Loadout) *Loadout { func FromDatabaseLoadout(l *storage.DB_Loadout) *Loadout {
return &Loadout{ return &Loadout{
ID: loadout.ID, ID: l.ID,
Character: loadout.Character, Character: l.Character,
Backpack: loadout.Backpack, Backpack: l.Backpack,
Pickaxe: loadout.Pickaxe, Pickaxe: l.Pickaxe,
Glider: loadout.Glider, Glider: l.Glider,
Dances: loadout.Dances, Dances: l.Dances,
ItemWraps: loadout.ItemWraps, ItemWraps: l.ItemWraps,
LoadingScreen: loadout.LoadingScreen, LoadingScreen: l.LoadingScreen,
SkyDiveContrail: loadout.SkyDiveContrail, SkyDiveContrail: l.SkyDiveContrail,
MusicPack: loadout.MusicPack, MusicPack: l.MusicPack,
BannerIcon: loadout.BannerIcon, BannerIcon: l.BannerIcon,
BannerColor: loadout.BannerColor, BannerColor: l.BannerColor,
} }
} }

View File

@ -14,6 +14,8 @@ type ProfileSnapshot struct {
Gifts map[string]GiftSnapshot Gifts map[string]GiftSnapshot
Quests map[string]Quest Quests map[string]Quest
Attributes map[string]Attribute Attributes map[string]Attribute
Revision int
Type string
} }
type ItemSnapshot struct { type ItemSnapshot struct {

View File

@ -35,6 +35,21 @@ func (m *ItemMutex) GetItem(id string) *Item {
return item.(*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) { func (m *ItemMutex) RangeItems(f func(key string, value *Item) bool) {
m.Range(func(key, value interface{}) bool { m.Range(func(key, value interface{}) bool {
return f(key.(string), value.(*Item)) return f(key.(string), value.(*Item))

View File

@ -11,42 +11,57 @@ Performance first, universal Fortnite backend written in Go.
## Examples ## Examples
### Person Structure
```golang ```golang
user := person.NewPerson() 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) user.CommonCoreProfile.Items.AddItem(person.NewItem("Currency:MtxPurchased", 100))
quest.AddObjective("quest_objective_top1", 0) user.CommonCoreProfile.Items.AddItem(person.NewItem("Token:CampaignAccess", 1))
quest.AddObjective("quest_objective_place_top10", 0)
quest.UpdateObjectiveCount("quest_objective_eliminateplayers", 10) quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1")
quest.UpdateObjectiveCount("quest_objective_place_top10", -3) {
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.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? ## 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. - 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._ - 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. - 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. - 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. - Game Server Communication. This would mean a websocket server that communicates with the game servers to send and receive data.

View File

@ -45,6 +45,7 @@ type DB_Profile struct {
Quests []DB_Quest `gorm:"foreignkey:ProfileID"` Quests []DB_Quest `gorm:"foreignkey:ProfileID"`
Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"` Attributes []DB_PAttribute `gorm:"foreignkey:ProfileID"`
Type string Type string
Revision int
} }
func (DB_Profile) TableName() string { func (DB_Profile) TableName() string {