Start to track profile changes.
This commit is contained in:
parent
6b09cd5e82
commit
8e3cd016d9
66
main.go
66
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()
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
61
readme.md
61
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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user