From 3f5adcba6ca4efbc66cb785027b8356e7b70681b Mon Sep 17 00:00:00 2001 From: Eccentric Date: Wed, 31 Jan 2024 21:40:47 +0000 Subject: [PATCH] Gifting support --- fortnite/shop.go | 16 ++++- handlers/client.go | 131 ++++++++++++++++++++++++++++++++-------- handlers/storage.go | 16 ++--- person/relationships.go | 5 +- readme.md | 7 ++- storage/hotfix.go | 5 +- 6 files changed, 137 insertions(+), 43 deletions(-) diff --git a/fortnite/shop.go b/fortnite/shop.go index a9aced5..ea791c2 100644 --- a/fortnite/shop.go +++ b/fortnite/shop.go @@ -296,12 +296,13 @@ func (e *Entry) GenerateResponse(p *person.Person) aid.JSON { "itemGrants": []aid.JSON{}, "metaInfo": e.Meta, "meta": aid.JSON{}, - "displayAssetPath": e.DisplayAssetPath, "title": e.Title, + "displayAssetPath": e.DisplayAssetPath, "shortDescription": e.ShortDescription, } grants := []aid.JSON{} requirements := []aid.JSON{} + purchaseRequirements := []aid.JSON{} meta := []aid.JSON{} for _, templateId := range e.Grants { @@ -316,6 +317,12 @@ func (e *Entry) GenerateResponse(p *person.Person) aid.JSON { "requiredId": item.ID, "minQuantity": 1, }) + + purchaseRequirements = append(purchaseRequirements, aid.JSON{ + "requirementType": "DenyOnItemOwnership", + "requiredId": item.ID, + "minQuantity": 1, + }) } } @@ -331,6 +338,13 @@ func (e *Entry) GenerateResponse(p *person.Person) aid.JSON { json["itemGrants"] = grants json["requirements"] = requirements json["metaInfo"] = meta + json["giftInfo"] = aid.JSON{ + "bIsEnabled": true, + "forcedGiftBoxTemplateId": "", + "purchaseRequirements": purchaseRequirements, + "giftRecordIds": []any{}, + } + return json } diff --git a/handlers/client.go b/handlers/client.go index d0942a9..cb406e3 100644 --- a/handlers/client.go +++ b/handlers/client.go @@ -24,6 +24,8 @@ var ( "SetCosmeticLockerSlot": clientSetCosmeticLockerSlotAction, "SetCosmeticLockerBanner": clientSetCosmeticLockerBannerAction, "PurchaseCatalogEntry": clientPurchaseCatalogEntryAction, + "GiftCatalogEntry": clientGiftCatalogEntryAction, + "RemoveGiftBox": clientRemoveGiftBoxAction, } ) @@ -70,8 +72,7 @@ func PostClientProfileAction(c *fiber.Ctx) error { profile.Diff(profileSnapshot) } - known, _ := strconv.Atoi(c.Query("rvn")) - revision := known + revision, _ := strconv.Atoi(c.Query("rvn")) if revision == -1 { revision = profile.Revision } @@ -81,8 +82,6 @@ func PostClientProfileAction(c *fiber.Ctx) error { delete(profileSnapshots, profile.Type) multiUpdate := []aid.JSON{} - if known != -1 { - for key := range profileSnapshots { profile := person.GetProfileFromType(key) if profile == nil { @@ -105,7 +104,6 @@ func PostClientProfileAction(c *fiber.Ctx) error { profile.ClearProfileChanges() go profile.Save() } - } return c.Status(200).JSON(aid.JSON{ "profileId": c.Query("profileId"), @@ -121,11 +119,7 @@ func PostClientProfileAction(c *fiber.Ctx) error { } func clientQueryProfileAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { - known, _ := strconv.Atoi(c.Query("rvn")) - - if known == -1 { - profile.CreateFullProfileUpdateChange() - } + profile.CreateFullProfileUpdateChange() return nil } @@ -134,8 +128,7 @@ func clientMarkItemSeenAction(c *fiber.Ctx, person *p.Person, profile *p.Profile ItemIds []string `json:"itemIds"` } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -159,8 +152,7 @@ func clientEquipBattleRoyaleCustomizationAction(c *fiber.Ctx, person *p.Person, IndexWithinSlot int `json:"indexWithinSlot"` } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -207,8 +199,7 @@ func clientSetBattleRoyaleBannerAction(c *fiber.Ctx, person *p.Person, profile * HomebaseBannerIconID string `json:"homebaseBannerIconId" binding:"required"` } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -248,8 +239,7 @@ func clientSetItemFavoriteStatusBatchAction(c *fiber.Ctx, person *p.Person, prof Favorite []bool `json:"itemFavStatus" binding:"required"` } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -275,8 +265,7 @@ func clientSetCosmeticLockerSlotAction(c *fiber.Ctx, person *p.Person, profile * VariantUpdates []aid.JSON `json:"variantUpdates" binding:"required"` // variant updates } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -342,8 +331,7 @@ func clientSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile BannerIconTemplateName string `json:"bannerIconTemplateName" binding:"required"` // template id } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -372,14 +360,13 @@ func clientSetCosmeticLockerBannerAction(c *fiber.Ctx, person *p.Person, profile } func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { - var body struct{ + var body struct { OfferID string `json:"offerId" binding:"required"` PurchaseQuantity int `json:"purchaseQuantity" binding:"required"` ExpectedTotalPrice int `json:"expectedTotalPrice" binding:"required"` } - err := c.BodyParser(&body) - if err != nil { + if err := c.BodyParser(&body); err != nil { return fmt.Errorf("invalid Body") } @@ -409,7 +396,7 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p vbucks.Quantity -= body.ExpectedTotalPrice go func() { - profile0Vbucks.Quantity = vbucks.Quantity // for season 2 and lower + profile0Vbucks.Quantity = vbucks.Quantity vbucks.Save() profile0Vbucks.Save() }() @@ -449,5 +436,97 @@ func clientPurchaseCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p "primary": true, }) + return nil +} + +func clientGiftCatalogEntryAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + Currency string `json:"currency" binding:"required"` + CurrencySubType string `json:"currencySubType" binding:"required"` + ExpectedTotalPrice int `json:"expectedTotalPrice" binding:"required"` + GameContext string `json:"gameContext" binding:"required"` + GiftWrapTemplateId string `json:"giftWrapTemplateId" binding:"required"` + PersonalMessage string `json:"personalMessage" binding:"required"` + ReceiverAccountIds []string `json:"receiverAccountIds" binding:"required"` + OfferId string `json:"offerId" binding:"required"` + } + + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + offer := fortnite.StaticCatalog.GetOfferById(body.OfferId) + if offer == nil { + return fmt.Errorf("offer not found") + } + + if offer.Price != body.ExpectedTotalPrice { + return fmt.Errorf("invalid price") + } + + price := offer.Price * len(body.ReceiverAccountIds) + + vbucks := profile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if vbucks == nil { + return fmt.Errorf("vbucks not found") + } + + profile0Vbucks := person.Profile0Profile.Items.GetItemByTemplateID("Currency:MtxPurchased") + if profile0Vbucks == nil { + return fmt.Errorf("profile0vbucks not found") + } + + if vbucks.Quantity < price { + return fmt.Errorf("not enough vbucks") + } + + vbucks.Quantity -= price + + go func() { + profile0Vbucks.Quantity = price + vbucks.Save() + profile0Vbucks.Save() + }() + + for _, receiverAccountId := range body.ReceiverAccountIds { + receiverPerson := p.Find(receiverAccountId) + if receiverPerson == nil { + continue + } + + gift := p.NewGift(body.GiftWrapTemplateId, 1, person.ID, body.PersonalMessage) + for _, grant := range offer.Grants { + item := p.NewItem(grant, 1) + item.ProfileType = offer.ProfileType + gift.AddLoot(item) + } + + receiverPerson.CommonCoreProfile.Gifts.AddGift(gift) + receiverPerson.CommonCoreProfile.Save() + } + + return nil +} + +func clientRemoveGiftBoxAction(c *fiber.Ctx, person *p.Person, profile *p.Profile, notifications *[]aid.JSON) error { + var body struct { + GiftBoxItemId string `json:"giftBoxItemId" binding:"required"` + } + + if err := c.BodyParser(&body); err != nil { + return fmt.Errorf("invalid Body") + } + + gift := profile.Gifts.GetGift(body.GiftBoxItemId) + if gift == nil { + return fmt.Errorf("gift not found") + } + + for _, item := range gift.Loot { + person.GetProfileFromType(item.ProfileType).Items.AddItem(item).Save() + } + + profile.Gifts.DeleteGift(gift.ID) + return nil } \ No newline at end of file diff --git a/handlers/storage.go b/handlers/storage.go index 4ff331f..c4f6494 100644 --- a/handlers/storage.go +++ b/handlers/storage.go @@ -38,11 +38,11 @@ func GetCloudStorageFiles(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusOK).JSON(result) + return c.Status(200).JSON(result) } func GetCloudStorageConfig(c *fiber.Ctx) error { - return c.Status(fiber.StatusOK).JSON(aid.JSON{ + return c.Status(200).JSON(aid.JSON{ "enumerateFilesPath": "/api/cloudstorage/system", "enableMigration": true, "enableWrites": true, @@ -55,15 +55,17 @@ func GetCloudStorageConfig(c *fiber.Ctx) error { } func GetCloudStorageFile(c *fiber.Ctx) error { + c.Set("Content-Type", "application/octet-stream") switch c.Params("fileName") { case "DefaultEngine.ini": - c.Set("Content-Type", "application/octet-stream") - c.Status(fiber.StatusOK) - c.Send(storage.GetDefaultEngine()) - return nil + return c.Status(200).Send(storage.GetDefaultEngine()) + case "DefaultGame.ini": + return c.Status(200).Send(storage.GetDefaultGame()) + case "DefaultRuntimeOptions.ini": + return c.Status(200).Send(storage.GetDefaultRuntime()) } - return c.Status(400).JSON(aid.ErrorBadRequest) + return c.Status(404).JSON(aid.ErrorBadRequest("File not found")) } func GetUserStorageFiles(c *fiber.Ctx) error { diff --git a/person/relationships.go b/person/relationships.go index 2d2e171..4811c7c 100644 --- a/person/relationships.go +++ b/person/relationships.go @@ -2,6 +2,7 @@ package person import ( "fmt" + "time" "github.com/ectrc/snow/aid" "github.com/ectrc/snow/storage" @@ -33,7 +34,7 @@ func (r *Relationship) ToDatabase() *storage.DB_Relationship { func (r *Relationship) GenerateFortniteFriendEntry(t RelationshipGenerateType) aid.JSON { result := aid.JSON{ "status": r.Status, - "created": "0000-00-00T00:00:00.000Z", + "created": time.Now().Add(-time.Hour * 24 * 3).Format(time.RFC3339), "favorite": false, } @@ -46,8 +47,6 @@ func (r *Relationship) GenerateFortniteFriendEntry(t RelationshipGenerateType) a result["accountId"] = r.From.ID } - aid.PrintJSON(result) - return result } diff --git a/readme.md b/readme.md index f09336e..c4da15f 100644 --- a/readme.md +++ b/readme.md @@ -13,9 +13,10 @@ Performance first, universal Fortnite private server backend written in Go. ## What's next? -- Gifting, Matchmaker and Battle Pass support. -- Interact with external Services like Amazon S3 Buckets to save player data externally. -- Refactor the XMPP solution to use [melium/xmpp](https://github.com/mellium/xmpp) +- More profile actions like `RefundMtxPurchase` and `CopyCosmeticLoadout`. +- 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). ## Version Support diff --git a/storage/hotfix.go b/storage/hotfix.go index e768090..295d972 100644 --- a/storage/hotfix.go +++ b/storage/hotfix.go @@ -53,7 +53,7 @@ bShouldCheckIfPlatformAllowed=false`) } func GetDefaultRuntime() []byte { -return []byte(` + return []byte(` [/Script/FortniteGame.FortRuntimeOptions] bEnableGlobalChat=true bDisableGifting=false @@ -61,6 +61,5 @@ bDisableGiftingPC=false bDisableGiftingPS4=false bDisableGiftingXB=false !ExperimentalCohortPercent=ClearArray -+ExperimentalCohortPercent=(CohortPercent=100,ExperimentNum=20) -`) ++ExperimentalCohortPercent=(CohortPercent=100,ExperimentNum=20)`) } \ No newline at end of file