diff --git a/fortnite/person.go b/fortnite/person.go index 414ff61..2ee86b6 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -138,7 +138,7 @@ func NewFortnitePersonWithId(id string, displayName string, everything bool) *p. person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("allowed_to_send_gifts", true)).Save() person.CommonCoreProfile.Attributes.AddAttribute(p.NewAttribute("gift_history", aid.JSON{})).Save() - loadout := p.NewLoadout("sandbox_loadout", person.AthenaProfile) + loadout := p.NewLoadoutWithID("sandbox_loadout", "", person.AthenaProfile) person.AthenaProfile.Loadouts.AddLoadout(loadout).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("loadouts", []string{loadout.ID})).Save() person.AthenaProfile.Attributes.AddAttribute(p.NewAttribute("last_applied_loadout", loadout.ID)).Save() diff --git a/handlers/client.go b/handlers/client.go index cb406e3..9e4e51c 100644 --- a/handlers/client.go +++ b/handlers/client.go @@ -9,6 +9,7 @@ import ( "github.com/ectrc/snow/aid" "github.com/ectrc/snow/fortnite" p "github.com/ectrc/snow/person" + "github.com/google/uuid" "github.com/gofiber/fiber/v2" ) @@ -23,6 +24,9 @@ var ( "SetBattleRoyaleBanner": clientSetBattleRoyaleBannerAction, "SetCosmeticLockerSlot": clientSetCosmeticLockerSlotAction, "SetCosmeticLockerBanner": clientSetCosmeticLockerBannerAction, + "SetCosmeticLockerName": clientSetCosmeticLockerNameAction, + "CopyCosmeticLoadout": clientCopyCosmeticLoadoutAction, + "DeleteCosmeticLoadout": clientDeleteCosmeticLoadoutAction, "PurchaseCatalogEntry": clientPurchaseCatalogEntryAction, "GiftCatalogEntry": clientGiftCatalogEntryAction, "RemoveGiftBox": clientRemoveGiftBoxAction, @@ -188,8 +192,8 @@ func clientEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, default: attr.ValueJSON = aid.JSONStringify(item.ID) } - go attr.Save() + return nil } @@ -225,11 +229,9 @@ func clientSetBattleRoyaleBannerAction(c *fiber.Ctx, person *p.Person, profile * iconAttr.ValueJSON = aid.JSONStringify(strings.Split(iconItem.TemplateID, ":")[1]) colorAttr.ValueJSON = aid.JSONStringify(strings.Split(colorItem.TemplateID, ":")[1]) + iconAttr.Save() + colorAttr.Save() - go func() { - iconAttr.Save() - colorAttr.Save() - }() return nil } @@ -321,6 +323,7 @@ func clientSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile * } go currentLocker.Save() + return nil } @@ -351,11 +354,194 @@ func clientSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile if currentLocker == nil { return fmt.Errorf("current locker not found") } - currentLocker.BannerColorID = color.ID currentLocker.BannerID = icon.ID go currentLocker.Save() + + return nil +} + +func clientSetCosmeticLockerNameAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + LockerItem string `json:"lockerItem" binding:"required"` + Name string `json:"name" binding:"required"` + } + + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + loadoutsAttribute := profile.Attributes.GetAttributeByKey("loadouts") + if loadoutsAttribute == nil { + return fmt.Errorf("loadouts not found") + } + loadouts := p.AttributeConvertToSlice[string](loadoutsAttribute) + + currentLocker := profile.Loadouts.GetLoadout(body.LockerItem) + if currentLocker == nil { + return fmt.Errorf("current locker not found") + } + + if loadouts[0] == currentLocker.ID { + return fmt.Errorf("cannot rename default locker") + } + + currentLocker.LockerName = body.Name + go currentLocker.Save() + + return nil +} + +func clientCopyCosmeticLoadoutAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + OptNewNameForTarget string `json:"optNewNameForTarget" binding:"required"` + SourceIndex int `json:"sourceIndex" binding:"required"` + TargetIndex int `json:"targetIndex" binding:"required"` + } + + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + lastAppliedLoadoutAttribute := profile.Attributes.GetAttributeByKey("last_applied_loadout") + if lastAppliedLoadoutAttribute == nil { + return fmt.Errorf("last_applied_loadout not found") + } + + activeLoadoutIndexAttribute := profile.Attributes.GetAttributeByKey("active_loadout_index") + if activeLoadoutIndexAttribute == nil { + return fmt.Errorf("active_loadout_index not found") + } + + loadoutsAttribute := profile.Attributes.GetAttributeByKey("loadouts") + if loadoutsAttribute == nil { + return fmt.Errorf("loadouts not found") + } + loadouts := p.AttributeConvertToSlice[string](loadoutsAttribute) + + if body.SourceIndex >= len(loadouts) { + return fmt.Errorf("source index out of range") + } + + sandboxLoadout := profile.Loadouts.GetLoadout("sandbox_loadout") + if sandboxLoadout == nil { + return fmt.Errorf("sandbox loadout not found") + } + + lastAppliedLoadout := profile.Loadouts.GetLoadout(aid.JSONParse(lastAppliedLoadoutAttribute.ValueJSON).(string)) + if lastAppliedLoadout == nil { + return fmt.Errorf("last applied loadout not found") + } + + if body.TargetIndex >= len(loadouts) { + clone := lastAppliedLoadout.Copy() + clone.ID = uuid.New().String() + clone.LockerName = body.OptNewNameForTarget + profile.Loadouts.AddLoadout(&clone).Save() + + lastAppliedLoadout.CopyFrom(sandboxLoadout) + go lastAppliedLoadout.Save() + + sandboxLoadout.CopyFrom(&clone) + go sandboxLoadout.Save() + + loadouts = append(loadouts, clone.ID) + loadoutsAttribute.ValueJSON = aid.JSONStringify(loadouts) + go loadoutsAttribute.Save() + + lastAppliedLoadoutAttribute.ValueJSON = aid.JSONStringify(clone.ID) + activeLoadoutIndexAttribute.ValueJSON = aid.JSONStringify(body.TargetIndex) + go lastAppliedLoadoutAttribute.Save() + go activeLoadoutIndexAttribute.Save() + return nil + } + + if body.SourceIndex > 0 { + sourceLoadout := profile.Loadouts.GetLoadout(loadouts[body.SourceIndex]) + if sourceLoadout == nil { + return fmt.Errorf("target loadout not found") + } + + sandboxLoadout.CopyFrom(sourceLoadout) + go sandboxLoadout.Save() + lastAppliedLoadoutAttribute.ValueJSON = aid.JSONStringify(sourceLoadout.ID) + activeLoadoutIndexAttribute.ValueJSON = aid.JSONStringify(body.SourceIndex) + go lastAppliedLoadoutAttribute.Save() + go activeLoadoutIndexAttribute.Save() + + return nil + } + + activeLoadout := profile.Loadouts.GetLoadout(loadouts[body.TargetIndex]) + if activeLoadout == nil { + return fmt.Errorf("target loadout not found") + } + + sandboxLoadout.CopyFrom(activeLoadout) + go sandboxLoadout.Save() + + if len(profile.Changes) == 0{ + // dance ids and item wrap ids are registered as changes in the client so force a fix + profile.CreateLoadoutChangedChange(sandboxLoadout, "DanceID") + } + + return nil +} + +func clientDeleteCosmeticLoadoutAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + FallbackLoadoutIndex int `json:"fallbackLoadoutIndex" binding:"required"` + LoadoutIndex int `json:"index" binding:"required"` + } + + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + lastAppliedLoadoutAttribute := profile.Attributes.GetAttributeByKey("last_applied_loadout") + if lastAppliedLoadoutAttribute == nil { + return fmt.Errorf("last_applied_loadout not found") + } + + activeLoadoutIndexAttribute := profile.Attributes.GetAttributeByKey("active_loadout_index") + if activeLoadoutIndexAttribute == nil { + return fmt.Errorf("active_loadout_index not found") + } + + loadoutsAttribute := profile.Attributes.GetAttributeByKey("loadouts") + if loadoutsAttribute == nil { + return fmt.Errorf("loadouts not found") + } + loadouts := p.AttributeConvertToSlice[string](loadoutsAttribute) + + if body.LoadoutIndex >= len(loadouts) { + return fmt.Errorf("loadout index out of range") + } + + if body.LoadoutIndex == 0 { + return fmt.Errorf("cannot delete default loadout") + } + + if body.FallbackLoadoutIndex == -1 { + body.FallbackLoadoutIndex = 0 + } + + fallbackLoadout := profile.Loadouts.GetLoadout(loadouts[body.FallbackLoadoutIndex]) + if fallbackLoadout == nil { + return fmt.Errorf("fallback loadout not found") + } + + lastAppliedLoadoutAttribute.ValueJSON = aid.JSONStringify(fallbackLoadout.ID) + activeLoadoutIndexAttribute.ValueJSON = aid.JSONStringify(body.FallbackLoadoutIndex) + lastAppliedLoadoutAttribute.Save() + activeLoadoutIndexAttribute.Save() + + profile.Loadouts.DeleteLoadout(loadouts[body.LoadoutIndex]) + loadouts = append(loadouts[:body.LoadoutIndex], loadouts[body.LoadoutIndex+1:]...) + loadoutsAttribute.ValueJSON = aid.JSONStringify(loadouts) + loadoutsAttribute.Save() + return nil } @@ -394,12 +580,9 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p } vbucks.Quantity -= body.ExpectedTotalPrice - - go func() { - profile0Vbucks.Quantity = vbucks.Quantity - vbucks.Save() - profile0Vbucks.Save() - }() + profile0Vbucks.Quantity = vbucks.Quantity + vbucks.Save() + profile0Vbucks.Save() if offer.ProfileType != "athena" { return fmt.Errorf("save the world not implemeted yet") @@ -481,12 +664,9 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro } vbucks.Quantity -= price - - go func() { - profile0Vbucks.Quantity = price - vbucks.Save() - profile0Vbucks.Save() - }() + profile0Vbucks.Quantity = price + vbucks.Save() + profile0Vbucks.Save() for _, receiverAccountId := range body.ReceiverAccountIds { receiverPerson := p.Find(receiverAccountId) @@ -501,8 +681,7 @@ func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Pro gift.AddLoot(item) } - receiverPerson.CommonCoreProfile.Gifts.AddGift(gift) - receiverPerson.CommonCoreProfile.Save() + receiverPerson.CommonCoreProfile.Gifts.AddGift(gift).Save() } return nil diff --git a/main.go b/main.go index e8b087a..5db7156 100644 --- a/main.go +++ b/main.go @@ -45,14 +45,19 @@ func init() { fortnite.GenerateRandomStorefront() fortnite.GeneratePlaylistImages() - if found := person.FindByDisplay("god"); found == nil { - god := fortnite.NewFortnitePersonWithId("god", "god", true) - god.AddPermission("all") + for _, username := range aid.Config.Accounts.Gods { + found := person.FindByDisplay(username) + if found == nil { + found = fortnite.NewFortnitePersonWithId(username, username, true) + } - angel := fortnite.NewFortnitePersonWithId("angel", "angel", true) - angel.AddPermission("all") + for _, perm := range found.Permissions { + found.RemovePermission(perm) + } + + found.AddPermission("all") + aid.Print("(snow) max account " + username + " loaded") } - } func main() { r := fiber.New(fiber.Config{ diff --git a/person/attribute.go b/person/attribute.go index 5397f15..25b5ed8 100644 --- a/person/attribute.go +++ b/person/attribute.go @@ -54,4 +54,18 @@ func (a *Attribute) Save() { return } storage.Repo.SaveAttribute(a.ToDatabase(a.ProfileID)) +} + +func AttributeConvertToSlice[T any](attribute *Attribute) []T { + valuesRaw := aid.JSONParse(attribute.ValueJSON).([]interface{}) + values := make([]T, len(valuesRaw)) + for i, value := range valuesRaw { + values[i] = value.(T) + } + + return values +} + +func AttributeConvert[T any](attribute *Attribute) T { + return aid.JSONParse(attribute.ValueJSON).(T) } \ No newline at end of file diff --git a/person/loadout.go b/person/loadout.go index e91f9d5..74a9908 100644 --- a/person/loadout.go +++ b/person/loadout.go @@ -68,7 +68,8 @@ func NewLoadout(name string, athena *Profile) *Loadout { return &Loadout{ ID: uuid.New().String(), - PersonID: athena.ID, + PersonID: athena.PersonID, + ProfileID: athena.ID, TemplateID: "CosmeticLocker:CosmeticLocker_Athena", LockerName: name, CharacterID: aid.JSONParse(character.ValueJSON).(string), @@ -85,6 +86,12 @@ func NewLoadout(name string, athena *Profile) *Loadout { } } +func NewLoadoutWithID(id string, name string, athena *Profile) *Loadout { + loadout := NewLoadout(name, athena) + loadout.ID = id + return loadout +} + func FromDatabaseLoadout(loadout *storage.DB_Loadout) *Loadout { return &Loadout{ ID: loadout.ID, @@ -229,14 +236,7 @@ func (l *Loadout) GetItemsSlotData(itemIds []string) aid.JSON { items := json["items"].([]string) items[pos] = item.TemplateID - - activeVariants := json["activeVariants"].([]aid.JSON) - activeVariants[pos] = aid.JSON{ - "variants": []aid.JSON{}, - } - json["items"] = items - json["activeVariants"] = activeVariants } return json @@ -268,4 +268,25 @@ func (l *Loadout) ToDatabase(profileId string) *storage.DB_Loadout { func (q *Loadout) Save() { storage.Repo.SaveLoadout(q.ToDatabase(q.ProfileID)) +} + +func (l *Loadout) Copy() Loadout { + return *l +} + +func (l *Loadout) CopyFrom(loadout *Loadout) { + l.ProfileID = loadout.ProfileID + l.BannerID = loadout.BannerID + l.BannerColorID = loadout.BannerColorID + l.CharacterID = loadout.CharacterID + l.PickaxeID = loadout.PickaxeID + l.BackpackID = loadout.BackpackID + l.GliderID = loadout.GliderID + copy(l.DanceID, loadout.DanceID) + copy(l.ItemWrapID, loadout.ItemWrapID) + l.ContrailID = loadout.ContrailID + l.LoadingScreenID = loadout.LoadingScreenID + l.MusicPackID = loadout.MusicPackID + + l.Save() } \ No newline at end of file diff --git a/person/sync.go b/person/sync.go index d6f073e..5a16c52 100644 --- a/person/sync.go +++ b/person/sync.go @@ -255,11 +255,11 @@ func (m *LoadoutMutex) AddLoadout(loadout *Loadout) *Loadout { loadout.PersonID = m.PersonID loadout.ProfileID = m.ProfileID m.Store(loadout.ID, loadout) - // storage.Repo.SaveLoadout(loadout.ToDatabase(m.ProfileID)) + storage.Repo.SaveLoadout(loadout.ToDatabase(m.ProfileID)) return loadout } -func (m *LoadoutMutex) DeleteItem(id string) { +func (m *LoadoutMutex) DeleteLoadout(id string) { loadout := m.GetLoadout(id) if loadout == nil { return diff --git a/readme.md b/readme.md index c4da15f..8374a65 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ Performance first, universal Fortnite private server backend written in Go. ## What's next? -- More profile actions like `RefundMtxPurchase` and `CopyCosmeticLoadout`. +- More profile actions like `RefundMtxPurchase` and more. - Integrating matchmaking with a hoster to smartly put players into games and know when servers become available. - Interact with external services like Amazon S3 or Cloudflare R2 to save player data externally. - Refactor the XMPP solution to use [melium/xmpp](https://github.com/mellium/xmpp). @@ -23,6 +23,7 @@ Performance first, universal Fortnite private server backend written in Go. ### Supported - **_Chapter 1 Season 2_** `Fortnite+Release-2.5-CL-3889387-Windows` +- **_Chapter 1 Season 3_** `Fortnite+Release-3.6-CL-4019403-Windows` - **_Chapter 1 Season 5_** `Fortnite+Release-5.41-CL-4363240-Windows` - **_Chapter 1 Season 8_** `Fortnite+Release-8.51-CL-6165369-Windows` - **_Chapter 2 Season 2_** `Fortnite+Release-12.41-CL-12905909-Windows`