Start to track profile changes.
This commit is contained in:
parent
6b09cd5e82
commit
8e3cd016d9
36
main.go
36
main.go
|
@ -44,7 +44,9 @@ func init() {
|
||||||
func init() {
|
func init() {
|
||||||
if DROP_TABLES {
|
if DROP_TABLES {
|
||||||
user := person.NewPerson()
|
user := person.NewPerson()
|
||||||
snapshot := user.AthenaProfile.Snapshot()
|
{
|
||||||
|
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 := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeBundleSchedule:Paid_1")
|
||||||
{
|
{
|
||||||
|
@ -64,28 +66,26 @@ func init() {
|
||||||
giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena"))
|
giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena"))
|
||||||
}
|
}
|
||||||
user.CommonCoreProfile.Gifts.AddGift(giftBox)
|
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()
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -26,6 +28,7 @@ func NewProfile(profile string) *Profile {
|
||||||
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
45
readme.md
45
readme.md
|
@ -11,12 +11,16 @@ 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")
|
|
||||||
{
|
{
|
||||||
|
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_eliminateplayers", 0)
|
||||||
quest.AddObjective("quest_objective_top1", 0)
|
quest.AddObjective("quest_objective_top1", 0)
|
||||||
quest.AddObjective("quest_objective_place_top10", 0)
|
quest.AddObjective("quest_objective_place_top10", 0)
|
||||||
|
@ -25,28 +29,39 @@ quest := person.NewQuest("Quest:Quest_1", "ChallengeBundle:Daily_1", "ChallengeB
|
||||||
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)
|
user.AthenaProfile.Quests.AddQuest(quest)
|
||||||
|
|
||||||
giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!")
|
giftBox := person.NewGift("GiftBox:GB_Default", 1, user.ID, "Hello, Bully!")
|
||||||
{
|
{
|
||||||
giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena"))
|
giftBox.AddLoot(person.NewItemWithType("AthenaCharacter:CID_002_Athena_Commando_F_Default", 1, "athena"))
|
||||||
|
}
|
||||||
|
user.CommonCoreProfile.Gifts.AddGift(giftBox)
|
||||||
}
|
}
|
||||||
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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user