From 2d96b4ae31127c324d96ec7fbe994d4fffd490e6 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 15:52:25 +0200 Subject: [PATCH 01/84] Update GDUnit --- addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs index 14a1355b..6beff479 100644 --- a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs @@ -188,29 +188,29 @@ internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) public partial class GdUnit4CSharpApi : RefCounted { - [Signal] - public delegate void ExecutionCompletedEventHandler(); + [Signal] + public delegate void ExecutionCompletedEventHandler(); - public static bool IsApiLoaded() - { - GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); - return false; - } + public static bool IsApiLoaded() + { + GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); + return false; + } - public static string Version() - => "Unknown"; + public static string Version() + => "Unknown"; - public static Array DiscoverTests(CSharpScript sourceScript) => new(); + public static Array DiscoverTests(CSharpScript sourceScript) => new(); - public void ExecuteAsync(Array tests, Callable listener) - { - } + public void ExecuteAsync(Array tests, Callable listener) + { + } - public static bool IsTestSuite(CSharpScript script) - => false; + public static bool IsTestSuite(CSharpScript script) + => false; - public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) - => new(); + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => new(); } #endif From a14ac7c769126872db6548714a918c10b34b6636 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Fri, 3 Oct 2025 20:40:45 +0200 Subject: [PATCH 02/84] Update gdUnit to a pre-release compatible with Godot 4.5 --- addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs index 6beff479..14a1355b 100644 --- a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs @@ -188,29 +188,29 @@ internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) public partial class GdUnit4CSharpApi : RefCounted { - [Signal] - public delegate void ExecutionCompletedEventHandler(); + [Signal] + public delegate void ExecutionCompletedEventHandler(); - public static bool IsApiLoaded() - { - GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); - return false; - } + public static bool IsApiLoaded() + { + GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); + return false; + } - public static string Version() - => "Unknown"; + public static string Version() + => "Unknown"; - public static Array DiscoverTests(CSharpScript sourceScript) => new(); + public static Array DiscoverTests(CSharpScript sourceScript) => new(); - public void ExecuteAsync(Array tests, Callable listener) - { - } + public void ExecuteAsync(Array tests, Callable listener) + { + } - public static bool IsTestSuite(CSharpScript script) - => false; + public static bool IsTestSuite(CSharpScript script) + => false; - public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) - => new(); + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => new(); } #endif From 68df358b66712049e85b69db31d0c6d321dfb828 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 15:52:25 +0200 Subject: [PATCH 03/84] Upgrade GDUnit --- addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs index 14a1355b..6beff479 100644 --- a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs @@ -188,29 +188,29 @@ internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) public partial class GdUnit4CSharpApi : RefCounted { - [Signal] - public delegate void ExecutionCompletedEventHandler(); + [Signal] + public delegate void ExecutionCompletedEventHandler(); - public static bool IsApiLoaded() - { - GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); - return false; - } + public static bool IsApiLoaded() + { + GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); + return false; + } - public static string Version() - => "Unknown"; + public static string Version() + => "Unknown"; - public static Array DiscoverTests(CSharpScript sourceScript) => new(); + public static Array DiscoverTests(CSharpScript sourceScript) => new(); - public void ExecuteAsync(Array tests, Callable listener) - { - } + public void ExecuteAsync(Array tests, Callable listener) + { + } - public static bool IsTestSuite(CSharpScript script) - => false; + public static bool IsTestSuite(CSharpScript script) + => false; - public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) - => new(); + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => new(); } #endif From 674553f0b2dd24b0639d602356430bd496714f7e Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 22:45:52 +0200 Subject: [PATCH 04/84] Update GDUnit --- addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid | 1 + addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid | 1 + 2 files changed, 2 insertions(+) create mode 100644 addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid create mode 100644 addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid new file mode 100644 index 00000000..7b32adad --- /dev/null +++ b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://riwtcdugf7la diff --git a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid new file mode 100644 index 00000000..d581f7d4 --- /dev/null +++ b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://cftsh0exnhxgm From 9dee1f5cd8d2028e468fecbc204e93e7761b2806 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 16:52:26 +0200 Subject: [PATCH 05/84] Add new Models --- .../Models/CatalogAlternateId.gd | 20 ++++ .../Models/CatalogAlternateId.gd.uid | 1 + addons/godot-playfab/Models/CatalogItem.gd | 98 +++++++++++++++++++ .../godot-playfab/Models/CatalogItem.gd.uid | 1 + .../Models/CatalogItemReference.gd | 23 +++++ .../Models/CatalogItemReference.gd.uid | 1 + addons/godot-playfab/Models/CatalogPrice.gd | 23 +++++ .../godot-playfab/Models/CatalogPrice.gd.uid | 1 + .../Models/CatalogPriceAmount.gd | 20 ++++ .../Models/CatalogPriceAmount.gd.uid | 1 + .../Models/CatalogPriceAmountOverride.gd | 23 +++++ .../Models/CatalogPriceAmountOverride.gd.uid | 1 + .../Models/CatalogPriceOptions.gd | 17 ++++ .../Models/CatalogPriceOptions.gd.uid | 1 + .../Models/CatalogPriceOptionsOverride.gd | 17 ++++ .../Models/CatalogPriceOptionsOverride.gd.uid | 1 + .../Models/CatalogPriceOverride.gd | 17 ++++ .../Models/CatalogPriceOverride.gd.uid | 1 + addons/godot-playfab/Models/Content.gd | 32 ++++++ addons/godot-playfab/Models/Content.gd.uid | 1 + addons/godot-playfab/Models/DeepLink.gd | 20 ++++ addons/godot-playfab/Models/DeepLink.gd.uid | 1 + addons/godot-playfab/Models/FilterOptions.gd | 20 ++++ .../godot-playfab/Models/FilterOptions.gd.uid | 1 + .../godot-playfab/Models/ModerationState.gd | 29 ++++++ .../Models/ModerationState.gd.uid | 1 + addons/godot-playfab/Models/Permissions.gd | 17 ++++ .../godot-playfab/Models/Permissions.gd.uid | 1 + addons/godot-playfab/Models/PlayFabImage.gd | 26 +++++ .../godot-playfab/Models/PlayFabImage.gd.uid | 1 + addons/godot-playfab/Models/Rating.gd | 35 +++++++ addons/godot-playfab/Models/Rating.gd.uid | 1 + .../Models/RealMoneyPriceDetails.gd | 32 ++++++ .../Models/RealMoneyPriceDetails.gd.uid | 1 + addons/godot-playfab/Models/StoreDetails.gd | 23 +++++ .../godot-playfab/Models/StoreDetails.gd.uid | 1 + .../godot-playfab/Scenes/PlayFabMainScreen.gd | 43 ++++---- 37 files changed, 533 insertions(+), 20 deletions(-) create mode 100644 addons/godot-playfab/Models/CatalogAlternateId.gd create mode 100644 addons/godot-playfab/Models/CatalogAlternateId.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogItem.gd create mode 100644 addons/godot-playfab/Models/CatalogItem.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogItemReference.gd create mode 100644 addons/godot-playfab/Models/CatalogItemReference.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPrice.gd create mode 100644 addons/godot-playfab/Models/CatalogPrice.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPriceAmount.gd create mode 100644 addons/godot-playfab/Models/CatalogPriceAmount.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPriceAmountOverride.gd create mode 100644 addons/godot-playfab/Models/CatalogPriceAmountOverride.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPriceOptions.gd create mode 100644 addons/godot-playfab/Models/CatalogPriceOptions.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd create mode 100644 addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd.uid create mode 100644 addons/godot-playfab/Models/CatalogPriceOverride.gd create mode 100644 addons/godot-playfab/Models/CatalogPriceOverride.gd.uid create mode 100644 addons/godot-playfab/Models/Content.gd create mode 100644 addons/godot-playfab/Models/Content.gd.uid create mode 100644 addons/godot-playfab/Models/DeepLink.gd create mode 100644 addons/godot-playfab/Models/DeepLink.gd.uid create mode 100644 addons/godot-playfab/Models/FilterOptions.gd create mode 100644 addons/godot-playfab/Models/FilterOptions.gd.uid create mode 100644 addons/godot-playfab/Models/ModerationState.gd create mode 100644 addons/godot-playfab/Models/ModerationState.gd.uid create mode 100644 addons/godot-playfab/Models/Permissions.gd create mode 100644 addons/godot-playfab/Models/Permissions.gd.uid create mode 100644 addons/godot-playfab/Models/PlayFabImage.gd create mode 100644 addons/godot-playfab/Models/PlayFabImage.gd.uid create mode 100644 addons/godot-playfab/Models/Rating.gd create mode 100644 addons/godot-playfab/Models/Rating.gd.uid create mode 100644 addons/godot-playfab/Models/RealMoneyPriceDetails.gd create mode 100644 addons/godot-playfab/Models/RealMoneyPriceDetails.gd.uid create mode 100644 addons/godot-playfab/Models/StoreDetails.gd create mode 100644 addons/godot-playfab/Models/StoreDetails.gd.uid diff --git a/addons/godot-playfab/Models/CatalogAlternateId.gd b/addons/godot-playfab/Models/CatalogAlternateId.gd new file mode 100644 index 00000000..902ca43d --- /dev/null +++ b/addons/godot-playfab/Models/CatalogAlternateId.gd @@ -0,0 +1,20 @@ +extends JsonSerializable +class_name CatalogAlternateId + +## Type of the alternate ID. +var Type: String + +## Value of the alternate ID. +var Value: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogAlternateId.gd.uid b/addons/godot-playfab/Models/CatalogAlternateId.gd.uid new file mode 100644 index 00000000..b833aa68 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogAlternateId.gd.uid @@ -0,0 +1 @@ +uid://cyb2w6nccaqcj diff --git a/addons/godot-playfab/Models/CatalogItem.gd b/addons/godot-playfab/Models/CatalogItem.gd new file mode 100644 index 00000000..a8e2a5b2 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogItem.gd @@ -0,0 +1,98 @@ +extends JsonSerializable +class_name CatalogItem + +## The alternate IDs associated with this item. An alternate ID can be set to 'FriendlyId' or any of the supported marketplace names. +var AlternateIds: Array[CatalogAlternateId] + +## The client-defined type of the item. +var ContentType: String + +## The set of content/files associated with this item. Up to 100 files can be added to an item. +var Contents: Array[Content] + +## The date and time when this item was created. +var CreationDate: String + +## The ID of the creator of this catalog item. +var CreatorEntity: EntityKey + +## The set of platform specific deep links for this item. +var DeepLinks: Array[DeepLink] + +## The Stack Id that will be used as default for this item in Inventory when an explicit one is not provided. This DefaultStackId can be a static stack id or '{guid}', which will generate a unique stack id for the item. If null, Inventory's default stack id will be used. +var DefaultStackId: String + +## A dictionary of localized descriptions. Key is language code and localized string is the value. The NEUTRAL locale is required. Descriptions have a 10000 character limit per country code. +var Description: Dictionary[String, String] + +## Game specific properties for display purposes. This is an arbitrary JSON blob. The Display Properties field has a 10000 byte limit per item. +var DisplayProperties: Dictionary + +## The user provided version of the item for display purposes. Maximum character length of 50. +var DisplayVersion: String + +## The current ETag value that can be used for optimistic concurrency in the If-None-Match header. +var ETag: String + +## The date of when the item will cease to be available. If not provided then the product will be available indefinitely. +var EndDate: String + +## The unique ID of the item. +var Id: String + +## The images associated with this item. Images can be thumbnails or screenshots. Up to 100 images can be added to an item. Only .png, .jpg, .gif, and .bmp file types can be uploaded +var Images: Array[PlayFabImage] + +## Indicates if the item is hidden. +var IsHidden: bool + +## The item references associated with this item. For example, the items in a Bundle/Store/Subscription. Every item can have up to 50 item references. +var ItemReferences: Array[CatalogItemReference] + +## A dictionary of localized keywords. Key is language code and localized list of keywords is the value. Keywords have a 50 character limit per keyword and up to 32 keywords can be added per country code. +var Keywords: Dictionary[String, String] + +## The date and time this item was last updated. +var LastModifiedDate: String + +## The moderation state for this item. +var Moderation: ModerationState + +## The platforms supported by this item. +var Platforms: Array[String] + +## The prices the item can be purchased for. +var PriceOptions: CatalogPriceOptions + +## Rating summary for this item. +var Rating: Rating + +## The real price the item was purchased for per marketplace. +var RealMoneyPriceDetails: RealMoneyPriceDetails + +## The date of when the item will be available. If not provided then the product will appear immediately. +var StartDate: String + +## Optional details for stores items. +var StoreDetails: StoreDetails + +## The list of tags that are associated with this item. Up to 32 tags can be added to an item. +var Tags: Array[String] + +## A dictionary of localized titles. Key is language code and localized string is the value. The NEUTRAL locale is required. Titles have a 512 character limit per country code. +var Title: Dictionary[String, String] + +## The high-level type of the item. The following item types are supported: bundle, catalogItem, currency, store, ugc, subscription. +var Type: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogItem.gd.uid b/addons/godot-playfab/Models/CatalogItem.gd.uid new file mode 100644 index 00000000..1e718f41 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogItem.gd.uid @@ -0,0 +1 @@ +uid://ddo316sr85mhp diff --git a/addons/godot-playfab/Models/CatalogItemReference.gd b/addons/godot-playfab/Models/CatalogItemReference.gd new file mode 100644 index 00000000..0f1490fe --- /dev/null +++ b/addons/godot-playfab/Models/CatalogItemReference.gd @@ -0,0 +1,23 @@ +extends JsonSerializable +class_name CatalogItemReference + +## The amount of the catalog item. +var Amount: float + +## The unique ID of the catalog item. +var Id: String + +## The prices the catalog item can be purchased for. +var PriceOptions: CatalogPriceOptions + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogItemReference.gd.uid b/addons/godot-playfab/Models/CatalogItemReference.gd.uid new file mode 100644 index 00000000..290960b9 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogItemReference.gd.uid @@ -0,0 +1 @@ +uid://bgq16spf627x6 diff --git a/addons/godot-playfab/Models/CatalogPrice.gd b/addons/godot-playfab/Models/CatalogPrice.gd new file mode 100644 index 00000000..23217a4a --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPrice.gd @@ -0,0 +1,23 @@ +extends JsonSerializable +class_name CatalogPrice + +## The amounts of the catalog item price. Each price can have up to 15 item amounts. +var Amounts: Array[CatalogPriceAmount] + +## The per-unit amount this price can be used to purchase. +var UnitAmount: float + +## The per-unit duration this price can be used to purchase. The maximum duration is 100 years. +var UnitDurationInSeconds: float + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPrice.gd.uid b/addons/godot-playfab/Models/CatalogPrice.gd.uid new file mode 100644 index 00000000..e4ea461c --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPrice.gd.uid @@ -0,0 +1 @@ +uid://box2uyibsjdo8 diff --git a/addons/godot-playfab/Models/CatalogPriceAmount.gd b/addons/godot-playfab/Models/CatalogPriceAmount.gd new file mode 100644 index 00000000..cdc65193 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceAmount.gd @@ -0,0 +1,20 @@ +extends JsonSerializable +class_name CatalogPriceAmount + +## The amount of the price. +var Amount: float + +## The Item Id of the price. +var ItemId: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPriceAmount.gd.uid b/addons/godot-playfab/Models/CatalogPriceAmount.gd.uid new file mode 100644 index 00000000..06a6d488 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceAmount.gd.uid @@ -0,0 +1 @@ +uid://viyyn5up2wtj diff --git a/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd b/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd new file mode 100644 index 00000000..37a34ef0 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd @@ -0,0 +1,23 @@ +extends JsonSerializable +class_name CatalogPriceAmountOverride + +## The exact value that should be utilized in the override. +var FixedValue: float + +## The id of the item this override should utilize. +var ItemId: String + +## The multiplier that will be applied to the base Catalog value to determine what value should be utilized in the override. +var Multiplier: int + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd.uid b/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd.uid new file mode 100644 index 00000000..4aab7a6a --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceAmountOverride.gd.uid @@ -0,0 +1 @@ +uid://bca371bwoypke diff --git a/addons/godot-playfab/Models/CatalogPriceOptions.gd b/addons/godot-playfab/Models/CatalogPriceOptions.gd new file mode 100644 index 00000000..d589d8f4 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOptions.gd @@ -0,0 +1,17 @@ +extends JsonSerializable +class_name CatalogPriceOptions + +## Prices of the catalog item. An item can have up to 15 prices +var Prices: Array[CatalogPrice] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPriceOptions.gd.uid b/addons/godot-playfab/Models/CatalogPriceOptions.gd.uid new file mode 100644 index 00000000..ea522848 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOptions.gd.uid @@ -0,0 +1 @@ +uid://dt1ybgfimvlmq diff --git a/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd b/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd new file mode 100644 index 00000000..4c09a50f --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd @@ -0,0 +1,17 @@ +extends JsonSerializable +class_name CatalogPriceOptionsOverride + +## The prices utilized in the override. +var Prices: Array[CatalogPriceOverride] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd.uid b/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd.uid new file mode 100644 index 00000000..22224b29 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOptionsOverride.gd.uid @@ -0,0 +1 @@ +uid://r7lvb3agn4fc diff --git a/addons/godot-playfab/Models/CatalogPriceOverride.gd b/addons/godot-playfab/Models/CatalogPriceOverride.gd new file mode 100644 index 00000000..f0210c68 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOverride.gd @@ -0,0 +1,17 @@ +extends JsonSerializable +class_name CatalogPriceOverride + +## The currency amounts utilized in the override for a singular price. +var Amounts: Array[CatalogPriceAmountOverride] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/CatalogPriceOverride.gd.uid b/addons/godot-playfab/Models/CatalogPriceOverride.gd.uid new file mode 100644 index 00000000..b0ebef78 --- /dev/null +++ b/addons/godot-playfab/Models/CatalogPriceOverride.gd.uid @@ -0,0 +1 @@ +uid://cs42120j245rh diff --git a/addons/godot-playfab/Models/Content.gd b/addons/godot-playfab/Models/Content.gd new file mode 100644 index 00000000..1533dcc0 --- /dev/null +++ b/addons/godot-playfab/Models/Content.gd @@ -0,0 +1,32 @@ +extends JsonSerializable +class_name Content + +## The content unique ID. +var Id: String + +## The maximum client version that this content is compatible with. Client Versions can be up to 3 segments separated by periods(.) and each segment can have a maximum value of 65535. +var MaxClientVersion: String + +## The minimum client version that this content is compatible with. Client Versions can be up to 3 segments separated by periods(.) and each segment can have a maximum value of 65535. +var MinClientVersion: String + +## The list of tags that are associated with this content. Tags must be defined in the Catalog Config before being used in content. +var Tags: Array[String] + +## The client-defined type of the content. Content Types must be defined in the Catalog Config before being used. +var Type: String + +## The Azure CDN URL for retrieval of the catalog item binary content. +var Url: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/Content.gd.uid b/addons/godot-playfab/Models/Content.gd.uid new file mode 100644 index 00000000..c2c3e093 --- /dev/null +++ b/addons/godot-playfab/Models/Content.gd.uid @@ -0,0 +1 @@ +uid://ctpkxd2bp8h7o diff --git a/addons/godot-playfab/Models/DeepLink.gd b/addons/godot-playfab/Models/DeepLink.gd new file mode 100644 index 00000000..46f5f89a --- /dev/null +++ b/addons/godot-playfab/Models/DeepLink.gd @@ -0,0 +1,20 @@ +extends JsonSerializable +class_name DeepLink + +## Target platform for this deep link. +var Platform: String + +## The deep link for this platform. +var Url: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/DeepLink.gd.uid b/addons/godot-playfab/Models/DeepLink.gd.uid new file mode 100644 index 00000000..7856da65 --- /dev/null +++ b/addons/godot-playfab/Models/DeepLink.gd.uid @@ -0,0 +1 @@ +uid://8takr4sryjsm diff --git a/addons/godot-playfab/Models/FilterOptions.gd b/addons/godot-playfab/Models/FilterOptions.gd new file mode 100644 index 00000000..83b94577 --- /dev/null +++ b/addons/godot-playfab/Models/FilterOptions.gd @@ -0,0 +1,20 @@ +extends JsonSerializable +class_name FilterOptions + +## The OData filter utilized. Mutually exclusive with 'IncludeAllItems'. More info about Filter Complexity limits can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/search#limits +var Filter: String + +## The flag that overrides the filter and allows for returning all catalog items. Mutually exclusive with 'Filter'. +var IncludeAllItems: bool + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/FilterOptions.gd.uid b/addons/godot-playfab/Models/FilterOptions.gd.uid new file mode 100644 index 00000000..0dd86c49 --- /dev/null +++ b/addons/godot-playfab/Models/FilterOptions.gd.uid @@ -0,0 +1 @@ +uid://ckk8jrntu6enk diff --git a/addons/godot-playfab/Models/ModerationState.gd b/addons/godot-playfab/Models/ModerationState.gd new file mode 100644 index 00000000..5628d0aa --- /dev/null +++ b/addons/godot-playfab/Models/ModerationState.gd @@ -0,0 +1,29 @@ +extends JsonSerializable +class_name ModerationState + +## The date and time this moderation state was last updated. +var LastModifiedDate: String + +## The current stated reason for the associated item being moderated. +var Reason: String + +## The current moderation status for the associated item. +var Status: ModerationStatus + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + +enum ModerationStatus { + Unknown, + Moderation, + Approved, + Rejected +} diff --git a/addons/godot-playfab/Models/ModerationState.gd.uid b/addons/godot-playfab/Models/ModerationState.gd.uid new file mode 100644 index 00000000..4081c8ff --- /dev/null +++ b/addons/godot-playfab/Models/ModerationState.gd.uid @@ -0,0 +1 @@ +uid://b8ieu8ubpoxe diff --git a/addons/godot-playfab/Models/Permissions.gd b/addons/godot-playfab/Models/Permissions.gd new file mode 100644 index 00000000..c101a4b3 --- /dev/null +++ b/addons/godot-playfab/Models/Permissions.gd @@ -0,0 +1,17 @@ +extends JsonSerializable +class_name Permissions + +## The list of ids of Segments that the a player can be in to purchase from the store. When a value is provided, the player must be in at least one of the segments listed for the purchase to be allowed. +var SegmentIds: Array[String] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/Permissions.gd.uid b/addons/godot-playfab/Models/Permissions.gd.uid new file mode 100644 index 00000000..e5725ad3 --- /dev/null +++ b/addons/godot-playfab/Models/Permissions.gd.uid @@ -0,0 +1 @@ +uid://djkikiemt13dt diff --git a/addons/godot-playfab/Models/PlayFabImage.gd b/addons/godot-playfab/Models/PlayFabImage.gd new file mode 100644 index 00000000..ec0bbbbc --- /dev/null +++ b/addons/godot-playfab/Models/PlayFabImage.gd @@ -0,0 +1,26 @@ +extends JsonSerializable +class_name PlayFabImage + +## The image unique ID. +var Id: String + +## The client-defined tag associated with this image. Tags must be defined in the Catalog Config before being used in images +var Tag: String + +## Images can be defined as either a "thumbnail" or "screenshot". There can only be one "thumbnail" image per item. +var Type: String + +## The URL for retrieval of the image. +var Url: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/PlayFabImage.gd.uid b/addons/godot-playfab/Models/PlayFabImage.gd.uid new file mode 100644 index 00000000..5675f787 --- /dev/null +++ b/addons/godot-playfab/Models/PlayFabImage.gd.uid @@ -0,0 +1 @@ +uid://bh2jvq70ut0oj diff --git a/addons/godot-playfab/Models/Rating.gd b/addons/godot-playfab/Models/Rating.gd new file mode 100644 index 00000000..8090a43a --- /dev/null +++ b/addons/godot-playfab/Models/Rating.gd @@ -0,0 +1,35 @@ +extends JsonSerializable +class_name Rating + +## The average rating for this item. +var Average: float + +## The total count of 1 star ratings for this item. +var Count1Star: int + +## The total count of 2 star ratings for this item. +var Count2Star: int + +## The total count of 3 star ratings for this item. +var Count3Star: int + +## The total count of 4 star ratings for this item. +var Count4Star: int + +## The total count of 5 star ratings for this item. +var Count5Star: int + +## The total count of ratings for this item. +var TotalCount: int + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/Rating.gd.uid b/addons/godot-playfab/Models/Rating.gd.uid new file mode 100644 index 00000000..98c24f5f --- /dev/null +++ b/addons/godot-playfab/Models/Rating.gd.uid @@ -0,0 +1 @@ +uid://mh1qbb3eqmkc diff --git a/addons/godot-playfab/Models/RealMoneyPriceDetails.gd b/addons/godot-playfab/Models/RealMoneyPriceDetails.gd new file mode 100644 index 00000000..0334c125 --- /dev/null +++ b/addons/godot-playfab/Models/RealMoneyPriceDetails.gd @@ -0,0 +1,32 @@ +extends JsonSerializable +class_name RealMoneyPriceDetails + +## The 'AppleAppStore' price amount per CurrencyCode. 'USD' supported only. +var AppleAppStorePrices: Object + +## The 'GooglePlay' price amount per CurrencyCode. 'USD' supported only. +var GooglePlayPrices: Object + +## The 'MicrosoftStore' price amount per CurrencyCode. 'USD' supported only. +var MicrosoftStorePrices: Object + +## The 'NintendoEShop' price amount per CurrencyCode. 'USD' supported only. +var NintendoEShopPrices: Object + +## The 'PlayStationStore' price amount per CurrencyCode. 'USD' supported only. +var PlayStationStorePrices: Object + +## The 'Steam' price amount per CurrencyCode. 'USD' supported only. +var SteamPrices: Object + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/RealMoneyPriceDetails.gd.uid b/addons/godot-playfab/Models/RealMoneyPriceDetails.gd.uid new file mode 100644 index 00000000..5b362c53 --- /dev/null +++ b/addons/godot-playfab/Models/RealMoneyPriceDetails.gd.uid @@ -0,0 +1 @@ +uid://c6i574furyeqs diff --git a/addons/godot-playfab/Models/StoreDetails.gd b/addons/godot-playfab/Models/StoreDetails.gd new file mode 100644 index 00000000..e6a7ee60 --- /dev/null +++ b/addons/godot-playfab/Models/StoreDetails.gd @@ -0,0 +1,23 @@ +extends JsonSerializable +class_name StoreDetails + +## The options for the filter in filter-based stores. These options are mutually exclusive with item references. +var FilterOptions: FilterOptions + +## The permissions that control which players can purchase from the store. +var Permissions: Permissions + +## The global prices utilized in the store. These options are mutually exclusive with price options in item references. +var PriceOptionsOverride: CatalogPriceOptionsOverride + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/StoreDetails.gd.uid b/addons/godot-playfab/Models/StoreDetails.gd.uid new file mode 100644 index 00000000..949a3d60 --- /dev/null +++ b/addons/godot-playfab/Models/StoreDetails.gd.uid @@ -0,0 +1 @@ +uid://d375y8vs4whit diff --git a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd index 223a4ed1..6b01a147 100644 --- a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd +++ b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd @@ -8,10 +8,10 @@ func _ready(): editor_resource_filesystem_cached = EditorPlugin.new().get_editor_interface().get_resource_filesystem() func _on_SaveModel_pressed(): - + if !guard_class_name_set(): return - + var file_dialog = $FileDialog file_dialog.current_file = $VBoxContainer/ClassNameContainer/LineEdit.text + ".gd" file_dialog.show() @@ -19,47 +19,49 @@ func _on_SaveModel_pressed(): func _on_save_direct_pressed(): - + if !guard_class_name_set(): return - + var file_name = $VBoxContainer/ClassNameContainer/LineEdit.text + ".gd" var file_path = "res://addons/godot-playfab/Models/" + file_name _on_file_selected(file_path) func _on_file_selected(file_path: String): - + var model = to_model($VBoxContainer/ClassNameContainer/LineEdit.text, $VBoxContainer/Input.text) var file = FileAccess.open(file_path, FileAccess.WRITE) file.store_string(model) # Refresh the "FileSystem" panel editor_resource_filesystem_cached.scan() - + print("Saved model to file path: \"%s\"" % file_path) - + func guard_class_name_set() -> bool: if $VBoxContainer/ClassNameContainer/LineEdit.text.is_empty(): $ErrorPopupDialog/Label.text = "Please first enter a Class Name!" $ErrorPopupDialog.popup_centered(Vector2(0,0)) return false - + return true -static func to_model(object_name: String, input: String) -> String: +func to_model(object_name: String, input: String) -> String: var lines = input.split("\n", true) lines.push_back("") # Hack: add an empty line at the bottom so below logic works & is simpler :-) Otherwise, the last prop would not be written - + var props = [] var current_prop = "" var prop_line = 0 + print("new prop") for line in lines: - + print(line) + var str_line = (line as String).strip_edges() - + if not str_line.is_empty(): match prop_line: 0: # Variable name @@ -71,17 +73,17 @@ static func to_model(object_name: String, input: String) -> String: else: current_prop = str_line 2: # Comment - current_prop = "# %s\n%s" % [str_line, current_prop] - + current_prop = "## %s\n%s" % [str_line, current_prop] + prop_line += 1 else: props.append(current_prop) prop_line = 0 - + var model = "extends JsonSerializable\nclass_name " + object_name + "\n\n" for prop in props: model += prop + "\n\n" - + # TODO: Find a way to generate the mapping for props automatically! model += """ func _get_type_for_property(property_name: String) -> String: @@ -90,15 +92,15 @@ func _get_type_for_property(property_name: String) -> String: # return "" _: pass - + push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - + """ return model -static func fix_type(type: String) -> String: - +func fix_type(type: String) -> String: + match type: "string": return "String" @@ -108,3 +110,4 @@ static func fix_type(type: String) -> String: if type.ends_with("]"): return "Array" return type + From f576ab4c5f09004f6d88f514cfde4e7878011042 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 22:42:20 +0200 Subject: [PATCH 06/84] Started implementation of Catalog (Economy V2) API --- addons/godot-playfab/Models/CatalogItem.gd | 6 ++++ .../Models/Catalog_GetItemsRequest.gd | 32 +++++++++++++++++++ .../Models/Catalog_GetItemsRequest.gd.uid | 1 + .../godot-playfab/Models/GetItemsResponse.gd | 19 +++++++++++ .../Models/GetItemsResponse.gd.uid | 1 + addons/godot-playfab/PlayFabCatalog.gd | 15 +++++++++ addons/godot-playfab/PlayFabCatalog.gd.uid | 1 + addons/godot-playfab/PlayFabManager.gd | 5 +++ 8 files changed, 80 insertions(+) create mode 100644 addons/godot-playfab/Models/Catalog_GetItemsRequest.gd create mode 100644 addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid create mode 100644 addons/godot-playfab/Models/GetItemsResponse.gd create mode 100644 addons/godot-playfab/Models/GetItemsResponse.gd.uid create mode 100644 addons/godot-playfab/PlayFabCatalog.gd create mode 100644 addons/godot-playfab/PlayFabCatalog.gd.uid diff --git a/addons/godot-playfab/Models/CatalogItem.gd b/addons/godot-playfab/Models/CatalogItem.gd index a8e2a5b2..05956098 100644 --- a/addons/godot-playfab/Models/CatalogItem.gd +++ b/addons/godot-playfab/Models/CatalogItem.gd @@ -96,3 +96,9 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) +const ITEM_TYPE_BUNDLE: String = "bundle" +const ITEM_TYPE_CATALOG_ITEM: String = "catalogItem" +const ITEM_TYPE_CURRENCY: String = "currency" +const ITEM_TYPE_STORE: String = "store" +const ITEM_TYPE_UGC: String = "ugc" +const ITEM_TYPE_SUBSCRIPTION: String = "subscription" diff --git a/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd b/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd new file mode 100644 index 00000000..1c34ea6c --- /dev/null +++ b/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd @@ -0,0 +1,32 @@ +extends JsonSerializable +## Request to get items from the catalog. +## @tutorial(PlayFab REST API Docs):https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-items?view=playfab-rest +class_name Catalog_GetItemsRequest + +## List of item alternate IDs. +var AlternateIds: Array[CatalogAlternateId] + +## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). +var CustomTags: Dictionary[String, String] + +## The entity to perform this action on. +var Entity: EntityKey + +## List of Item Ids. +var Ids: Array[String] + + +func _init(): + pass + + +func _get_type_for_property(property_name: String) -> String: + match property_name: + "Entity": + return "EntityKey" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid b/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid new file mode 100644 index 00000000..28513869 --- /dev/null +++ b/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid @@ -0,0 +1 @@ +uid://chkgfeb6e8c8o diff --git a/addons/godot-playfab/Models/GetItemsResponse.gd b/addons/godot-playfab/Models/GetItemsResponse.gd new file mode 100644 index 00000000..f01e0e45 --- /dev/null +++ b/addons/godot-playfab/Models/GetItemsResponse.gd @@ -0,0 +1,19 @@ +extends JsonSerializable +## Response of [Catalog] +## @tutorial https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-items?view=playfab-rest#getitemsresponse +class_name GetItemsResponse + +## Metadata of set of items. +var Items: Array[CatalogItem] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/GetItemsResponse.gd.uid b/addons/godot-playfab/Models/GetItemsResponse.gd.uid new file mode 100644 index 00000000..2baa3b95 --- /dev/null +++ b/addons/godot-playfab/Models/GetItemsResponse.gd.uid @@ -0,0 +1 @@ +uid://cds1khgm21iep diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd new file mode 100644 index 00000000..f383d56e --- /dev/null +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -0,0 +1,15 @@ +@icon("res://addons/godot-playfab/icon.png") + +extends PlayFab +class_name PlayFabCatalog + + +## Retrieves items from the public catalog. Up to 50 items can be returned at once. +## GetItems does not work off a cache of the Catalog and should be used when trying to get recent item updates. +## However, please note that item references data is cached and may take a few moments for changes to propagate. +## @tutorial: https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items +## @Visibility: Public +## @param request_data: Catalog_GetItemsRequest - Request object, optional +## @callback: Callable (optional) - Optional callback function, receiving a [GetItemsResponse]. +func get_items(request_data: Catalog_GetItemsRequest = Catalog_GetItemsRequest.new(), callback: Callable = func(): pass): + _post_with_entity_auth(request_data, "/Catalog/GetItems", callback) diff --git a/addons/godot-playfab/PlayFabCatalog.gd.uid b/addons/godot-playfab/PlayFabCatalog.gd.uid new file mode 100644 index 00000000..60017b32 --- /dev/null +++ b/addons/godot-playfab/PlayFabCatalog.gd.uid @@ -0,0 +1 @@ +uid://3g8wadxdycsq diff --git a/addons/godot-playfab/PlayFabManager.gd b/addons/godot-playfab/PlayFabManager.gd index 149c3a90..c66c80a5 100644 --- a/addons/godot-playfab/PlayFabManager.gd +++ b/addons/godot-playfab/PlayFabManager.gd @@ -22,6 +22,10 @@ var client : PlayFabClient = PlayFabClient.new() # see https://docs.microsoft.com/en-us/rest/api/playfab/events/?view=playfab-rest var event: PlayFabEvent = PlayFabEvent.new() +## Represents the PlayFab `Catalog` (Economy V2) API +## @tutorial: https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog?view=playfab-rest +var catalog: PlayFabCatalog = PlayFabCatalog.new() + # Retrieves the `title_id` from `ProjectSettings` func _init(): @@ -39,6 +43,7 @@ func _ready(): set_process_mode(manager_process_mode) add_child(client) add_child(event) + add_child(catalog) client_config = _client_config_loader.load(title_id) From ba427adb4ba107076eb31995f090a91931f70aa9 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 23:50:53 +0200 Subject: [PATCH 07/84] Fixed naming of GetItemsRequest --- .../Models/{Catalog_GetItemsRequest.gd => GetItemsRequest.gd} | 2 +- .../{Catalog_GetItemsRequest.gd.uid => GetItemsRequest.gd.uid} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename addons/godot-playfab/Models/{Catalog_GetItemsRequest.gd => GetItemsRequest.gd} (95%) rename addons/godot-playfab/Models/{Catalog_GetItemsRequest.gd.uid => GetItemsRequest.gd.uid} (100%) diff --git a/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd b/addons/godot-playfab/Models/GetItemsRequest.gd similarity index 95% rename from addons/godot-playfab/Models/Catalog_GetItemsRequest.gd rename to addons/godot-playfab/Models/GetItemsRequest.gd index 1c34ea6c..42f0f723 100644 --- a/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd +++ b/addons/godot-playfab/Models/GetItemsRequest.gd @@ -1,7 +1,7 @@ extends JsonSerializable ## Request to get items from the catalog. ## @tutorial(PlayFab REST API Docs):https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog/get-items?view=playfab-rest -class_name Catalog_GetItemsRequest +class_name GetItemsRequest ## List of item alternate IDs. var AlternateIds: Array[CatalogAlternateId] diff --git a/addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid b/addons/godot-playfab/Models/GetItemsRequest.gd.uid similarity index 100% rename from addons/godot-playfab/Models/Catalog_GetItemsRequest.gd.uid rename to addons/godot-playfab/Models/GetItemsRequest.gd.uid From 24066c6c61842d57bd3aa6f8cfcd33f2afd7cbed Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 20 Sep 2025 23:51:29 +0200 Subject: [PATCH 08/84] Made some improvements to the docs->model parser --- .../godot-playfab/Scenes/PlayFabMainScreen.gd | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd index 6b01a147..04e6a6ef 100644 --- a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd +++ b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd @@ -7,31 +7,31 @@ func _ready(): # Needed, so can ater refresh the "FileSystem" panel of the Editor editor_resource_filesystem_cached = EditorPlugin.new().get_editor_interface().get_resource_filesystem() -func _on_SaveModel_pressed(): +func _on_SaveModel_pressed() -> void: if !guard_class_name_set(): return - var file_dialog = $FileDialog + var file_dialog: FileDialog = $FileDialog file_dialog.current_file = $VBoxContainer/ClassNameContainer/LineEdit.text + ".gd" file_dialog.show() file_dialog.connect("file_selected",Callable(self,"_on_file_selected").bind(),CONNECT_ONE_SHOT) -func _on_save_direct_pressed(): +func _on_save_direct_pressed() -> void: if !guard_class_name_set(): return - var file_name = $VBoxContainer/ClassNameContainer/LineEdit.text + ".gd" - var file_path = "res://addons/godot-playfab/Models/" + file_name + var file_name: String = $VBoxContainer/ClassNameContainer/LineEdit.text + ".gd" + var file_path: String = "res://addons/godot-playfab/Models/" + file_name _on_file_selected(file_path) func _on_file_selected(file_path: String): - var model = to_model($VBoxContainer/ClassNameContainer/LineEdit.text, $VBoxContainer/Input.text) - var file = FileAccess.open(file_path, FileAccess.WRITE) + var model: String = to_model($VBoxContainer/ClassNameContainer/LineEdit.text, $VBoxContainer/Input.text) + var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE) file.store_string(model) # Refresh the "FileSystem" panel @@ -50,17 +50,17 @@ func guard_class_name_set() -> bool: func to_model(object_name: String, input: String) -> String: - var lines = input.split("\n", true) + var lines: PackedStringArray = input.split("\n", true) lines.push_back("") # Hack: add an empty line at the bottom so below logic works & is simpler :-) Otherwise, the last prop would not be written - var props = [] - var current_prop = "" - var prop_line = 0 + var props: Array[Variant] = [] + var current_prop: String = "" + var prop_line: int = 0 print("new prop") for line in lines: print(line) - var str_line = (line as String).strip_edges() + var str_line: String = (line as String).strip_edges() if not str_line.is_empty(): match prop_line: @@ -80,7 +80,7 @@ func to_model(object_name: String, input: String) -> String: props.append(current_prop) prop_line = 0 - var model = "extends JsonSerializable\nclass_name " + object_name + "\n\n" + var model: String = "extends JsonSerializable\nclass_name " + object_name + "\n\n" for prop in props: model += prop + "\n\n" @@ -106,8 +106,14 @@ func fix_type(type: String) -> String: return "String" "boolean": return "bool" + "number": + return "float" + "object": + return "Dictionary[String, Variant]" _: - if type.ends_with("]"): - return "Array" + if type.ends_with("[]"): + # Example: CatalogItem[] --> Array[CatalogItem] + var inner_type: String = type.substr(0, type.length() - 2) + return "Array[%s]" % inner_type return type From 6b2d3152dd9722b0bffa4b6f3442c7adbaefdb73 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 21 Sep 2025 02:05:52 +0200 Subject: [PATCH 09/84] Fix model generation from docs --- .../godot-playfab/Scenes/PlayFabMainScreen.gd | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd index 04e6a6ef..7db48616 100644 --- a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd +++ b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd @@ -51,38 +51,41 @@ func guard_class_name_set() -> bool: func to_model(object_name: String, input: String) -> String: var lines: PackedStringArray = input.split("\n", true) + lines = remove_empty_lines(lines) lines.push_back("") # Hack: add an empty line at the bottom so below logic works & is simpler :-) Otherwise, the last prop would not be written var props: Array[Variant] = [] - var current_prop: String = "" var prop_line: int = 0 - print("new prop") + var current_prop: Dictionary[String, String] = {} for line in lines: print(line) var str_line: String = (line as String).strip_edges() - if not str_line.is_empty(): - match prop_line: - 0: # Variable name - current_prop = "var " + str_line - 1: # Type - str_line = fix_type(str_line) - if not str_line.is_empty() and not str_line.begins_with("#"): - current_prop += ": %s" % str_line - else: - current_prop = str_line - 2: # Comment - current_prop = "## %s\n%s" % [str_line, current_prop] - - prop_line += 1 - else: + match prop_line: + 0: # Variable name + current_prop["name"] = "var " + str_line + 1: # Type + str_line = fix_type(str_line) + if not str_line.is_empty() and not str_line.begins_with("#"): + current_prop["type"] = ": %s" % str_line + else: + print ("No type specified for property %s, defaulting to Variant" % current_prop.get("name", "UNKNOWN")) + current_prop["type"] = "Variant" + 2: # Comment + current_prop["comment"] = "## %s" % [str_line] + + prop_line += 1 + if prop_line > 2: props.append(current_prop) + current_prop = {} prop_line = 0 var model: String = "extends JsonSerializable\nclass_name " + object_name + "\n\n" for prop in props: - model += prop + "\n\n" + model += prop["comment"] + "\n" + model += prop["name"] + prop["type"] + "\n" + model += "\n\n" # TODO: Find a way to generate the mapping for props automatically! model += """ @@ -117,3 +120,10 @@ func fix_type(type: String) -> String: return "Array[%s]" % inner_type return type + +func remove_empty_lines(lines: PackedStringArray) -> PackedStringArray: + var result: PackedStringArray = [] + for line in lines: + if not line.strip_edges().is_empty(): + result.append(line) + return result From aa320b87e9446b6d6d34ed38e92eefd7c0ec9750 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 21 Sep 2025 02:17:06 +0200 Subject: [PATCH 10/84] cleanup model generation --- .../godot-playfab/Scenes/PlayFabMainScreen.gd | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd index 7db48616..7e227cbe 100644 --- a/addons/godot-playfab/Scenes/PlayFabMainScreen.gd +++ b/addons/godot-playfab/Scenes/PlayFabMainScreen.gd @@ -1,6 +1,11 @@ @tool extends Control +class DocProperty: + var name: String + var type: String + var comment: String + var editor_resource_filesystem_cached func _ready(): @@ -54,37 +59,35 @@ func to_model(object_name: String, input: String) -> String: lines = remove_empty_lines(lines) lines.push_back("") # Hack: add an empty line at the bottom so below logic works & is simpler :-) Otherwise, the last prop would not be written - var props: Array[Variant] = [] + var props: Array[DocProperty] = [] var prop_line: int = 0 - var current_prop: Dictionary[String, String] = {} + var current_prop: DocProperty = DocProperty.new() for line in lines: - print(line) - var str_line: String = (line as String).strip_edges() match prop_line: 0: # Variable name - current_prop["name"] = "var " + str_line + current_prop.name = str_line 1: # Type str_line = fix_type(str_line) if not str_line.is_empty() and not str_line.begins_with("#"): - current_prop["type"] = ": %s" % str_line + current_prop.type = str_line else: - print ("No type specified for property %s, defaulting to Variant" % current_prop.get("name", "UNKNOWN")) - current_prop["type"] = "Variant" + push_warning("No type specified for property %s, defaulting to Variant" % current_prop.name) + current_prop.type = "Variant" 2: # Comment - current_prop["comment"] = "## %s" % [str_line] + current_prop.comment = str_line prop_line += 1 if prop_line > 2: props.append(current_prop) - current_prop = {} + current_prop = DocProperty.new() prop_line = 0 var model: String = "extends JsonSerializable\nclass_name " + object_name + "\n\n" - for prop in props: - model += prop["comment"] + "\n" - model += prop["name"] + prop["type"] + "\n" + for prop: DocProperty in props: + model += "## %s \n" % [prop.comment] + model += "var %s: %s \n" % [prop.name, prop.type] model += "\n\n" # TODO: Find a way to generate the mapping for props automatically! From 9424c14160ff2d1e091a5641c71f89c664f2d93b Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 21 Sep 2025 02:17:32 +0200 Subject: [PATCH 11/84] Clean up PlayFabCatalog.gd --- addons/godot-playfab/PlayFabCatalog.gd | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index f383d56e..f0f2809a 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -7,9 +7,7 @@ class_name PlayFabCatalog ## Retrieves items from the public catalog. Up to 50 items can be returned at once. ## GetItems does not work off a cache of the Catalog and should be used when trying to get recent item updates. ## However, please note that item references data is cached and may take a few moments for changes to propagate. -## @tutorial: https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items -## @Visibility: Public -## @param request_data: Catalog_GetItemsRequest - Request object, optional -## @callback: Callable (optional) - Optional callback function, receiving a [GetItemsResponse]. -func get_items(request_data: Catalog_GetItemsRequest = Catalog_GetItemsRequest.new(), callback: Callable = func(): pass): +## Callback receives a [GetItemsResponse] +## @tutorial(Request Documentation): https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items +func get_items(request_data: GetItemsRequest = GetItemsRequest.new(), callback: Callable = func(): pass): _post_with_entity_auth(request_data, "/Catalog/GetItems", callback) From 847b362ee22c41283e641a34bc0bf24c9ac0b95d Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 21 Sep 2025 02:17:50 +0200 Subject: [PATCH 12/84] Added searchItems request to catalog --- .../Models/SearchItemsRequest.gd | 47 +++++++++++++++++++ .../Models/SearchItemsRequest.gd.uid | 1 + .../Models/SearchItemsResponse.gd | 20 ++++++++ .../Models/SearchItemsResponse.gd.uid | 1 + addons/godot-playfab/Models/StoreReference.gd | 22 +++++++++ .../Models/StoreReference.gd.uid | 1 + addons/godot-playfab/PlayFabCatalog.gd | 12 +++++ 7 files changed, 104 insertions(+) create mode 100644 addons/godot-playfab/Models/SearchItemsRequest.gd create mode 100644 addons/godot-playfab/Models/SearchItemsRequest.gd.uid create mode 100644 addons/godot-playfab/Models/SearchItemsResponse.gd create mode 100644 addons/godot-playfab/Models/SearchItemsResponse.gd.uid create mode 100644 addons/godot-playfab/Models/StoreReference.gd create mode 100644 addons/godot-playfab/Models/StoreReference.gd.uid diff --git a/addons/godot-playfab/Models/SearchItemsRequest.gd b/addons/godot-playfab/Models/SearchItemsRequest.gd new file mode 100644 index 00000000..68bb20ce --- /dev/null +++ b/addons/godot-playfab/Models/SearchItemsRequest.gd @@ -0,0 +1,47 @@ +extends JsonSerializable +class_name SearchItemsRequest + +## Number of items to retrieve. This value is optional. Maximum page size is 50. Default value is 10. +var Count: int + +## An opaque token used to retrieve the next page of items, if any are available. +var ContinuationToken: String = "" + +## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). +var CustomTags: Dictionary[String, Variant] = {} + +## The entity to perform this action on. +var Entity: EntityKey + +## An OData filter used to refine the search query (For example: "type eq 'ugc'"). More info about Filter Complexity limits can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/search#limits +var Filter: String = "" + +## The locale to be returned in the result. +var Language: String = "" + +## An OData orderBy used to order the results of the search query. For example: "rating/average asc" +var OrderBy: String = "" + +## The text to search for. +var Search: String = "" + +## An OData select query option used to augment the search results. If not defined, the default search result metadata will be returned. +var Select: String = "" + +## The store to restrict the search request to. +var Store: StoreReference + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + "Entity": + return "EntityKey" + "Store": + return "StoreReference" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) diff --git a/addons/godot-playfab/Models/SearchItemsRequest.gd.uid b/addons/godot-playfab/Models/SearchItemsRequest.gd.uid new file mode 100644 index 00000000..60d15ebd --- /dev/null +++ b/addons/godot-playfab/Models/SearchItemsRequest.gd.uid @@ -0,0 +1 @@ +uid://c40xsh48tsvg3 diff --git a/addons/godot-playfab/Models/SearchItemsResponse.gd b/addons/godot-playfab/Models/SearchItemsResponse.gd new file mode 100644 index 00000000..606ea7bf --- /dev/null +++ b/addons/godot-playfab/Models/SearchItemsResponse.gd @@ -0,0 +1,20 @@ +extends JsonSerializable +class_name SearchItemsResponse + +## An opaque token used to retrieve the next page of items, if any are available. +var ContinuationToken: String + +## The paginated set of results for the search query. +var Items: Array[CatalogItem] + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/SearchItemsResponse.gd.uid b/addons/godot-playfab/Models/SearchItemsResponse.gd.uid new file mode 100644 index 00000000..8fe48b67 --- /dev/null +++ b/addons/godot-playfab/Models/SearchItemsResponse.gd.uid @@ -0,0 +1 @@ +uid://dbxfgoxaq2ssl diff --git a/addons/godot-playfab/Models/StoreReference.gd b/addons/godot-playfab/Models/StoreReference.gd new file mode 100644 index 00000000..d0da77cb --- /dev/null +++ b/addons/godot-playfab/Models/StoreReference.gd @@ -0,0 +1,22 @@ +extends JsonSerializable +class_name StoreReference + +## An alternate ID of the store. +var AlternateId: CatalogAlternateId + +## The unique ID of the store. +var Id: String + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + "AlternateId": + return "CatalogAlternateId" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/StoreReference.gd.uid b/addons/godot-playfab/Models/StoreReference.gd.uid new file mode 100644 index 00000000..24692bd4 --- /dev/null +++ b/addons/godot-playfab/Models/StoreReference.gd.uid @@ -0,0 +1 @@ +uid://cmrufw3fntl7u diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index f0f2809a..c780cb95 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -11,3 +11,15 @@ class_name PlayFabCatalog ## @tutorial(Request Documentation): https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items func get_items(request_data: GetItemsRequest = GetItemsRequest.new(), callback: Callable = func(): pass): _post_with_entity_auth(request_data, "/Catalog/GetItems", callback) + +## Executes a search against the public catalog using the provided search parameters +## and returns a set of paginated results. +## SearchItems uses a cache of the catalog with item updates taking up to a few minutes to propagate. +## You should use the GetItem API for when trying to immediately get recent item updates. +## Callback receives a [SearchItemsResponse] of items which can be paginated with a continuation token. +## More information about the Search API can be found here: +## @tutorial(Search API): https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/search +## @tutorial(Request Documentation): https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items +func search_items(request_data: SearchItemsRequest = SearchItemsRequest.new(), callback: Callable = func(): pass): + _post_with_entity_auth(request_data, "/Catalog/SearchItems", callback) + From 7c5faabba6357a88de65e3769ac0737a921de542 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 13:49:13 +0200 Subject: [PATCH 13/84] do not in clude fields with empty strings in the PlayFab request --- Scenes/LoggedIn.gd | 4 ++++ Scenes/LoggedIn.tscn | 5 +++++ addons/godot-playfab/JsonSerializable.gd | 5 ++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Scenes/LoggedIn.gd b/Scenes/LoggedIn.gd index 3a56a560..499682a6 100644 --- a/Scenes/LoggedIn.gd +++ b/Scenes/LoggedIn.gd @@ -37,6 +37,10 @@ func _on_EventsPlayStream_pressed(): SceneManager.goto_scene("res://Scenes/Events.tscn") +func _on_CatalogButton_pressed(): + SceneManager.goto_scene("res://Scenes/Catalog.tscn") + + func _on_RequestBuilder_pressed(): SceneManager.goto_scene("res://Scenes/RequestBuilder.tscn") diff --git a/Scenes/LoggedIn.tscn b/Scenes/LoggedIn.tscn index 9ae5f589..0fdf6c87 100644 --- a/Scenes/LoggedIn.tscn +++ b/Scenes/LoggedIn.tscn @@ -105,6 +105,10 @@ text = "Get Title Data" layout_mode = 2 text = "Events & PlayStream API" +[node name="CatalogButton" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Catalog (Economy V2)" + [node name="RequestBuilder" type="Button" parent="VBoxContainer"] layout_mode = 2 text = "Request Builder" @@ -124,6 +128,7 @@ text = "Logout" [connection signal="pressed" from="VBoxContainer/GetTitleDataButton" to="." method="_on_GetTitleDataButton_pressed"] [connection signal="pressed" from="VBoxContainer/EventsPlayStream" to="." method="_on_EventsPlayStream_pressed"] +[connection signal="pressed" from="VBoxContainer/CatalogButton" to="." method="_on_CatalogButton_pressed"] [connection signal="pressed" from="VBoxContainer/RequestBuilder" to="." method="_on_RequestBuilder_pressed"] [connection signal="pressed" from="VBoxContainer/MainMenuButton" to="." method="_on_MainMenuButton_pressed"] [connection signal="pressed" from="VBoxContainer/LogoutButton" to="." method="_on_LogoutButton_pressed"] diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 049c549e..4ade8f7c 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -46,7 +46,10 @@ func to_dict() -> Dictionary: dict[name] = type_name else: # Get the value of the property - dict[name] = get(name) + var value = get(name) + if type == TYPE_STRING && value == "": + continue + dict[name] = value return dict From 28a677e537c8758f625dfc1f0dab177f4bde6255 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 13:50:16 +0200 Subject: [PATCH 14/84] introduced a global `api_error` handler --- addons/godot-playfab/PlayFab.gd | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/addons/godot-playfab/PlayFab.gd b/addons/godot-playfab/PlayFab.gd index 53719743..0e91b954 100644 --- a/addons/godot-playfab/PlayFab.gd +++ b/addons/godot-playfab/PlayFab.gd @@ -23,7 +23,8 @@ func _init(): func _ready(): super._ready() - connect("logged_in",Callable(self,"_on_logged_in")) + connect("logged_in",_on_logged_in) + connect("api_error",_on_api_error) func _on_logged_in(login_result: LoginResult): @@ -192,3 +193,16 @@ func _add_auth_headers(additional_headers: Dictionary, auth_type) -> bool: push_error("auth_type \"" + auth_type + "\" is invalid") return true + + +func _on_api_error(api_error_wrapper: ApiErrorWrapper): + var text: String = "%s\n\n" % api_error_wrapper.errorMessage + var error_details = api_error_wrapper.errorDetails + + if error_details: + for key in error_details.keys(): + text += key + for element in error_details[key]: + text += "%s\n" % element + + push_error(text) From ca39f72a9bab5e1829a0b1a434f34526f06ea8a1 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 13:50:56 +0200 Subject: [PATCH 15/84] cleanup --- addons/godot-playfab/PlayFab.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-playfab/PlayFab.gd b/addons/godot-playfab/PlayFab.gd index 0e91b954..3be0b47b 100644 --- a/addons/godot-playfab/PlayFab.gd +++ b/addons/godot-playfab/PlayFab.gd @@ -82,14 +82,14 @@ func login_with_custom_id(custom_id: String, create_user: bool, info_request_par func login_with_steam(steam_auth_ticket: String, is_auth_ticket_for_api: bool, create_account: bool, info_request_parameters: GetPlayerCombinedInfoRequestParams) -> void: PlayFabManager.client_config.login_type = PlayFabClientConfig.LoginType.LOGIN_STEAM PlayFabManager.client_config.login_id = steam_auth_ticket - + var request_params = LoginWithSteamRequest.new() request_params.TitleId = _title_id request_params.CreateAccount = create_account request_params.InfoRequestParameters = info_request_parameters request_params.SteamTicket = steam_auth_ticket request_params.TicketIsServiceSpecific = is_auth_ticket_for_api - + var result = _post(request_params, "/Client/LoginWithSteam", _on_login) # Anonymous login with a GUID as username From b42c583d2706f2b6fc5ec29b6349c3e7ba313a46 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 14:46:35 +0200 Subject: [PATCH 16/84] cleanup --- addons/godot-playfab/JsonSerializable.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 4ade8f7c..3bb5e650 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -23,7 +23,7 @@ func to_dict() -> Dictionary: var name = prop["name"] # The name of the property on the object. Will be used to access its's value var type = prop["type"] # The godot built-in type (Array, Object etc) var usage = prop["usage"] # is a combination of PropertyUsageFlags. - + # If it's not PROPERTY_USAGE_SCRIPT_VARIABLE, it's not an actual property and we can ignore it if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE) != PROPERTY_USAGE_SCRIPT_VARIABLE: continue From a36a8b9c4f5d80d3fac7b9a9eebe131225c1ce1d Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 14:47:02 +0200 Subject: [PATCH 17/84] WIP: Rework JsonSerializable.gd to support typed arrays. --- addons/godot-playfab/JsonSerializable.gd | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 3bb5e650..1d27d40c 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -69,7 +69,21 @@ func from_dict(data: Dictionary, instance: JsonSerializable): break # If basic data type - just set it - if type != TYPE_OBJECT: + if type == TYPE_ARRAY: + var field = instance.get(key) + var is_typed = field.is_typed() + if is_typed: + var script = field.get_typed_script() + var elements + for i in data[key].size(): + var element = data[key][i] + var nested_instance = script.new() + nested_instance.from_dict(element, nested_instance) + field.append(nested_instance) + + + print(type, is_typed) + elif type != TYPE_OBJECT: instance.set(key, data[key]) elif data[key] == null: instance.set(key, null) From cafe237d19ba6a00f63bd17278df62d25a35b797 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:26:54 +0200 Subject: [PATCH 18/84] Added type hints to CatalogItem.gd --- addons/godot-playfab/Models/CatalogItem.gd | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/addons/godot-playfab/Models/CatalogItem.gd b/addons/godot-playfab/Models/CatalogItem.gd index 05956098..d9e530ef 100644 --- a/addons/godot-playfab/Models/CatalogItem.gd +++ b/addons/godot-playfab/Models/CatalogItem.gd @@ -88,8 +88,18 @@ var Type: String func _get_type_for_property(property_name: String) -> String: match property_name: -# "": -# return "" + "CreatorEntity": + return "EntityKey" + "Moderation": + return "ModerationState" + "PriceOptions": + return "CatalogPriceOptions" + "Rating": + return "Rating" + "RealMoneyPriceDetails": + return "RealMoneyPriceDetails" + "StoreDetails": + return "StoreDetails" _: pass From c9661cf158b952115827a82a08631400ac9363d7 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:27:18 +0200 Subject: [PATCH 19/84] cleanup --- addons/godot-playfab/Models/GetItemsResponse.gd | 1 - addons/godot-playfab/Models/PlayFabImage.gd | 1 - addons/godot-playfab/Models/SearchItemsResponse.gd | 1 - 3 files changed, 3 deletions(-) diff --git a/addons/godot-playfab/Models/GetItemsResponse.gd b/addons/godot-playfab/Models/GetItemsResponse.gd index f01e0e45..f085acfb 100644 --- a/addons/godot-playfab/Models/GetItemsResponse.gd +++ b/addons/godot-playfab/Models/GetItemsResponse.gd @@ -16,4 +16,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - diff --git a/addons/godot-playfab/Models/PlayFabImage.gd b/addons/godot-playfab/Models/PlayFabImage.gd index ec0bbbbc..5816980a 100644 --- a/addons/godot-playfab/Models/PlayFabImage.gd +++ b/addons/godot-playfab/Models/PlayFabImage.gd @@ -23,4 +23,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - diff --git a/addons/godot-playfab/Models/SearchItemsResponse.gd b/addons/godot-playfab/Models/SearchItemsResponse.gd index 606ea7bf..2a70967f 100644 --- a/addons/godot-playfab/Models/SearchItemsResponse.gd +++ b/addons/godot-playfab/Models/SearchItemsResponse.gd @@ -17,4 +17,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - From a27634f565ce9d814922b90539ff7bd71ead1716 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:30:09 +0200 Subject: [PATCH 20/84] Implemented Typed Array deserialization in JsonSerializable.gd --- addons/godot-playfab/JsonSerializable.gd | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 1d27d40c..9e137a45 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -75,6 +75,15 @@ func from_dict(data: Dictionary, instance: JsonSerializable): if is_typed: var script = field.get_typed_script() var elements + if script == null: + # Builtin type, just set it and continue with the next element + for i in data[key].size(): + var element = data[key][i] + field.append(key, element) + continue + + var script_name: StringName = (script as Script).get_global_name() + for i in data[key].size(): var element = data[key][i] var nested_instance = script.new() From 427db86d6436362c593deb490f94e3562feccf48 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:45:21 +0200 Subject: [PATCH 21/84] added todo for typed dictionaries --- addons/godot-playfab/JsonSerializable.gd | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 9e137a45..47f0f3e2 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -89,9 +89,8 @@ func from_dict(data: Dictionary, instance: JsonSerializable): var nested_instance = script.new() nested_instance.from_dict(element, nested_instance) field.append(nested_instance) - - - print(type, is_typed) +# elif type == TYPE_DICTIONARY: +# # TODO: implementation for Typed Dictionaries elif type != TYPE_OBJECT: instance.set(key, data[key]) elif data[key] == null: From c48701932450fc2c2fd861f302d3a906e0a7c0a2 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:45:49 +0200 Subject: [PATCH 22/84] Fixed model to not use typed dictionaries --- addons/godot-playfab/Models/CatalogItem.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/godot-playfab/Models/CatalogItem.gd b/addons/godot-playfab/Models/CatalogItem.gd index d9e530ef..17b141bb 100644 --- a/addons/godot-playfab/Models/CatalogItem.gd +++ b/addons/godot-playfab/Models/CatalogItem.gd @@ -23,7 +23,7 @@ var DeepLinks: Array[DeepLink] var DefaultStackId: String ## A dictionary of localized descriptions. Key is language code and localized string is the value. The NEUTRAL locale is required. Descriptions have a 10000 character limit per country code. -var Description: Dictionary[String, String] +var Description: Dictionary ## Game specific properties for display purposes. This is an arbitrary JSON blob. The Display Properties field has a 10000 byte limit per item. var DisplayProperties: Dictionary @@ -50,7 +50,7 @@ var IsHidden: bool var ItemReferences: Array[CatalogItemReference] ## A dictionary of localized keywords. Key is language code and localized list of keywords is the value. Keywords have a 50 character limit per keyword and up to 32 keywords can be added per country code. -var Keywords: Dictionary[String, String] +var Keywords: Dictionary ## The date and time this item was last updated. var LastModifiedDate: String @@ -80,7 +80,7 @@ var StoreDetails: StoreDetails var Tags: Array[String] ## A dictionary of localized titles. Key is language code and localized string is the value. The NEUTRAL locale is required. Titles have a 512 character limit per country code. -var Title: Dictionary[String, String] +var Title: Dictionary ## The high-level type of the item. The following item types are supported: bundle, catalogItem, currency, store, ugc, subscription. var Type: String From c54890c608c7d8571e427408727091d2958548bc Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:53:29 +0200 Subject: [PATCH 23/84] removed typings for dictionaries in models, cleaned up --- addons/godot-playfab/Models/GetItemsRequest.gd | 2 +- addons/godot-playfab/Models/SearchItemsRequest.gd | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/godot-playfab/Models/GetItemsRequest.gd b/addons/godot-playfab/Models/GetItemsRequest.gd index 42f0f723..4684802d 100644 --- a/addons/godot-playfab/Models/GetItemsRequest.gd +++ b/addons/godot-playfab/Models/GetItemsRequest.gd @@ -7,7 +7,7 @@ class_name GetItemsRequest var AlternateIds: Array[CatalogAlternateId] ## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). -var CustomTags: Dictionary[String, String] +var CustomTags: Dictionary ## The entity to perform this action on. var Entity: EntityKey diff --git a/addons/godot-playfab/Models/SearchItemsRequest.gd b/addons/godot-playfab/Models/SearchItemsRequest.gd index 68bb20ce..d56bfc56 100644 --- a/addons/godot-playfab/Models/SearchItemsRequest.gd +++ b/addons/godot-playfab/Models/SearchItemsRequest.gd @@ -5,28 +5,28 @@ class_name SearchItemsRequest var Count: int ## An opaque token used to retrieve the next page of items, if any are available. -var ContinuationToken: String = "" +var ContinuationToken: String ## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). -var CustomTags: Dictionary[String, Variant] = {} +var CustomTags: Dictionary ## The entity to perform this action on. var Entity: EntityKey ## An OData filter used to refine the search query (For example: "type eq 'ugc'"). More info about Filter Complexity limits can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/search#limits -var Filter: String = "" +var Filter: String ## The locale to be returned in the result. -var Language: String = "" +var Language: String ## An OData orderBy used to order the results of the search query. For example: "rating/average asc" -var OrderBy: String = "" +var OrderBy: String ## The text to search for. -var Search: String = "" +var Search: String ## An OData select query option used to augment the search results. If not defined, the default search result metadata will be returned. -var Select: String = "" +var Select: String ## The store to restrict the search request to. var Store: StoreReference From 3cd1477cf96ca3ff32901a23520afbdd619e0ed8 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 17:53:57 +0200 Subject: [PATCH 24/84] cleanup --- addons/godot-playfab/JsonSerializable.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 47f0f3e2..1e3f80f7 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -72,9 +72,9 @@ func from_dict(data: Dictionary, instance: JsonSerializable): if type == TYPE_ARRAY: var field = instance.get(key) var is_typed = field.is_typed() + if is_typed: var script = field.get_typed_script() - var elements if script == null: # Builtin type, just set it and continue with the next element for i in data[key].size(): From ab598c6a06faba1b438ae43086278fd6aa001075 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 18:08:39 +0200 Subject: [PATCH 25/84] Add type hints --- addons/godot-playfab/JsonSerializable.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 1e3f80f7..5870c9fe 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -15,8 +15,8 @@ func _get_type_for_property(property_name: String): # @returns Dictionary - A Dictionary representation of this object instance func to_dict() -> Dictionary: - var dict = {} - var props = get_property_list() + var dict := {} + var props: Array[Dictionary] = get_property_list() # Skipping the first 3 items because they are metadata we do not need for prop in props: @@ -59,7 +59,7 @@ func to_dict() -> Dictionary: # @returns void func from_dict(data: Dictionary, instance: JsonSerializable): - var props = instance.get_property_list() + var props: Array[Dictionary] = instance.get_property_list() for key in data.keys(): var type From f4f8fc8c326ab0284f2aa42b8c2485385f527fab Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 18:30:21 +0200 Subject: [PATCH 26/84] Add search_items capability to PlayFabCatalog.gd --- addons/godot-playfab/PlayFabCatalog.gd | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index c780cb95..0855932b 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -3,6 +3,57 @@ extends PlayFab class_name PlayFabCatalog +signal search_complete + +var _catalog: Dictionary[String, Dictionary] = {} # item_id -> ShopItem +var _last_catalog_fetch_time: float = 0 +var _fetching_catalog := false +var _has_full_catalog := false + +const PAGE_SIZE := 50 +# Search for all items using PlayFabManager.catalog.search_items() with pagination +var search_results : Dictionary[String, Variant] = {} +var continuation_token := "" + + +func _search_all_items(): + search_results.clear() + _search_page("") + +func _search_page(token: String) -> void: + var request_data: SearchItemsRequest = SearchItemsRequest.new() + request_data.Search = "" +# request_data.Filter "tags/any(t:t eq 'desert') and contentType eq 'gameitem'" + request_data.OrderBy = "CreationDate asc" + request_data.ContinuationToken = token + request_data.Count = PAGE_SIZE +# request_data.Language = _locale + + if token != "": + request_data.ContinuationToken = token + + search_items(request_data, _on_search_page_ok) + +func _on_search_page_ok(result: Dictionary) -> void: + Loggie.debug("Hello").color(Color.CYAN) + var res = SearchItemsResponse.new() + res.from_dict(result.data, res) + for item: CatalogItem in res.Items: + search_results[item.Id] = item + + var next_token: String = res.ContinuationToken + if next_token != null and next_token != "": + _search_page(next_token) + else: + emit_signal("search_complete") + _on_search_complete() + +func _on_search_complete() -> void: + print("All item IDs:", search_results) + Loggie.debug(search_results) + + + ## Retrieves items from the public catalog. Up to 50 items can be returned at once. ## GetItems does not work off a cache of the Catalog and should be used when trying to get recent item updates. From 9f8c849d3ee3b492ab92c1caa570f0f2b93e8864 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 23:53:20 +0200 Subject: [PATCH 27/84] remove loggie statement --- addons/godot-playfab/PlayFabCatalog.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 0855932b..236a7663 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -35,7 +35,6 @@ func _search_page(token: String) -> void: search_items(request_data, _on_search_page_ok) func _on_search_page_ok(result: Dictionary) -> void: - Loggie.debug("Hello").color(Color.CYAN) var res = SearchItemsResponse.new() res.from_dict(result.data, res) for item: CatalogItem in res.Items: From f69be71615db597f2c8636ef95d114572465a13a Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 23:53:43 +0200 Subject: [PATCH 28/84] refine search_results type to use CatalogItem for improved clarity --- addons/godot-playfab/PlayFabCatalog.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 236a7663..3f38516c 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -12,7 +12,7 @@ var _has_full_catalog := false const PAGE_SIZE := 50 # Search for all items using PlayFabManager.catalog.search_items() with pagination -var search_results : Dictionary[String, Variant] = {} +var search_results : Dictionary[String, CatalogItem] = {} var continuation_token := "" From f2af1e22b994ed66cc7ea453fdd04af3336e6e40 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 27 Sep 2025 23:54:22 +0200 Subject: [PATCH 29/84] cleanup cleaned up unnecessary method, changed emit call to be safely typed --- addons/godot-playfab/Models/CatalogPrice.gd | 1 - addons/godot-playfab/PlayFabCatalog.gd | 11 +++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/addons/godot-playfab/Models/CatalogPrice.gd b/addons/godot-playfab/Models/CatalogPrice.gd index 23217a4a..b02376f4 100644 --- a/addons/godot-playfab/Models/CatalogPrice.gd +++ b/addons/godot-playfab/Models/CatalogPrice.gd @@ -20,4 +20,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 3f38516c..4d4be889 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -5,12 +5,13 @@ class_name PlayFabCatalog signal search_complete +const PAGE_SIZE := 50 + var _catalog: Dictionary[String, Dictionary] = {} # item_id -> ShopItem var _last_catalog_fetch_time: float = 0 var _fetching_catalog := false var _has_full_catalog := false -const PAGE_SIZE := 50 # Search for all items using PlayFabManager.catalog.search_items() with pagination var search_results : Dictionary[String, CatalogItem] = {} var continuation_token := "" @@ -44,12 +45,7 @@ func _on_search_page_ok(result: Dictionary) -> void: if next_token != null and next_token != "": _search_page(next_token) else: - emit_signal("search_complete") - _on_search_complete() - -func _on_search_complete() -> void: - print("All item IDs:", search_results) - Loggie.debug(search_results) + search_complete.emit() @@ -72,4 +68,3 @@ func get_items(request_data: GetItemsRequest = GetItemsRequest.new(), callback: ## @tutorial(Request Documentation): https://docs.microsoft.com/gaming/playfab/features/economy/catalog/get-items func search_items(request_data: SearchItemsRequest = SearchItemsRequest.new(), callback: Callable = func(): pass): _post_with_entity_auth(request_data, "/Catalog/SearchItems", callback) - From 8bdcd0360015f8071753121316b2f0da2f51295c Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 28 Sep 2025 01:57:48 +0200 Subject: [PATCH 30/84] Introducing PlayFab.LANG_NEUTRAL constant --- addons/godot-playfab/PlayFab.gd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addons/godot-playfab/PlayFab.gd b/addons/godot-playfab/PlayFab.gd index 3be0b47b..36f43709 100644 --- a/addons/godot-playfab/PlayFab.gd +++ b/addons/godot-playfab/PlayFab.gd @@ -12,6 +12,8 @@ signal logged_in(login_result) enum AUTH_TYPE {SESSION_TICKET, ENTITY_TOKEN} +const LANG_NEUTRAL := "NEUTRAL" + func _init(): From d4931a94815c6073c9eeb2bffcd70df41776a072 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 28 Sep 2025 02:00:38 +0200 Subject: [PATCH 31/84] On search complete, emit signal --- addons/godot-playfab/PlayFabCatalog.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 4d4be889..1f00b58a 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -45,7 +45,7 @@ func _on_search_page_ok(result: Dictionary) -> void: if next_token != null and next_token != "": _search_page(next_token) else: - search_complete.emit() + search_complete.emit(search_results) From 61367cac16787ade9ad080a8d1dc88748c838de1 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 28 Sep 2025 02:01:16 +0200 Subject: [PATCH 32/84] Add Catalog screen and ItemCard widget --- Scenes/Catalog.gd | 22 ++++++++++++ Scenes/Catalog.gd.uid | 1 + Scenes/Catalog.tscn | 66 ++++++++++++++++++++++++++++++++++ Scenes/Widgets/ItemCard.gd | 16 +++++++++ Scenes/Widgets/ItemCard.gd.uid | 1 + Scenes/Widgets/ItemCard.tscn | 43 ++++++++++++++++++++++ 6 files changed, 149 insertions(+) create mode 100644 Scenes/Catalog.gd create mode 100644 Scenes/Catalog.gd.uid create mode 100644 Scenes/Catalog.tscn create mode 100644 Scenes/Widgets/ItemCard.gd create mode 100644 Scenes/Widgets/ItemCard.gd.uid create mode 100644 Scenes/Widgets/ItemCard.tscn diff --git a/Scenes/Catalog.gd b/Scenes/Catalog.gd new file mode 100644 index 00000000..527a7350 --- /dev/null +++ b/Scenes/Catalog.gd @@ -0,0 +1,22 @@ +extends Control + +@onready var card_scene: PackedScene = preload("res://Scenes/Widgets/ItemCard.tscn") + +func _ready() -> void: + %LoadingIndicator.show() + PlayFabManager.catalog.search_complete.connect(_on_search_complete) + PlayFabManager.catalog._search_all_items() + + +func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: + print("All item IDs:", PlayFabManager.catalog.search_results) + for key in result.keys(): + var catalog_item: CatalogItem = result[key] + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %CardGridContainer.add_child(card) + + +func _on_back_button_pressed() -> void: + SceneManager.goto_scene("res://Scenes/LoggedIn.tscn") diff --git a/Scenes/Catalog.gd.uid b/Scenes/Catalog.gd.uid new file mode 100644 index 00000000..d40f4e80 --- /dev/null +++ b/Scenes/Catalog.gd.uid @@ -0,0 +1 @@ +uid://cvn4peu38pqpe diff --git a/Scenes/Catalog.tscn b/Scenes/Catalog.tscn new file mode 100644 index 00000000..711eb8b0 --- /dev/null +++ b/Scenes/Catalog.tscn @@ -0,0 +1,66 @@ +[gd_scene load_steps=3 format=3 uid="uid://cq24viuorfsjc"] + +[ext_resource type="Script" uid="uid://cvn4peu38pqpe" path="res://Scenes/Catalog.gd" id="1_lnqhp"] +[ext_resource type="PackedScene" uid="uid://wwvhjjuojn77" path="res://Scenes/Widgets/LoadingIndicator.tscn" id="2_8nk0b"] + +[node name="Catalog" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_lnqhp") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_bottom = -46.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Header" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Catalog" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 + +[node name="CardGridContainer" type="GridContainer" parent="VBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 100) +layout_mode = 2 +size_flags_vertical = 3 +columns = 4 + +[node name="BackButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -29.0 +grow_horizontal = 2 +grow_vertical = 0 +text = "Back" + +[node name="ProgressCenter" type="CenterContainer" parent="."] +custom_minimum_size = Vector2(100, 100) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="LoadingIndicator" parent="ProgressCenter" instance=ExtResource("2_8nk0b")] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(100, 100) +layout_mode = 2 + +[connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"] diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd new file mode 100644 index 00000000..d6403b9a --- /dev/null +++ b/Scenes/Widgets/ItemCard.gd @@ -0,0 +1,16 @@ +extends Control +class_name ItemCard + + +func reset(catalog_item: CatalogItem) -> void: + %Title.text = catalog_item.Title[PlayFab.LANG_NEUTRAL] + %Description.text = catalog_item.Description[PlayFab.LANG_NEUTRAL] + %Type.text = catalog_item.Type + + for price in catalog_item.PriceOptions.Prices: + var unit_amount: float = price.UnitAmount + var price_amount = price.Amounts[0]["Amount"] + var text: String = "%s pieces: %sG" % [unit_amount, price_amount] + var buy_button = Button.new() + buy_button.text = text + %VirtualPrices.add_child(buy_button) diff --git a/Scenes/Widgets/ItemCard.gd.uid b/Scenes/Widgets/ItemCard.gd.uid new file mode 100644 index 00000000..ddbe9f13 --- /dev/null +++ b/Scenes/Widgets/ItemCard.gd.uid @@ -0,0 +1 @@ +uid://md2a6mqg54jn diff --git a/Scenes/Widgets/ItemCard.tscn b/Scenes/Widgets/ItemCard.tscn new file mode 100644 index 00000000..4496def0 --- /dev/null +++ b/Scenes/Widgets/ItemCard.tscn @@ -0,0 +1,43 @@ +[gd_scene load_steps=2 format=3 uid="uid://cd8qcg4hmyogs"] + +[ext_resource type="Script" uid="uid://md2a6mqg54jn" path="res://Scenes/Widgets/ItemCard.gd" id="1_0mt03"] + +[node name="ItemCard" type="Control"] +custom_minimum_size = Vector2(100, 100) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_0mt03") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Title" type="Label" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Title" + +[node name="Description" type="RichTextLabel" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Description" +fit_content = true + +[node name="Type" type="Label" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Type" + +[node name="VirtualPrices" type="VBoxContainer" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 From e628a251d7e7165d30c8eb20954bafc5532014ff Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 28 Sep 2025 02:01:36 +0200 Subject: [PATCH 33/84] Add GodotFest assets --- Assets/godotfest_swag_shirt.png | Bin 0 -> 19142 bytes Assets/godotfest_swag_shirt.png.import | 40 ++++++++++++++++++++++ raw_assets/Shirt.pdn | Bin 0 -> 32481 bytes raw_assets/godotfest_logo_blue.png | Bin 0 -> 66411 bytes raw_assets/godotfest_logo_blue.png.import | 40 ++++++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 Assets/godotfest_swag_shirt.png create mode 100644 Assets/godotfest_swag_shirt.png.import create mode 100644 raw_assets/Shirt.pdn create mode 100644 raw_assets/godotfest_logo_blue.png create mode 100644 raw_assets/godotfest_logo_blue.png.import diff --git a/Assets/godotfest_swag_shirt.png b/Assets/godotfest_swag_shirt.png new file mode 100644 index 0000000000000000000000000000000000000000..ecec2b0da3047eb28c1ba7f5ea9055d7963a6af9 GIT binary patch literal 19142 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 literal 0 HcmV?d00001 diff --git a/Assets/godotfest_swag_shirt.png.import b/Assets/godotfest_swag_shirt.png.import new file mode 100644 index 00000000..e2e82ff2 --- /dev/null +++ b/Assets/godotfest_swag_shirt.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bpbaxw0vyursa" +path="res://.godot/imported/godotfest_swag_shirt.png-8414ca085264c6db3e10a2f7cd68b762.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Assets/godotfest_swag_shirt.png" +dest_files=["res://.godot/imported/godotfest_swag_shirt.png-8414ca085264c6db3e10a2f7cd68b762.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/raw_assets/Shirt.pdn b/raw_assets/Shirt.pdn new file mode 100644 index 0000000000000000000000000000000000000000..1356ad9a807af2a7a903d6d441d06cb08c5a0537 GIT binary patch literal 32481 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%0D7Me`$`gz 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�lA)1b3zMfN7Hw+>mkk z5uH@e8o*%X1UY>L;$giR^Qj@(ETSOtlW1fX+Uyb;5&@C6C|B@>IW!76zEtj%i%=!tb{377#7N3SsUU_V z2q#mJn$&`*UW)J`ZDzLRsZdgrOL2M2Y&HRiwwa$m05n6RSS2E+-N^{90{7ekA$F<6 zH&zNV8)j6|c|w#0g_cQ^Ng_pBF{1$vj#`iISq^DhE;w(|}{8QbLsB zds0a>O{M`4W7RB%+r&W&qZRX$8a7QK;P0DFrGiU?No|z^U&|z-nL-3pnM(w#BQ1J> zN0kb_Qi1~>A^`2sF%Jfb%#2DNr5#Kd zGS)gsgOElH)R?a12QZpcgi*Yhz?3SZ6Yx#U8V8J&PjI-eU1$W+Me<5O_;Zvi4}J=j zQ^mm0dXXZ(Ad%~%mUP0C$1r|6#3hC#{xG5?ATM6ba@Qw*E&+LjLJ0=|E~GT5 zA;1=z;d+9XO?ljQXWYORAb||ma+GYOs0qo;_E14l*@ zpRckb5W{HsQN9Ah%ub2IXXNg|pmC2A6cd+m^qD}erHnsgjF%y8X^#J6|E(AoQ}R*N zXbZ*RqJbaWTaziLj8kx|NFv3TVljsTfNygt&gJp__N>^bhQlKkU^0NdBqfJe>+#5J z2}(m+;N1JIWl`9ea#zebS`kqilt#M_)LgQR+wnYy?~{5GzyXYDxZ6T|6jIQUQrv8- z*fX>lz^sBbZxYcUq-25)E7_1n0(yk9`gDAw0@Rn%fNKH6&?pPx9GXLPq!QCZ##9{2 z3*;4ZR2d}`3|xUQ#S~yNNE0Bg0SmLZczX!!5t11iCT)_E=I&~ z1q!UVBy45M zq9!I!1IUl%!Gl%svyj$v6wIlFT4?vlg9Y#`i*WD6rjp`f92YQlELcEO(ZJoLJHITDgv4x^#i>tVI;EwgCV&2=);SIAAGH2W z_(f=xUa!-a%jH>V$|wSb7i5zVBdC;wL1;^Ic&Y`G!)6lvFcVxU0R+}Vf=bX)Tuww^ zluP)Kr&Li~Zj;EGSar!3BOMlfsTeLmS4^Q+IQeP|AckD#4cDtG3VomeDM^FMku<7< z7$~+h<)z&!?&eM^TO!1O4bwIei0gm85BMMN2-0k>sU(rq$f<;8%i+* zkjoDd4H)oqQx~0&XUL~Jrwb7(pNfZn<%XoEfcy79=R#O64@o0ZnGkNp5&{J}@ag|f z;sXZ^xqIf||8S26O<8pjc**gz+1)_po_|o5aD?xHBK5C?#R8zy zKv*dIg|g@$ls!6aHk@(5ZCJ0zp!P5pmqZGK+!^KkFQc^i-6#rfl(taB|8*1*3d9>m z0or$_59|4P`WX51w9o!R@V?&>L^*=@L(%W}0fhclezfHdD|eP!{)v0E;~wqdao61s z!1H&u0!$$Bt9JZfvlZMR5Byf4E8}!Jlx?~UropkJszr+~zpc76 zc*FDe1R8YVgV3YpL-6=jOFzsJ;dtD@>_^~B%SYkyYl9B(QpNGVp+QIZ(y|jger?bh zUYc^0|KF{O)631nAQW(Arpe>KU4sYAuJTSr0kKl~bSU-nE4PS-g>>P@6vZhWZYRdENSfh5AcaVO+I zk$jq~=?sqsl0D!{6Ej6p|Ddk3L7T2Cv)uq!bEgHV@?mGRkkmKNA=U%j(PB))>r}W?Td63ujr`A9I z%qd{YAB_4ZlJGl5HTr{5|Ab9E-tQO%O70Jd`X{XV9izZS`h!vb3Zo#n|ASHg3Zua9 z{ew~e3ZuB__SSFRgV9{C&2z812LFxc;ar9PpMGXF`>-=O^!bMH=9e`#3;u=`2Tpqq zYyZ^jhts3(`>=^|ZWH6T_dPlKm;XYhxlQs1)y8$7e4smjJb9r{-SNSH4R`zQ@~qyS zsz>}n@Bi5E)s^AHZw9ZkqaItge*K!gRlN2~HqM?iXJf~X9i3xl4cf7CPW8xJLpL5e zbZBJXzI}&(0RarNaD#Eyl*-8g2d^BycJbu2USn&A4tv)3fnj#l`u@C8<@29yYP$7u zKV#2w6W)-i>&|VNQVt|Hc6?{?>k(%6V-t8h-IQxjF!pil&fQ#FQ{x)2^9^xw)_X@M zO`G<>_PcpI@PiNb9(iP+^tHhk`YV)YyKQ$(Tm3$9dCL|Z(EE1Qn)BMvMosQ<@X(=+ zKi1dRJ8w3%Jh*nt^|~JpA3l8c>xrj2a|0}MA5^@3lDYe$cF11m?KXWMQ60ILH&RlWPvm`^+2b9(W_brWX~ww!OlTek9>=0m@FByudoNZXwn zvGjnje#u*}>Ug8}oxeHwdY{p|S2UUW`6H3KP+-Egjz=DPcg5K5c!;ubN$By1sSQt9@UXWPZ&?$Y2r-z@~<{0zd+VTkBoX~fN*9}|pnE30m^{0K8TkyW= zN~OM9Rla=l!4vx~-*fuO8KxQJjQor_k5Axjsy;IA^j8lSzF0SY$>2tlt6LdAIJ2|1 z@-^16>IeO?8`~z!YkMO%SN2=k^SR~5`o&kh*T3xcb_7cvitrxYvOkjgOgU}JoOB)0ei~ z^zik)pFeVa>YB{L;XTmj9$ZnKnmuygwr-cUH=a9n`61ogFfjL`RPbOKXUMcw^O=Xw<4EbpV)i+B%Sftkz*T{vwWVk((%$# zboNtd_sfRMx0h|+K6-h5&!giHKXssb>B-ltHXWHg=4Nty(%AY$9r;}+@_UiBZ*j@S z{n+v6d4j34o12bKeCFNUo-W@{l=eQ*XZV_DUxy02ar?6mqxetd*WUHLG4R7}56yGk z8g#R6^!|_Au5ZhG;dt5peUHeAx69oxzjg8C#RVN7T|wsEcbW!5`AXfP(@Sr3tXq=& zZsGQ}bxV%S**!8cZtu5CPhMWPLbkr`s7tS%!WPth-0{5r%+WPNt9lF?KK{$=Pg2V? zKrOwae(up#-EOXFcmDIE>#7!CROZnqS_~O_p^xI#BX#?~nsGnVYva2|Q~9;yACnFk zv~hh7b@CT{uWL1G%8Bx(+Y4obD;D-R=`ZdICGWfR^W`N^J|38iTTW(a7?wzYz>R-I` ztG}|lTG_pG=tJvid(El`-V%2o-s{BK_ve*{bR2WxsIAxA_b+?m?Afz*H8nN$C+KVE z>#knCdgI)=T(ePq%fpBFc(`uRpg}*%d5=^om9>?=8#ZpNo^W`2-(kE>TaT6dRrl*R zs6##2g+&3yS(Pt7=1#oPGwLl3Oj zk{z}Fsr7Z9dfwb8a!X!r-)PzF$1bmF)PL#9wxiDO?!y~$v;BQ_%IJi1ydFFI@O~(o z4!$y_@3F=>RQ+%r+ANKCf$Y zpSL#V?R)Eao-WpL?~)KuKDFZcyEhN%&`6>J>W5cP{8l%%?%Nf09r|ySwBa3J`usE7 z>YKh}B^U{4*&90`JNq19vj$>cl>*aCD)O-d#|>d-L2fkN5fx<=i&FqT0vL^>pz@^@w-LrnuK9V72y|=hY{7D1Ug68-c0s zGqTvaN0Z{8XFsy>!)aXK9xr|N=JT6weNtWnjhE?yEl!OAhFAdAx6@wr>Peh|a9?u<|xuwtc?`{`)yt;z}J{Q~_Y*DiSCUk1IV*H%s z-94Tg_+sIGSk0*6s~ZIA%|I@?XVuwvfYP!~!4`|Tajs>$EZ_d<_T}Vu6Dos6&TX@> z$C3PO{vs<3e*Bg8N{%gasbGs$z$adl9?$iFed67K3T^$1XI+a%SMiQdXs_IQ?A}9N zc)T6TkEQ_iO^c_Vijdofez9u7>+p8lyqoTMVE4A!jVI4vIfd*0+Aigl$+pBW6-2H&B)HXl|q(b5X-2a^~zHP&SdwF_KnR? zcVD*oZ+~CDQ~(T}+dTf+q-cj|mvigZt((rRrNfGerSl`Ue+}zl5C6=tZ)Bt^DGW59o0#eLUXE%ui_B?IS)p zxy;!LIJkIb+42#Ijun0gIToYoK)Nn&VKfV)u|^rbN5-0)RDmV#)acE9pO`DX;%m2RyfS zwC@0UsL(wEBMC++smyf2Md3mYvx`W5N(&Iq8y|2}Ndakr+eF}tfQO%cUmEj=^*FY}dhWp+6 zrNhsf?&i82n|P2|Q1fHM5(>1h{Mnl|KR(V4arwzvg@M-xO`Fcm^2Q5iUOvQe?C-9% z$eA}`0Y5xJ`}Ui->D}HjP}rR7vHJLu>65uB-FKi}ftzr4+?kgjJH8BNV`M*lRrdNg zu5u*pdzxET|9Lgxduk43^0Q`H-`-aZ(U*5_O;_cZDI1#e=8jy8%%lDBevNpWrXTQ( zAn#pN8(KL0CFnn7-I+hnd8qj-9IM6>!0`0lPg{YU;05~F>t|lx$IUjPCpVl}@*=lE zJ66-RP^{;QhwmDA?<62G*9O#gLH`RHRzm|5M)fpu4yyg{tX6~vw!{?LJMx0UwJZnJTFx6_?E@A_a@W%w}bx{V_XA8wyh zeR~AlcD_9Q)}w#DcHzYCABP^kdUegk@7wc+=;6<^y;A1`-S9#48>_B#yn61i?&NAd zZri!J;pFs5L^|&*Nnd4YaiR zp>JI4hLN{3Y}3ojVH=;EDC-JD zI&tM9_;NS*B@JH`+?S^CrBClgw{G6Jy5gdXH@EASn-aAhY zA5-jgmg%spM-8Hw)xk^d-SActIp4hhrW=#S-;7_LylwQm+t1v!jU2OQaNVS8-iy#c}a5pPq9r8F2mP$g7PzzqRm$dfngW*@oOs}0v@*9{@t1(>~f&iTKeS~7S2;#-q?-5F>0 z4Po`J?Z=gw)vKzh;nrr8z76d;+h_mnyGAX%vHa52xvkdBIneIH=GN~IsuSMs`L_$Z zzUssq-Q(zla%1*lozMA@d1m)C7J?zPm4z8$?q$klW^zh!pM?gLe0mqnJ<{3WM35Sq2Wd<3>@)A+AD zEgbmyo~7OP4gB~@*VD`X%ydM#9`ozBJe_|a%r@QIHZ;bw{La|3rc}=>b^%f9aN@z@ zo(|q;f1Yym){C{zgE(DW)M4YUITPM<)J@`8_Wq>mc{JDDlH9Un=b00}edPrwu_rbz z0HGSP>im{HHEnB<7gujKuEAeBEBo{5KHK`-BDqN=>Lwt?E(2=bKN0LE_{hQfJN!JW z_E#g`Ycswb`oISt*31;O&)z%#^s+&N2M_(JPoAUMqh9v`dH;_2uZ^pnU4NlNbdzT6 z2b+!C7t$y99bfnw%qTglT6$K&@q*GT%ENVaS#RKg= zsomTpMYPX2JGnM`@>_`40wPA@a| znsDLr`i`5$+&a1)olwis+z}q**;h8-jyB;+=O3Nm{Y~li(4QJgk4@-4>+;)U9rHO# zA1><9Eps3=aM_&=8}~+a^r^vbd z?R9X`#U~#){&Ve@ok(u$Hgz1ZW$X`kmU}93HDY_<)osl`Tw>Tauu2|0ZEnook(T$5 zP1x15JMq%0!e3U+S@Zct-Q07n5&s?j)#Pt|^meQ7)=smWyxm4{sWlS2Q~t`t>Pwt3 zZNmEXIn(6l9dvMl`pKEQ6v{6){Mr6QHAnu&Aknr!;mSoD@}Uu*j;s1yGu+>XJL z&#WwuZCR?OIH~>o>im@rWy>%2CPw#~@CMiL!VO_zBQp1x@bMvc2u`l9UFv`pG=Jsi zLvUom#gnhWOeP%}e;X9f=r1~b1yZ$nV>8|ppy&X`;k{#q{f3{p+(8puZ9M8rPEL=s z;r*Et^#A?_t^eM9$(NS%Kbtdf*BZ_HjXB%9yya$I+a?(E)-Lzjv8wss3=+s2Z=aSu zHT?G5^?lABR`ffwcFE=DqXw5wwdVD{(3~f{StT%!ID* z*FU)Nze^Z~ebGv2{%YyZk_xcSzbL7jZ=>7m`;9+*c*)7iNZys_x18ZEQ~jY#|F9+h zZKibC)>iX+4uVULVEEw@B&7Mb{nq+_e9yD}+cx?5t5FkMjN7sN%6C5=-LiG`TgHde zE#B{E`yqX4c=eVCt9y?>yp|`}eEKYJ2Pdp=Rr6jx(T4Ze3(fn3`RhOZ7Vk+YY5|-0 zDL4e7YmMhZQD@M)e}batPP7>nzSejYC}KP3HvE6CbIr|JCm)zGBe3PeG)%Yqfm`Ee z%wTLId4iqitI23|`^;OsA@h!o-1ha?v!yqyHmSe5{l<(LeUq@Hz>-ez2Y@cE-?NeX ze+vFjeny>J-R6U~^~(Q{jsN2Y$3NMG-(8;i{oJ}<7dz@3hcZpDbpIF52*#i-C!b(4rwhqLt~&3YR$sjL_bVG#ZF_Ia`nrQd zGtXCVTYBl%>Ga+Ie4_t%Yx=Xf4;5z~JTz?4WZ~Ovi~8$FR?O>nlxmpNgBxB;JZF42 z<3A8s@&1bFycJQ3+&ac|?DUC6=x2d?4Y_stGlkx)=BzK6|>hQ2XeaMQe5i z#CroP^yB8w{PE%1VP6GS^xsE6@$a`5 z{;>S`{_W?4h1;hbpZwl45A^HZQM7U7 zd*|n_Yktp4#r88t%BqV$Hc+8oDN(`GsWwA#C9>|sqVlFdeT3Y~9yyymGjf*nI-0zc zt?l#P`0E>O+1fY$wrI_kfVet0c;wPy{C-L0(0k@xK6PzYuVJefemHXPNw6vK+0=Z} z_#ZocTKMXfK`pNR(1F~VBDYQ(_~|h9*6TWQ>$=#WM=$@qpiKR>E_nLPqN|I)zNfIH zlkrBkgJjJh^q}cRBjh^ z0aDXZ@ALbgF4Uj+uI}ufb;yNauW=)fT-!X<`^k3eKd3CM4fCI=zxdvdYd>mN--X;t zFKm$Av;=|5v>vkVGRah`=wPse=`I9g2Ir+}5 z!nxVf+vATz1_u&oq8=kA4<1}8Jvw#IqPvD3y;heqcG`b>(Rba)N8UNtl-$~V&9tRU zdv6V_7;vKB##cY}tz?hfh;2NxYTM}i_N~s#4^CYF;g;m6(eA#7DjW8`ux;kjUdd~3 zNDoc^Xd6y$mCuq6fL+c!B;Gl*|L~EkCe|-Jc4^wlF;iwqo1@8X`PyOM2Ub{&-J~6o z9aMoqKXtjiDNy)e#WC?G)!gKU1Ui3@$S>czzF@^M*4TJI|N3{U-jMcI*WG&U?2K2g zbsPVDcio=kvp@EkGx_3cYd21qbL#k_fQ#Oe{%QYa5X{Nf&uy4GqRrsOSGtp1?@#~4 z8+_lZ29RFs+@N-Wd z8T#$~`i*lW0~OOG$Cy?)~} zv8>(3VQ;T`_VS=F&$T)9$Wi`J<+|9-osJvxul7E)KTzNG8h59SH&r_C=HjJ;nS*UF z9qV^VxFkEF?e>^_<)wEX+&x(MR_#aI_b#A9Z=d{VXJAF*(Ps^xRQLO7(bWmZ_sQRo zvM+5vaQTsUzZ`ngbFFiw@6?y?W!_`=jkC<3`H9$cQFr~`gTvu;)c-WQU-MRX(xhSiUfKN3QvOd<=CRQZ zCmz|j`k9TdtT;Al!L27hdwE^!Yu_*8uagc%lOIkV_R(txzuOzA&mVd792(g0_(L2A z`tN;b-1f_hYSgo<&W`)mvn_pO5$YQ8_T>>{cR%x3y+t0M@afHQeOGPi^z9SZ`(T&q zMo#*6?4cEpoO37VT>ER=SDrdI{OP(yR~-uiBfjnV*2F!$WWNn7RvfF_Hgn6-HG?Ls z@0xq<-SXGnFSLI5WaGhOetP7`S?3-;K9Jn{47qjdwwZOt3uA6SW2JZR`SJepfZ27c zc8^+DcniT(?f38pbx#iMbzwlm2|wQc1G2;EMQdEAeRcAq9_hNfzQ<>ud+y9T?{0rH zfAXAVjp^FK4ej?7TlQEs;6m@)a|V3#V{kxa{Bvt;N3XrNl-c?}+Pm_%rjEAFS6@r3 zRa`!lB7%U3Y+A$=K^7GxEV8SCh@eE45;Y=g2qA8OC=j4*g^(x#A%LKTNCgs9mY~RL zNB|{~HIM)y1jwFzfwxz$%fI34{BW+BU*?%}pP6~)zMu1)Gq!KLnN$!s*hjixH*Ao7 zXlas@=I0U79*NP%7rIBB`@QLElH6{np0V@sEdFrwtHMr zv0NkgVKBupW~}h=MSe>7)5jPdYd!^CWd_&cgFl`+i+q)EGcw+NNtGxEmXmMV zz>331#j!v;AR@LY| zW&=8gm6L&<7_Wt%)&2*E*P@zMo~d!GI6)5umoooCS(A!E)U;fZ#`UHn(-_Q=AxjFta2pD=>A9Bh0qvALK9S7?@e{xzX9 zg#D#jm#l}}*2aTR_*+(8%nPKSvs&agCJ{324`?s%Fx1>HYZlq9RCin9?mNlW({@z+ zX0mMOt`3mJ;AumRoe5t=19AalC9;Eyu^z(vT>=EaTv!bMw;-vT3m4r2Ra+`md*8j zFGjUwNj>&h*eN9_6_pnUy#Vf^X_Fq_swfZdX{;Lcjhqj?UkQ`6 z!I^8WGqMxT5j?EUG~n{r>sXVR3~M{fDcF!EuZX~wo~%k5E@un%eePQZ)WfL?{aG~8 zQ9pSSM2OEItN}VMY4^mq8}OUTR#kcb^#gj@_LmSvL0L(=M?}R5Y`h6HU6LYXDhxw$ z_P!jXhdA)sD4zBymokmi+Y98S@zPGbkM%x6+iWc7aVnoJ3_Ei$&w5+`;1oAK`;mgn z#h8qte#t{kM-gf>CIH*U27e#olk7h9ft#PpvywJYRv9}miqP5Ju{(2%gmX6#BJ~6R zRE1>{8JUSX`sI7hFNNa9j#rET2X~X0V}soeeIm@1^KY`3!S5*Jl%xUTCN@S8lo*id z38DmTMV`1euL19{ka<6+Zx#wFb-K5E+W_g*WpD^U-FHAd={TXYfv{;-mpAyya_XTe z-hb-0=6{8;YC##fi= zYp1^LE0wYAJfXk$Tde0mt30{n2zQ6%3C8!2C}^vc;5Btq#LR`%D$mcSrTWi-hbqCo z&1^sSTKg5L7^MRq?Bl8r?BCjf(3Y}Ve-KaR`xe{meJel~2h>G1uX*rC4WGnNd{+(KYwew8EXc7qy;$uJY#3QhORmGJ8+15wYk| zYeWt6aVVSAA&~DU$8-sE!&5dd$6ZbG1CEc18A$=T&D4k$*Nllp|iFh z9p(=XS;{BLmfw8(S8 zM$1^9-2ehw zX#>j-XeCkKJ)%_g7C}k?vWckIRzl?~Ey|J~BilJcZ1L2Ztc>2F2+xG3?+Fq_^KxhB z%$~ZK9G0Go*o<@G+pFc^?G+k~Bu+Am^srkvQmZu^bIAC=o-R4N3H zB(-RpR3a+SJ6hW;CSv-N(m?HPOZb+$_+o2+cPPiGraT5F*kfMo!J6S=F4D^&dqX%! z_QRi#Z)c}&IH(pJzitcoi4~*2p%3<Fl(_(kX^(CJ zwF$NzulF#PH-E$gwko*yj_{xlZOmL`x<%fID~Ul)>$dNNoJXAt2caxm$%T9iy-_Ys zfAtEhAUs_-Of3DMM0EO%4jeKxcoBKp@c6d5JdK5JU|QH`RuoA$CVtX9s8=TQ3f}H6 z#@7pzxiP}rmhSI(g?nxJrMgm$^h_4majs0rG@$9M*steN+!5TD|3LZWEyq#e^tucRPwz=`~DKqMP#i;TsT?47-Sy(i27w^ z^~hA>YCao{tAa9f<#(gneQy`6IJ8~}K5FEC0n4Rcl{!pE-B_rBd&B1~%*y$s?{^n% zY|39{t_`3geXXv-KAD|Jyoqg|WvUjQn1my>JZT-7S|6wkS`(MfgTCQ z;n+{P^ZYFU1z>J~-tV&Pv42*Lsdh^dL0VkUct5SFZ)@)qLL?X`2eOlTaB;NxgQ&*G z$Ql#AY&iTypaVz}9P7<&r0dvY0R!nR;^wn5B{Nr1`6b}tS~tfE8hB9*2?p}e!Rc4r z=YFpMB%=-I=M(lJMK`qykc@y`IwOQ0vit`?V}FHhc>dSqaJ)S>TvXH9ykKJ6hugIu z;MvfvKKNLl9P2&@6<#}zI+alTK%N?@wnjhrJzs6)WEX#9UZ-pwAj8exXKZPucxJ4o z(hj&2pRy-+@L(M9YerZJyDbiuCNHEODwuoh4a@r3NY?*UE7|bd)OQ*u_VCRT z^-x*bsPUh>-C)aqiuQJYL6*k#M`_wcD|JI<`ANT2CY$YIBAsGwrgYCr#vjQ2{MYEC zL6NsyuD@m3jO|=#ZnH}a)n!%rqvo|P$K5^_>pka;nxAK0jfEi{xsx%UAOrOgy+v=G zEu@Z@`Q;DnM9K9tu>-NHT`fII0*Ia=A4xFdzxgSCnqp|WXbr_5;<}|eLkSsQ zvqgv>)~@ro)8d z?K6F%m^t7}n`PNGJwrA2K9gnyKD3?x6k-6mZ|E?gm9X#@c>7$mJvVv8wXPa0=$E{7 zKd&%GaCh9-m2W&n9Tl0m$3jWacKO3+nhi~=@srxBZaSn}$V%LWUlXrSjX(Ha7)#Uu zrKQq#94(g`40X_T0#)Y^+GSd0TS~Rx|6HsOzrd~Hs~!%5*AQmhOuOjvLFlN*YMh|Y z)?ikxL{%9LS)ZI;6B->a3BnzF)LS&jiYIk%KZ8BBcFm!^3XB2wuvT_2gMtnN>O-At zhbqdRQ9sn?40R4!1@?7CspSg03%~mRjPcPiTcOzSZfY43$%%{9)4id5`LkquJHS2p z5+ut$>vC2vmZA=&?=XJFc4;|J4Ov2XBd(dR zs({`#%Sl?ohxD$Qt*Wlpt>564LUWU`3W>=8R^-DAsEER7QeI^&6KZ=WcUD=Zre7`r zsBP0F_s> zyk0cB9V>^OQbetr)<|jS-zB#cgQ$#Q#Phyb^?ILu+ipiUgb8HFevPGFKd^e8l`qu0 zU82Vs^;_w3$ze_DwF>moaQ{JfMYuC^*5v#$xff*25xreU^|gCKL8}qvh!_1_K4fcq zT~bi)(b(qr&lyya3^FAUjB>n48*cZ`_?dlXkp3qXF`zjx(dg^Jw&u&^ zT=a7c<5*CY;bX}B&qK{y+E`BN^nHjih%EA)+z#!=_>||vHNoKof+ZqZQ(NL*wtd*4 zR5vg?a3=6%iT=x?U*aHXsZ#%z{Yi_TdW-fgll4fOGbSOXt-0{6({hU{(zdz{0prar zT~58O7~juosoYFvoc`&`;kGn^?(~2#i#j^yDdiA29KhY7SwVvCpa<3u-KE8aiNeiR zoaQZ_9a>G^&8;b-$e~!kzY>``~jrTOpWy=pwhB8r?um8{w$hvj;PpXPcvaqNPn#O z>llgx$TTkbc4Bho;{FKd)o97*4F`P3;IwgTCCZg0hP=>FXbdnK|ynt$YL2`kCtgd7}IO0XkR38vpet8?HJpgJ)ST+HG9Z*+RGz!SySq#j#GY&)_EPGc6MTe@T+*DB{h-b2R zfJnW*>Kj+70EsZCO=HU5+`B%`=N8ATZ(R`sIwhnZhgifarfyz$P^O{gpjNzgN|=a1 z@ZGg+9{Gdy+^cCb-#0!rF@`OVuIuP7?$t_L`fg8!7XHciMl#vHyx05x@8jOvHc+q6 zxBh^zB0l!wUpQe>=2f`c@b_AYI+3y)e=d!}H(I;T>uOPft>~7$kHdc@8!~GG+1N&B zrLdv|@MG(_4}*8Bc_E}O8Q7N~%| zyR)&Vrbw%}9YP1pvZ9+pAJN~mP%I0g9N4Wq9({>pbvcClTN z8yMlLa`&QUJ65;_iE7{c`wKgDpa_5>-V_HxHdYkht-qc*+gHj0aYGYD|8#w7}EmKH(a4d}YOg*4{ zpnkz?B&2fm6a_aV0YK3rU0VpU%x~yqf!SxJW~F>2C|qNQK9=TapN18-8G4&Egsp}3 zOnjp`yq$X72&@2}nnPuV_YlPG(ZORv8M<&keK0#Uw5(FR z00j!7ie0_-;o>Jjv{XnP{PZ>)=zKBg;Or*q zA*$131ZJPUEK&zHN-|u4jV# zz-qU47HWQvAgM^U)P8C%mm%5A#$D^UfMPz9dU_8rY~Z_CEJ?<;qBxz0pa;Pg?ReP4 z)Br7zNF>F0qPO-xA%l1!Zkdc)zf8&IaicuI*vX7f5l~6c_e+u{H4C`(uc`4aS%9)c zZbU7oR@$2q+tL%g{(R$m`SB(fat+FBI#xAs`;_xy8JU3$qK19p)B)~0>*K(t+oYj4s?<1jG({I8i;X1MC2wfS+GBwskDsc4aqPk$K*R zv(*F)pJ(nF-{&s^5RxQlX(M?9#RP{HeQTDMSGkv@=yoG;@$AFV_LSUf4c6JnwzBqV zgTGRUUgD)3sgea<#$`ro@&H@IesAG2pnBoj1KKe zhrmm>en@`}ZZ~nu5;~u;Tq)(vCsFFfeLRj`#mX!3wp< zVB|IPCXTLAZmKMY%Pvpa;sQXDh2%+u7iSNRrK|nv?Pg#UoQOqz-Q6cNxAq+fcN7N! z?4%yodz{w9D^|i{x8OfpXj=+_X$>IuL3Y623Pj<9b$BFMmQQ=ZU#LC$$H5yja*(jV zsMoYW(o}I+5fH$6{M$uOir^>0P5ix2l>ox`EsAYW7kHVJ7Nj^Ahp>hEcn4D44)+q? zSm$Vgv~Xtt4^ObXbbw;owLF2pK%RX~6JTPROJkuOMZ3bdEuOHdP}zCGI2+-bd=C7)~)eWW_Pav@p)y_6IYX^ z?ho1{nk^5eTB<(sue6A-hSQ!sX@B}Poa@OA?Qa|@jdRh&e^2|1K3afG-7f?#8qe=C zU@e{`qy)8dL2>1GG6XCkGtWjBj*Pc-%QcxY;lbr(g7e3`A*+E2;rXgvYf2iXO^EcWX8p9imRaE<$}% z=dP5HcQ85pO@z+tE*iNv1zO-cU6jt|-${VjbsLN{ z8Z#m{Q#?+g7Yy^z_S}bag2ujmO5c9R|JO^%E37Zr?u9zqg>3D+xMd#E>D^48`UIPd zz}<%GLax?VlWKpD=O+B2`WbMK0i&yw8F4Ayf&-0M^JLJ5pFIzVT>cv~T)%4qy8E!f z7UPm5DepQKG@!DDd|TNG3J4;pxxs!BjAA!p<-_>#RspKhN-f5heu8E}dQ9L>V%b0} zRYNg`)}H)~5A9J6W}m%ev(t$I)%Ws{{dI-N&-?(<*ZvDkr|P4lw0TgOTK}9kszAeekv&f#kQAM^;BRETD;t!gCIOC{es4)qFLT_7_a@Ihe>YCL z;v(+!KjAWlBnZhfDp4hSSGT`L|FQlKTwG)y2?w$)g_CYRi((vu!buN>88H6eI74*3 zPAA#^C;`sqOZMIHzB}MRL9tFXZ5ZJWh-)OaHBiKQvf@>)!@o+@@_$6A&?OyAcc16d zfgl1vksFN?oZP8?OqO)Wh-|*X(Yf=Ew+<8D6`_3fZK#PKM5)v|$@6T7c8x0b)6GZ3 zTm4CekFO1L@X>j64tG8Kcg>45W8mmRiF(%u!I4l7<`wsDxW6UvIue&uMI%VyQA>!k z-DcdxGfq@ie$f)&FFOAEO-_88Pd@?l$ zzQXE8+y%SeBgxh$bwmw?A)N%W^n8?9HP<+WzvxhO2Q=m$@4)TO3!+r$ap#n+`ND90 z+?&R7f)taM>7IXi)f)%+?&n<=WJ^iLGUq)qfyL{lDOV^R$2m|zDX_ZI$3%Lp0Of<#Qw8ZWHiJiUUmFnB;^8hS zqkT>{TL=QY)aIQXR)kPFm9k*F2za>I$NR;9S7S2)jg9_dGF-D{z?f>yTu*HVf7m`x zCR9Qse4Hx4%HjXJOKmg-O#N~HYlT9*6%y$4*dM;ToUVmPGS5VDdk1wxY|uuiKE(cc zvRHT%p)qhP52K8`$Jo`|pzQFx9bG0*b{O0YqpM)I1;IkGknU?fDp*05)cp z#4X{rxuwn6Ld}kkH|_?asq_`JjYz1c*e%pDy1NdcI#v{zn;uk&-wc=UWX~h_FGxH% zJizrg28Z;76a(wqDTH#`e(1&2p`Ts+&6cv|ZSc5e9iViHQxpI$S<#O4jTLD;dX~xk zYka1hva!#CqXOl;12h`I2tP`8^6sy&8==p|PoKt`E3!^^{MJ=!C0x6SY=%D5y71Pf zM{PnxUV3;r?GBNJuXqF%xSRfx8Gxt?ws8m@d|IIcfjl3ue~e{+6y4UPDn(=SMyRn} z`9j8;s=>3%82Arp^~MsNgL3JhJ~F7=#uD~PJczoLBT{fHZ2*){N99GXE?Da=J%(T* z(nESs%*c3?lO-IT;RLUpx)mrnDRIl|W;nvI$R+1FvHwQ}b}0U7)p<^%WVO|6UGEBa zX5t*OG+EUQPYJ4O?*0v-9Vqc2lbp|@SbQZOvSMNci|e;XULmMN9O*8rz#K0HUG{G* zA-d9x&}@w3?g~+eeD9=}s%MAD%hwYi$ zucn)3AT!W(B+Y7~st#lYcM1`i z0GFdzAfogw2Iubiw<>Z+iDrkvcg16-?T$TnamnwBFzXF}$Q~9Wn~+Tt5m%mVEJypn zZnc2#yzwIB??v>xhZeYUI=sF%RDmHsdYY0kP(1bm{)Fum0V4miZz zm;iCk8~tBVrOQ08%(IO?%(GRy(W3O0h*`+IR%EOm?llEQFYzdmfPk(y4jcfGK1D@A&&?w>l$MZdZO{Si`l=_2%f!Mv%V! z--)%R2SPa*wP3kc?loE$hFp>aliQ+~32<4~OPF|q=T(7Gb-1v4faq3jRRxXQ!y{Q# z=Sh+z$ygLVY1sD^*5V2?am&W6zY1@PClIo!3=>O4lSY-bO?5i8s#OKM{-?JQe&9l} z6O0<)n(K+Ye+)rgoa|hs9U(MO&F}5kh;RqClg^%KwoCrOibLk_pF?ynm@U4RshbEN zn?L35Q>T~&!pJ^Rw^Yw-=UJdDVZJ;Pe-{oa!rx>sN=XqB14nJ6B|-ci5;k;OO_-nq z=1D*QvQKdNIZf(LlA~Vd8$Sr5-~{J%KrN_S?=qOtsQ$I(;XO6dDlpzK`3M-mTN-4@ zO$UkpA9P^tEKNfdKbWh~170BX@l@1vn`=A7o~JcF?yfl3uL7j2>);lWwRryA3DI;m z0=DP-)E+kW!vojNx0)Y`oVkCTJ6B>67<4sB7`!D;{FfiuRKt5L@m7O5JqaSE-jBnr zzmCo-9a8E15~uZItC?qCL7+gn#H^>C&rH2X3>Cp$F5zfktI11-EqMzRRofXTq^ePK zH2s(D5aXeS(RsE813?_xqfBe&-Jm|HIQY!bN=vZzC)QAwv*yLPe#Yu&25ad>K#?G3 zDVMc!pemi|W%2WS@q6ns+ja<^OpDKE4?vqX$Z}4BQpuet6yiGs<@z{r)zq zhmuJ37wqQ;elCZzR-G1=6QN#rq+6#AX}CxJPh;}-_^0bFf zccFD=bjCbi!r}AJk*565^fSUJL)y?XEER7|KsYogL>%#%(uUUe*878x6MHJN@r(|()m zt_1uIe2ehRTNjBE^s|=$kj{|~5L%2aT|9Onk-*k$Rir;y3yswxSFPuteeU^|YWbbq zgAc0CqJMLAs-wVb&4~NVVSl#gB=i;y=4{C#>Io(^gZq;bOdNZ`Hv3<*~6DBDNZZ^ z{(U81GHLb%R`SWKsgFtnf&rDQegDb7g?9<65px4-T-MDftsMy6>t=bnI`3stjJddI zy*Fq>H=L^3oxesIIVZn;HOgESSufV37ni&>78f%vFN^&*JpC4d4|B1`ZiI*lf90*$ z9~G(Fp4rk`X7@Lyo-#MflhEH^nNLa$?oMoJhQmN(eTQOt_4N7Mb<=5*0^JM8n%y7Hsy~XN$rvcS z4MIK%2{9RmVE?rLsFh;EKorIY%CDb|$mp{JzG08=6Xp_X#@ETT4JGEsEyV42djMcOa(-*jsOFO?ZTdx9N4@-iakGLJ$Njc2`cw||G!qd7ZWh$_K= z^+ zeiB1e71jbcU;9#tsc5n6fot58mee=>le}PAJfc5G>2Ga^eiv}jE-E5U+$6r0_r3YE zs#Nj~HSx0XmAc0UBP+Ssk0Nw*QQV=KVr6GSokRkg7cOX8;MM`5C#J`@I7@cq=u0`9 zaXQNy?&T}Z)J1D<07?A0l}o{A;mjV~1-6LX!sm{1=v9MxiCY&kMnZ}KFb}v(GZ{z! zr;baxGvSX)Z<9?fR`b%B;zm?zek`!i<{MtOTz(?{B!+);wkyjZ$7FLcdMyxjOVaL{=C!oHp%TsDWl!h zgBGZH61PD}E-I|W^sSbZeeX&)UNfnx657w^NR>G^UfSRimuymKM9WT(>AU<0*dm!q zN4p6%yX6>H6|7CI@5!UFay*P$ZS%e<{DEUg)J$VLYf&qLiRs$Uab)1JDQSX26Zd&7kwfa&bkhbLYRnVQ~2 zq&7D*-8J@`4{dCJJNPKwU*+YZcbJH&s0LROv;*^LI1fY2IV!BetU-!#QaM@T6U~$# ztWqTOnaVX-)$5p?Df>2lgtR>6OwzBFV;hXe!a(x0!*6D-GPavbW5=(Lths>bz8p-% zEvY?f1VQ);A~d`M&6^jDC{mqoMg-4y$*UL{J8&v^6WeqHuY_n%zU|&fr`wuo@LPK* zayI?t-kL6{gtGbm*f(5Sj7vzj{KK`{^TItj%|fzUZ1<8~H?R%yWjRafJDUoTJKi5| zdECc_G{frI(0u6XXP4q=SZm*S#FzCAqDQtJWdy>3FTbwK=ajBL-C}LGJScj9sce3T z3tkaMHz$4kieT4AEs=+e!sl(z$Z(Pz)Hs|xiWiw&wyTD8=XvhFXJE}d5QX3-c$rf$ z6(X9$g=l6(b7CAPy3hW6b&Kkzg#MD*7AZR3Y!84IL0i~bZnRLIxnms7A6&zlyK>z8b~!E*ZmRRf1jmc7 zZ(I=v-$ImpA=8_-qeqS)VdLaFphA!`BUs<`v8<{d;v$oJ%^78QM(Y7rv6vWQ!Yw^B z(wV7ASRFqo#q^qR3;$|S!yk%|?^ovQY)wXmGo`*}?6MQ$KEL2LOBQaT81yTbf0A#P z-}o7JJSPl(0_2CIpGMV~EvJ1E)^xc(%2pBrYGFFFw?VV9VeGl2w8XsDI|hX0c?1Qe zs3pMx4oAmOu9=Qwfttap)oz)DhZeuKfLXz->VpH8WgLVr8I=Zxl@>6av3dx|d#0G& zuMRK}Y-8V!3q-u!6g}rcf+p6kpu!0z=J8=d7mmGe2xn<*)KDbsY7vDxHJ&t_IKpEI zFsuQ!{LM_B1Z@YFqWbyun;0!MWajov=ku+Fn6y9P3Dj4Hd+i5=i9^ckE;5fq+MUz(2ZnWbj(Nax&E8b^ zDc0_VxNEo3`B3u%4XPrd2%hB=AtCpI(j&82ezjk#uXuF$w)Stu!o@EL-|QOS{zYb{ z{S8&vwtogn+tHw< zJ=_)T>ZD}n%8D6*bz0$@B#o4qGf(h~NrC_iL1h)u6U~s4e95}X2h_X%2R@4|N&N}o z+(^;iXR~JFFgT6={t#Y}q%><)?i-LFMmANOF-9N0!F8f~EAP@c3yzBKI z#)C=NVwr5j#wIeO6$PiRGabg%^oOucIRnB3S9d%Yeu!OAPv}6z*CTL3RG^Jz2y){h zBrr^x#Dm=iZ3T@PxqP1<9RBA(&K?Vvn$4r(1sXn3_;W(dn{Efa+<5R>?i$KjStlDu z@NH11gO3`H9}@sT^#ujym+Zhqn!yO8(rmXZ3!Cris1Kx4gKY$FDv;(#emEm@`3o zw){>gL&C9rX1Zg`;jWNgl5H#EunY}g;?-wruH*8r(Y;J*S#=c)Hx_QP+tRf1ho`T=`f;}C8=5p?S?jv`W%tuvxcnyU!xD*8GS)Z#`sxbqHplG-`z;@P`|4nXFV)jt9=}YTtOF}(@5g=6U{&joe%_4S5iZc!TF2h33*iEv@ z+fomX4j`Klpy`WR7O4lgs!mA~WH;}{{Fnx&^M_~*_gcz_5M7Pu@Xxwh3z>sqOg~;( zfl!Ok_4GNSwa-Xc`N`~h=IvLoC&CXTKTL@8xf3sC%uTPYG_9*itmk(Oz$waHusgqB z%E|t(7hopD=6X-X2r&*gl#C*y4vT`lZ!G&$+7w+zX%#JTO<3lBhAYY4#w=gDK;O-7 z7|^D6nH(=+%U{~PSB`(E6!-F(^cgXf;oR3^rOCqaX+YU>u40(@BZR}291n7!5-aSX zzK-XwXBm909t2Y8v0x&tDteD%#yz`8&XG--!IwMa@4#Wu5Q@|+st~*;@+;65%2Rq| za7DpS43askvg(GuSuejG{#ndc}?bgE{@XN*o1XWDk*It~2lK_aC zQit_4U{vL32s-c6N*Ej&Mm^b%UJ0igwH>c!ARPL3bQZvM$<_K&T?twV@6F5m~5X~ZlvUyh)okaP#fLb4BYHqP)3+jUzraw|Ctev3Z)K}*hog$yfTK5QDRp#a#|jY zXA=s}XGl#3m0H@e88}2Ygll(ujV_vA*q)5uc)jtotpF0h*)Nts*vqLK7ewaPEBrY_ zkD8m9F6>5Yue*Twg*JhgfT*}R-3VbZ8?nxAkJVjK&S1_5&Y~4}_-AW!@trJQO#Is% z-Xr)ckOE_u>=_3;lak{}s{;oRg@8p5TUg}G zRbK6#FNaqYY8vi?brLiAMbc>e2yYT}&`EyZAA;bTuvW&iOt#;8#@I*@XPkxz)s5yE zlkUQayzSfmJm{xAG_hgy3%)6>&!!_1B8&I39vNgkNtP_Svq-f1fxT|mC(ZF~d;^oM ztmkaa-03F|t_{^+kH=R~uFjMu7hY}G<-5K8RgdqGUjk++{=LLSPDK~6e zOFV#n-X-~5=jSN$E|B_M6TJ9xL+ncj;d|?+7t7N|f#@^^_u|1qTs8&huJNF;)3L$> zja3hyUknRTMYYDUneq7#4i>Io(%grI-``h|EPoSZ_o6`GU(BXn*TqUuOE^tjM8!99 zv$K@LUPwA>P91So`jdK35gjq5J1;?`z4Uwck4Li{B%HRnl?&6_R)wzNS`fekJDf$Mr6>omLzdU9bKx zV-RHFK2Z)bGB!p>|44H_$6;~7>gyuJTqCfk({0|39q}Oe9{FmG$#+Q#qJNq0JB(G22vsMv0OO{bk6GvuqgW4Qcb2nf;TNS2r3YaY_|j96wE_ zY28%th$IbDZDOkeh|<_4uOP$EM5bKPbsexen`L#O=4@T?C*X`({IQEvI+6CdgU~&v zzwh@D_#%gUaDTw6)Uj14*l>NTGykqjaeoG~!??MJ32+{9?~kc+8;sx~r`cenP6Fa1 zaiy!ZIUVB>^bGhD_?gCED#G4=$eSAtHjg@$Ya2&rw`GH|V$hxhl>WOugQm zJ#~U6G21M+zsDFsHLoX$KSo!Ta}Z?ws#dRE8juh!tmdMdr?HuV{5_?u3CVs^j5ws$ zS$g7Y$RKM&mD@eJQMzb$X~Fb-d^dwoZpPtvPe4ZYhOIeEtdQ!FX6%lsJ`xx7sj4m$`C4WE6tC-VX1+9* zhowA4kN{B+=UHu$>+zfy{3yrW&^Tp$bB};i<~_l%O1(55-f(Q~)^Daj9hWE3u4wl^ zWy4DziaoCjAWW*O#%tpJ#S0`MT&-mp&`0f!@eFuoHm7=l^at6Bb9pWV`LJ6hQS+PK zolor(C)rOq9SKZ1P4?V_zRZZ=#AF`xVZbC$h-57ktF`8;J>TJt3D3Lc;b(N{Z54H> zJa%d_T}i0r%dPQ48i(iQj;asofY0N0s!&My^3&RLuCXD0rz~|Z_BvvrXC2pOxsbUl z8t&=$(yEAo=}6WXFZkr}V>DQ(m}itAJ{T)Va<^lA^%ZI$U`};gCkHwP??N5p0s04Xc7VKP__0e$lVH3 z7g5H!UDov|AlZW-#;Wfw)z%jOfwV{_Ml(C)e%^(J|Vb92ZLGIL-5KGO^4 zuzyI(3&YKAN&_l(AM8`rYef(}kbEL~;`_WU?NJ(gHG;Twn){#wuwoiRi1nHNkhN|2 z-VJBry296}GrXxo$@J=l3T}71-CxWKFQX3RIxdi;sSmLY_EP&HPFs^gr`*?+knBB6 zF#f07$*3=?xa&)oEKNtsp;q8atc&Lv+oxiwsVkF zbItfpKDyo=(edm|3_AOF50mk8EgYXPC?)?$81E%ArNV+Wf;`(oP%KGq-VevU2reXZ z-fI65f-KoxSO8z}F2fOXpzAsr&E&~vUS=3mOR=0udYHLbv0!AjRSMo0i(zpOQVObF z2jAW90@o6BQ2xwgUncGeTz2p1qn z5&HyvU^vyqrAVHmnGWFy5gsdOZjh$XCSQ%}`kb$`!!1?P9JE^*TcE46$WG)Yk~kbk zxT}DMLD1&bqG%gyZZxMgRle-aNqv++G1V|Wmzxh4HE)h|xVUtwIsNBihi`^~*{@?H zl)Aodb#ugCetYoPtKmb-OLfyB870jKa#hZ?o&DU|-!Z*wrhLJagf8|24E2vu*kvWE$-Z);+i1Rhr(Km(wZu~g7qAW zXf9G4*#!`DdR-g*THKY_Er-ojC?v+TH1WP0*MLB@AGs7I_A>pLzTYeutFyT`3L1N- zBxriTSZooGUaf_7s*mN7th#$(?smu&CUCrU2=Ceyw$)4+8VE(RkpQ(NCUEB3gL^4- zn$86@eieJQkiy6CrH1EQjv3nIle%Tsv6&pii4ib64&7*(PaIvs#g;YS z+X|l<*NwHcO>N+quMBEX!(DTkMmsH~qpCG3e-ggbsbl6;>hee-JmnK3+06W=(y9jAsH2ic_2-Kc8l_EJDK4b z0V#w$#VL8Xbca9klA=H3!qW;c~e#e6dM^(?A9sm|}M|biQN|nG^=| zs(~ImEa_X$girPpr7Wn-*iY8KzxM0=jI7l z-p|HRbI|znRFr)sR&4NAY#3z*?rG&7E;eB1DL#YIGpXgP8dM*l$Y%+gjDsK&;Jp1K z@SpZs2(hmw43JA9be-m5V1Wy`m?3nOVvWslaa^Tuco;gGziuTaGkAq`dmCyS8iKiI zY&6Z>sNr$C1TQyk#vEl;e0EK)= zz-{zb)=$~jjbOx2NGyo$grXku0!R(!Vbr5A%L~7baY}4Ss7+mWcWgWP&)7reW>f(I zCb)LpYN9BO;VG@xT0q;&4$F*VDU5P_hf-^So)uDWmlZCg zr%R~Nn0Bqo^8s?XmNz2LmDXC14p14j6nK94l&D+V%po&lq4@ z3b_FXm&po@JfnlOiGT(m$#;jH&0J&%)?lZypV}k!I*1FTmkv>x+ZVnq$y-}^?r6wJ z2aH{$KVz%E)FX3O+jwvP@`xi{AQd%_tfSfM+gzbO0n3qQWS!Vkm3T?@Y$J}e`e(U- zqv^Qu>{+J8jV=aa9JacJLwSi<+RSZ5niJ9+?&5JMpuDHJaDrUo(^ZlrnQ5Qnai|nm zOd{%}Xe5X>d0@b}By}4(ZsGKi1>d^%uqjXKkJ8>N!;uDYoJ;)L)B->si}w&X*CDAibNBD;J2u88`v<~A|y@`$l!QS>GhYM&#QOs%< z#5vlJcn}UpN^A#d@?|3*tL7?I2zohJ>(Y zXRp4I2VAKRB`Sk&a0X}0^t9?Pg1h<063mOb(ny#-kulr8nqo1{*Vd(grxMlE`iSxu z?R0S2(}Y4ZPWAoS-dxubdpipqG(dr)-egR+-Hw}3fRV9KVn7dxab(6Jsr0;HBOR5- zm|d+~ALwJ&(ZUD&U#-%fPcl~$3(Sl?K~Ub76B%5IY65G;8n$_Ux1d5n3r$NO59$myb<3|0t>_K@M1pxH=7E>;#@G%dmxzL68=avc~Nf4V12IUL~~6w>Kp z3~()p8k+!GH!!*eAZlXV!G2aQz%;8YV{=XH^?D$=?JZDg{=k5K-%W!4uvUAh92>1s*3P+&`0&W_Nn#|`K>MNk%Wyp0d z9n-gGuLl?Sj>%bm(?|;diE~CIh**qVDO<(-5OO4auo^LXP23~`3*}p{<^ut`s^^OI z!?)3Q6|Wv^P3&pxPHMf`RUx3|qbeOg?$o&Dv;{#vMV%M(g4Ie`EP{UN`I7b2AbFsj zkcD)7VwonULIO;`3uDrFgKvy)kMfHfXyQK4-w;O%C_IVwa#eeEUnS(1!z~?o(hw3G zd?%4;iuRY*h|z6)Hw}I!A(~8A`S=CWoByq<39G^x<=}J)Rt4pVp9auJ7im}43Z08S zJ%k`Bk+b8}`&atUss0q2d%0qb5~_s^zHls1Ekr;V39N<*ae69Xz)Kyl3r3kl@%|2b z!|bB3ia4=a&V3&ckFQm?DAwErezu4I5ZKo@TuQEMc=jtkNTRH+!wY=B!nvkZqoEsy zRbo^KlAhs#kFvziqWgM;$El-M6{ykUO`IUCC<4!xq{H{7JvgqeW(36*gLo`pIuFJ$D_bHW=x+m~{W7D%`+qNj} zE~#%nt^?WmO1%t`s?S&TNj+pm)tnE8&o%zJ>r+9@XWUE6R#fw9Bmob?UCfUrNhd#y zW_gF1ccg#07;@T0v7sE1*HK9dIqSe@DFbY*7b%+O*N*0)_DrskM$pQtqgLIc59v-3 za0C@?KW+MQ`n=>X@de6m66680X0x@Gw3aYBH*6%sj{aE!SC>Up(ei6XS1JqD!|&pc z=S&)}&9c)GDpHSBw%ty3K_t_fJ{2DHF?fVt&NF$AHWx&jziUZz5=M1YVn|nE)K3)m z330$B=R5YXVMq`!nhI&sTI59tw}lm|H4NN9j-KawbCuqpxe`L_(nD}SJI6aN4}Z=* zC8SV?$k;nx^19PJ{`;bRj^Z4E8Yf+m5>FVYjnYPa6u&B-$L!2QF1~1SLEy5m*lTM$ zDs+$h%(aV!^s;m*l+je|`H@>B-j|c-%(Jmwu_G1SdB7WvpxIAsWuklFLM1O%%tQ)R zys%vZ8#8AP*?zzt5&-M;+>5T)RWrlq678vTLwf#dd%;594_fPWKP>n9m{$kg=1e?k z<-l6|DJ93m4bdd=ztO*-9-LQ@M?kR79J6J=XxHakXp1W+ntZVugg9!dUxuC0V>f6l z@&`snILbzvgY^i8QxkWm>vu;LI}KI54bow>cY^rCy!foi9;V+(5CBcxv{e7`?V751 zjeTJ#jaE6y~e@x}15M=MOeJ>XQBxn0aLlE2gtwKYx!?eyx zrBau@$T9SCc$z@ngW-0nGe#60-}2)zdf?WLL(#Q_*934YLbSn{aV*9@XL!3_1|f!g zlPc+wzj@_9?&i5zWMwazbtdx)k=J%vTs}{J2lV~D)opqJa`w(nMD(!?;K{c0?P@d& z%en<8JdQDli?`!O+A(I?4qB2|CRd%enbPo`cyU-n zn>j-CLi~)F+E4B1`TfuBp2Hvd%jHQ2Uj=tC7f=3S>u_jNJ>dmImm{M6IrG0qcEiq4 zm$t>zk$X>kZTaa&ql+|MT^f#Xn@9y<}YQ5Y;dVv@n`;{3A`Rm`_)bwUQhJ1Tu#I=EOtr<6gIJB{6;P*Gsm8A`i`Jb z>kA(S-PL5|4Gu|`a%=t3K!a-;{$O6t@3jtm8sJ9*UH3$rf1eFR;2sf3k_Avm54W}7(a5LopE7ei*|U#2AM6V{PQLW2#`Z7rA&_t;HMBH>ghJ5 zCl<*)%QGX{MRh+UB(0B^9$v7HIh%%z3~_@_6lW1u!{t=85{E3xC)I8AZdU{FK3#aUzkD4vLc)ids5j$A$YTjCI6ct16e0RIc zWsesZ@>u0?Z)r-D_`?>dwQVhiM?A0L`_$JH$Xomnk1coa{G~{?ElnWzAZ{3=SW%22 znq&lU@x75Quk1tPvVKIsZ5`Zq&eS7t0ijO-dBVL`K7YU8ipLx&G$<%4?=59@kOO%E z5gr!5xr;VA+^#`&ZGKOq>|UhK*E+4vba-l&o(z2LhS!}g44VTzzY5PVMyM8+yd-_a+R~!rFp0o#Js%80Ye5q)?a(aaKz}Hv6 zCQlRmDK=Kz{SnwhDyUDLMbl@WE|EH7qi%Q`*N<=A`o-=A(^q~Y#0aX7b6MKpZ@yZa zGk>laAcSfDT+^SgAvD7y_2u9r9noRub)y6^AS?H)o3oy7C|w~2%nech>MVy~Y{ygz z2QeCk6xF|}G%)En66;KNR5lSq37X=J3rEq#o%&gZ(j&yLUY>au(8Grte%XVr&SnzX zht~c2Zt~>6MEn0kuihYf#)tG64?61BM^2SDb59bne%8Hqec6pzx8n56WOhwpqL(^? z5w#$Q{DAKxwa0MfI)ayrCoi)}uq)ed_<3##&=SLhWbCRlAORE}YYM#XMBRgxXj74C zYKp|<4uE7yk=;t=5xdeYJ9A}NTuX9F0W~`%c3b~W(^JD3wqTkZS(IBE#=6M6{oGr{ zD{pRr78rQvBmjL5b_@2%I#CtZbO%)DTm*DqL9&xu7=?u9wTN(UsjNexk~dN#_2k&j zcFEanz-yupJ(xVq8c%F}vIe*mj-HP4O5W{lOuV_$+%m^|6VoTbN}x)@AV*_Ifku<> zU)iK5|D+4T0hr7npTT$9UK+L4bvoH~u3mQwS%!-0Fl?{sd<3;mUr!B7&YH0y zOMqVu=ErNe9(pL%sw%^_GF+?pV@jh&uh?-py(_XS{7Dl#f!fd6jW2>NB=BED_?BYO zlVcBr5C+hDsBM>%(k7>A zfyuYXXM2sDTxYqu;fz7AVa zjn%Rmzb4v+nRTtY>%*BZ-@F2Rm0<}R^FcnZS{inUg2YPS5S*i6tjV$qFAi5`V@KtNQJB=4g%-Qx$u0fgDH9vC@4d`9zqiCf=9WZ)OC~{3>__1@;219;dVp z4rA5nX0AO?OJk?=fvgTnPjs8zNOsbie)pK@7NPyRqMy?ZCzPhnn;eYc!-9ev6io3# zwD=O>fWt{q9a#~Z;IfAF*(n<`8O*Ei9@c!QZt;Sf4^9^Hlsy>xAzz_50qw15v>Brn zOrAGL;f^d)EB&Ls-B^m4&=*pYEbn1Bp-g42DoP59l+Po>TE4isUAD5T-#837k25)~ zBL`r#V**Of!$ztSU(}C2mH^C@cF)+hel{T$Ga75Ap}~67E+u{}9%?pdn6x}D%3D;T zW?cMFY*6sx#}8L8W{rVs$5=9IJziAVht96rF@IoXg7o(X+D#pFQDczP{bvz&xTBV4=Md!})f1+D~mQ zc8$juZcWn4CAS4zZ2Ku;h_DeZW0F!)=Ul&K6wgIUS9??E7eqR46%v285B>mR0$)*) zVMUdlp+!W2qv^sEpOFxx1#6CSz*dZ+zW9d<3#HsDY8roHXbl4ba`D8sz~E-mzkl z=?rk{OoG0<_9L7B<;@~0tW3!`dOlDSV-hW<`A3d{8qSYlL}R^}Md)W=RJ<$6fnw6) z`9&|(PYBG9>Z|gi(91O(QKI?UGe+#4)+4+iAw@g%RNLPFq3J8bqWZqJ&(Pf|-3U?w z(%sT6-O}AXbc1wvH&P-U5`uK8bV=9HIq&@b@AZ71YtC6`uf6YeuiWv#tRjv#t=j69 z$!!@pwogadxbZ`nl)AToHWU2h_0;-qU#Cw7^dRs6AJnzU*dXI$&fs|LIe3P!B^#Lo z%S{N}7zk@wduZ}H8UUYz+(?bY{$zzR0p|y5{{JL`(Yo-#@`}OrfgSUwaNKh%y3p!2JO5Anh2R9iI6#`boCN4F_b5h;; z>r8{w&)_;rol7l~&q%|c;3h|6*L|;rq3ldwIu zRs2$)gWCJJuC$^*1{sDx_{51TDhn83OpdS3Nm2Y*6~AqBg~Wof<*ehDX`lpV%Pjh2 zcF$RCv?Iw;282Sk3!o+FM?poreK9{)iqqTWc6007f4RJ+(QJ14O2t%g^7D-yrTnI- z^TdzIahm>0FzgRb)5tR(r+0x$dXcw3EdPFHCz2`5S1|$kd9@`R3N^|OQ$4NmCq#h4 z2f~;WrZ#-Bae5lB+yQ#k(E4BTp(Z6xlX-9cDva}i?})He)l3r@HsiTIf3#1sSB*_4 z_yFn+Jwl#v(kMtQnH*aARtk9H5TUq$m{&?qbRC7RN0vx_`Ed7`S(#U{d4&%DF;3qAOu&~y7&4hO-{y}6BNf*bFNTC)li}S zqB1RKclr~r?T^ZR(iGRvb9m@(f!yIUhG~6q6eyZ=S69-j*hg$pvy)3(sn7cgZL=-Qr^w z=pPRE9T9D|uLkA1wDS!s09($uiP?`)lV#G=QXb1cPH*pIKM*}jMzy5^`_w1%FUUMb zvo^26w^PD~88w2}ecEgLDkb2S&`|O1Xj$@pQ1|-8@O~EaL7XemPsNfDBR~&Gu&Yinw2k1*DZw7XObZ=QN@^5+-V z+H}1=wNKk_-xP7f=Vv<(7#y(cgcI8Auh~>iy%AepY};u%xLHS;8ee_aFGD>nFP^?g z%|WK}6Akx8te^LA#|F)kB~LWcj3iaO@Ujtn2E33JscBd0?aneEhYa^YHr#wsg&v3> zaB;gvi?bV(cfwC!&lln|!>1tr;6#aUi87OG>X#d4`K-_xd!xTjz6uAOPojMen z@`)BSY=V9)V^@RByfi4s;sqQ2Cw{XrVX~k+KP|n={6g)jQ<7q_`YBM8tN%akY|55F zxA2QCw(&ktp}2Rt2yuIJyBW!d`EJ@G=}w>G8KWEO_;HRdOHwysCThB>-}9i&nof=^ z1IHa|-84fr`@^cGMR!(C$P&BFsoEXeX-dXCC`i8h{ zeROC?dbALCD@$sK6XIGe51Z?z5#d=&oUm3C!XD#hR9LU$R_`lwK1LqG^FtB5b}WR| zG%zRei#ObYUe6*YhDFMs&JUHvC-noaEDseS*-3GVtAluXWUR8}Ko&aEsPO zezfCWMfh6#!gC@&Im-4L-g=f3#hiq>c!&s5;W?8VheO_wPfM@S9lq;hS|96SeNAj4 z9uJ|OhFgaVAoY-}t+ke)lR$gB1VP(epJe4>S_aOMd_|2!@-l?L>s*Fd%^d847>lLq z57wKN5^a-oClLo?Wl8xypig3^u2be8pAVTEX?&DRan*w`{0O9oJ~&K6Q?AROr1lwx zFf>SO`lNOd!p#HW)6EI(hhRI1zalA?srNf#VKxeE%0OY<7(TqsKioudJxmAeilL^w zCJMuWThwC!%xb^(bZJfet&R41G9Y<@Os(6ss%1gGs`GZ{pZnpleh-Nk7F5akn(ROF*v=UbKu?ue)Z5^xJd+r*54Zu)PFHTq$TY38x+a>)GTwem>CU?IAb&|}QJq{`H{x+j2nTpr1r*VmksH6OT zWN|1l%M-}^NTiWW;!NxHUyfP>{5l^LeYn_9q=p#(Cm&GWHeXn%*FLp@=zNQ=O0eO{ z^P=8|hGJlDVh~Ja;zRBBBId~biaEqtWA!NS<2mAz8`Ezc@pkMhb0)w##EcwGIcn|! zi0dV<`$x5e-4K=V*AFe4>N<%c3&yd@SO6(W8N0~KdQnv?>zQ8r4_AFnpS8NED`~n> ze@h&WW2~d9$$rDUAlpRvduHTOaz_(hI96SNBK)oCCH!i$l_u$U#PGfkrK$mV_3EI| znL?C*axQj9+_Vq!R7|w;_T5HcmJsaMwa^P0|GN2#oBT8li5ylq`l0YaIyk}Re!6NE zeDqoRmEt1ksaer?<)+fB62)h9c{aLX_~#Q2R+aJwBF){8OY>;-X>51r9MR@JC-Dst z9cKhleV?RpR-7D903}!&SxMRDuh58<3fMObxk#0+|0otSy6 zM9V?{TyyD&BZBXPnWJ}Oa*$V-tEUJ#l+gvr75mvm2x3KDybJF+rjknpof$vzUlltH zG_IP=-nG~zxb)h!;`8J4qonVsIZ-PA=8x?u8eaP1O zcl55!|09mW%?2k%DZ7dY3+mJP=!V$0BE!`tS<+pVer~i}6C*q)(T01%Iqh&Z1g}6x z|EVR*tTogoxZn1__Sn-XN}q-2w094Fe{rXk*Js_1Sj6C@A1@SN229@SVNt~I{UI>a z-jq(`i?7%mdnd(>3$T$+lBl?Ry^eKAl<4yb6mfye;i5DAb96!7b<)CU4aQdU_ETpL zz>HcnOP^5;8HLy7O3#auDoA>9ymNvp2*qo!O1$9joDHEnwOC1xW1MiX6fqFmiXYk! zrfBg^ZYvtdY0l)09QXgJqK3yLK02WDzs+rDwUR7KB_Ki59?^-CJK%_2LJS_q?Pqc$ z3#)q1qwX)GpbSQuErBm;zycf<=0L9)KJXVj0cJJB;BBUnOuz(5ltAp(G6AYvc$#mm zO)HGISG;)U0cdqa3lraqZf_GpQ)YsR2duFw@N$TgWV-8d7wxST5nkeMj1mT5BBg0_HHVx={%zVWtZ6v{A-T@O zwnxMed)McTsfBrPuI=bl>)VLzU+3f1-joIUDfqpI!XKwc*F7EhTrdunC9oQ_t|V-_ zh&SVuvCd7EiDHSRk~j65t3NYbuH=aZuLo!7kpmHI!MMNtOMcDFI(ls9AWRHp(7+7c zr#XV5fIg&W93RW8abVD54r&lrCZ+xL_9C`OVvY7j1`u~vN&ZFm*NsGF3UvEPSdsh3 z%l2Laiv2T7M*+etdPt*IJEX9jtk7Ov@U6f4r&1O_rUTu1QoC$?(}w6@+&WKv<+y?= zFIOySOtivHZ+eiWeQDYLU>|Tqe!EtID1;zMOG%Cy7v)oeS9ihOms(L3mGA zl6{VR0=G{AXd7Bg#O^FE?pY9o|0V925Yde|lNK(~Vf***s{edWuIqC*;DAn+Wl9qH z)$+HSrNSK{F?u0J2|oCUPR)_+%?{PUFnlFmdr!krefEsyBSfB+0@>w#L~0VviTX6P zGOah_onD?W%UWyQPWeYebJJt4#WNpjcI5R1ZUX$UwHr!~1%A;RW)6xNT`2cRA4>EI zeM+Wo#X}K_q;68;ipibjrxf=ZzDlg+M^$w#6AUNfj)EBV;ZB3ABB2Gwh?yGm*O>wfyft4|@^j$EJkw^W&`#JMuZ1RvVVs6y==6TG zllB^o)K#@3Q!I>Y3J!$li63*L^=3(~ox0fg)GT>c0xMLm{S2nT@fE`DiYZz^<&3}N z>uLCwRCUqy#CU*{-peBSNpXZv9aTl5E#gR%W%bxJIsmC~Fok*1{XQmfo18_BBOg31$UFJ2YAS7D`{Ym@$1NJ95jt+w-z7fRqkqxtl= zX}OQFDEaMzc^_fiS~1(BoI1TW^k_c0tGqVmux6WNo=yG}GI;!~6)vOVO_^hkS_kN`FUDf_|ol+*j!I2o~V&=lvAc+e$jhQH4?4?S2LNvdSH?klJ}U3h~dM124sQ^8xj!-$-4A z9-zP@dr7_2tk90C0&231~$KFx_Y2F4r|r?)KtDrCV3T^ zlDtW2{>IavKdTg&93CLl@xc80N~}}Tg{3te>ad&a}Dj$Yr@V3@;9zzabvxgc}pg%BrZ(|3N(4OU@a^EzkJ))9D zn#JRjO-C#xcAwzuy!}?1r7Jc(Jz|C)onKHG2>&_v z?cDZCPyK%c|H2 z2-+x85m+61icKSd>D^5J?_Sx|Z>_-_#=BJU zd}UEMNnGFKnt2$*#&JT?-IfLQ!0+WsL=sajb%|snn`(Vl05NNwzW5FK-QZNX z3yWt7z@E)T>x=447%?ZX>)_vT+BfqMy8apZ_FaWY2NA{os6_v{i$?U{ACBZy1eO7_ zEpx~2?S#fgMziZ}?4*1|g9S-7*ph9IxLb>AX8^Mw8;9Me#vm;d`GvE5mxi5k`^Eo+ zva{B_4J!P8=7)TG{AQUc?ud`rs4~kOGolF#ShR{e6Uwuki}8AVLTTBK&dX<+*K$vQ z*j;bPeXkCgRh$WnI{NYz21$VvrQ%q?9&**X8h_)r2Go+Nk_z4%>u7|dzbOJ#xhGz! zjg7}}0>Ja#tF^>PfjE(!1QhSyEI;;FjC|bLucyN)>5kt|IlQlY`Yd2G5?Qvi3jN__ zlfC+W3FVskC_CbeV6=>RCeJ-79hpjMp&=G6O*AkUEm{*XPQ^sz_&0Ri;)LU(q9ZUB zj~KuBb(1+^2cpI=3hWlQ_#h$e!6N`ewlX)9bC!So`qjzoxIJfuFGKFhP^s$7<&6kN z!U4Vo^FQc6+RKv1M$7|lAzD@G-8S)uap=guxdoWFoPm&=Lcj_-^izmgqRu0ctS3mm z0^5fjaS}u{&xL3L9jSkyJF$`OP2bN5JJBrM2G4e|5dUjCJ^PIg(JDy~6 z^zlbPMQ$=Ka#Q9ECZ3{h$VjtNH+)rRXq5ST=h#MRCdi~>ZpcK5F6&& zg(u9v9M3(FP5MYxiM*i?52Nf0Qf|4?jV*JCo!((*!JIR0pD?R>mzSr}4W*n-^?gyF zLDy$e5dvhqYA#r7OkZdzHg!Wr*lXc&{(F64a>Bj}`WK;)6TR<2=*5Ia609Yv?fRE} zi0XAQLwy${khKY<@0X$$t)pdj~WNIpn+PM#lStWa-k`whOzJc3eVj96Z z{~=XU?+mmXAB;`wwo##dB=rEg$X<^eK0uL2HG~&GGtSa-Y+vZ4CO~J3{QO}n6>mVFjC42H|IScyn#*<1a zLa63_@(cPw0`D;o6}F_nFx0^O*+2wE=R6bB|(geq&{`r>ch%k z#Cjv|B8kkkC}+bYrNaqJB%@~bS zI`1*LB**~d*fg0uE?>8B@;CqPJ2>j% z`}N!KpswqmRn!h%ji%wIf;J)qZ@6bAQns=WntE1@#D|4>M>IrlAe8!A|@6;eBxIhJ{9 z#Y5Lej~2CtOm*R?A{S-!8>zL>qbcu3JlCUghc7k=DwNA%jG?5wF8AX$0?vBk#l>er z7AX~ag9ou=lq|}t+h9~{a+ROs8vMvG@AHX|a|$y=lVYUM{IaVj>;TvGscG$;h3-&z zCii3mR~+4Cb{!Gk#uyE`xqv6vcXnmc^i4?n)i(Cd`*3ZE>#SHOYnBq#9Qvbw48Oh~ z#M!EZh+9Vx!%q4uBEzY13V9u+7@SDkhkBx^7IkCM)c@!*^&F?*Vf}B$Xkk(- zcbuNb@4!9qD}1RGOHvR39~2co<#UA7FNTQMcEm-PTgqpVHXKnzG;LJi{!}C>mHNcx za_u>rj6bH4EQB^59=rSLS1UOV=cD82e^{SCAEYw=^iH;s2_Xl7x}p~0&5P6l7jK_a zxNg!#6P=n#7YBHlH7iGaKls)XCJOT*dmJ=|qew-hrMws&2Q~aUKN>fjf^*@M7E5P4;K$*s_K?g(NP0>%Aw+*=CDk0F-@B{!S z(8jI|MgU~3R`jW=W;v)NnYg1$8R6>5yM%D$3V9l&27Ft6G=SKGJkC*9at#3@wjDPN z_0D_>i=B(xdV6mK1Hh>z^uu)w7bm(q;GmZp{&Kg&THO0)lE4xzvKx1+>Do-A?E|Vo zO9>dpd}NkJYoVH5-Si%rYw*{DDf(wYoyM@=mhXtD4#BU^XiL zA^UIobycu`6WZ=eH~zi)qdct*|I1fOLu0r*qfC*Q3d&EvySJY0qHN^=rk&lk0k_Uk z()4bG$F_-v63{xQ44q*70L^>eCR`(r-EjTNRbHrKyavy>VwKRe5BZR*0M$zV+rJc# zKS;8+*yXn&ccH5Ag@^?roge9_Pm&;@&F|$S&Dl#~uJ-7AvA-(EMlHSmFXX=+l$ix$0`$rQ&g427F$C+7gz`Ep-Uh7uI zV{hz<*q7*HH~Eg+_yp^{9YNFf!%6%r87W%zHc9yV?K;E9rjEfjXTEx1DkNb7KzqZb%uVx1pQ4+LFGI6E=2(IDTRwt8F6v<9?n;ln z;2qyMKFbBzF8lZ)Z$3CXkLEjTcLHaApFeY~3;E9<0_O->^gA2-dtSIfAaZi0BzY{v zSBg)jjaQ%pQYh)QYJVmRxs?e~7kp7i2l?mpN!zTTeRq6Wrb;RDuwNuua{pHgFc`9` zep~aHEUuEOP-JR-hUR8)9U+dW)(LtMP={CG9H3kD#jOwl!86(7&#~3iEQdfzJDiY>xeN%J}Vn5p8F*`IIE-}oE&_D$C z-ZIPVK*Mp-4twp5zF(Sns(=4E>4??hRySKCiQ?gNMqs%Ro$oqsuZqO<1zud2$l_Z9 z;;eCUrubWCl)>?$k>HGsFtsUuaQj(V{eLHe(0JAxzME=;l=|HuT3S+R-BDZfM@HmZ zDOhk`K4w9&vE=MCy~{V7J-&W>MOK~Ks|Z`B5c9;t>ChuO@@Uy%L7?<~Py4#TIXx+K zC&il8uTqlm^`GaMB6X=nmLEz4Jn0w+hsDtxE-Se^+`#wj9rn(1PC^FyJ`kbg-c(x+x&LxMlTaGN z!lUYpyd&LM{Z1T_gAJ}o5(bqSr^dt0YcNaak+H{M8T+Li(CR5RrftobZ3?okZ>zA=3 z@Lvyclkfv9>9_o#8bbj}IeiKnPcJ&iT7Ql-f1rR$puW6ZH&|eD$i=b9HBO}nzn!lU zkzRZB4sKAI$+IFz)jC!*8y2qLyL}{NljsPZ4sLlzWPvc4moyk8@#5M*ZI<_gr4RcE zC7Zg26^?&?+@A;vK7v*|Kuf9Xr1ria?rha|z<9M1HSRsy3N}_nD1>P9>Cj@f{Hy+A z&gGmz3A%|qu`!Z#`ZqWpBJpBQY3eX3Y&2s8PDMUJ&asYIxL}g!1jM+vzg)G}u(c&7 zFQ~G(u{c)j_DlD{4<;Ju!{^Y}#YuZEP<)WL*i9AvNgtKT?mjm&lnX+lO?^iHBYVpk zcAh_8+ejMGq@M#!nSNLK{TZmnFC38p&e%0c`#~KCq=%(tng~jt*Iz!V-%B9G0$;*3 zC`#UPD9^zdPCPu&P`Q|0%xmhHnD}q+$ZsJRIeD+?c^&3%DGe4T#W*-I(;nZuQ^T^f zo}!Vl(K-Az*(b`4r@?tT_k`dZn<8`*qU1jDlF`WjNWI0o87E^(N?GhP>g{1^kHAG} zzyTCt|D~|E)8Ux!EnCw(=k|IuzXB%oU2Z#H!&C~5@`M7zLik-!e=~JN`-gmk3@NIS zO`Mm&&h5Vim+#4SM`sa0zkB-n$}ERJk?d@L*(nm=@f-OU02_F3M@%ZE{$i(Ecu57C zWt;KS>p(Z58YO6VXkCPzBn-)jYg8tlb91o5S+pW!PPsVOJ9Q`E?jH(zj7|@F)Td% zb_c>a15$H1Md0$2{Oa4<>LQQG0fk@_K>xw0Fv-Qw$^tX)Xb1Uh&kn|Sk_mJ^gCU_8W?rcjP4;qKEM^pI+YA;TYW47VOyeQwi_2tWZg0iFDm8Nz%v z)H37c7`pTP2at%sQJnZ0%)uq%AA*o`A&Kc$d^_;bdE-3Z4Yv=~K=LoovP|jy>6OZOdR4U?I z@!cybsAPg=yN+%6_8zhvj$8qy z#-9uDV>51>6oBr+`!h+GQ|*oqf#xuxzrY-s&$qy=?_4a>7II6_TFJ~0YG+s95vzy) zGCnFb>kVt}Fq_%dj6geV-dLQ`MB!b9FcE;7qe=q2wY{Q@- zZLIvmRebh-*@yPCktfUG-d>7nO|)qF=DYBJ8e-vH)FBWk%~cQ6s5yNQ#tDfhlTuu6CulqljK^I1L1dB;g z>NZ4L>j`lMX#XwS(1`mat{SZ8%QJ^JRuunQr48FX08b*NwzZO6ia*T4-wG+pyTAiX zrBks%UDY6!v8eeyu)(~D7K&3A7B)lK=1&J$*M-Pmxk-4&Vnw?LC%E72L-!lE9B-1> zwqHB{C0kdX1PEIwb9xd9-3S6ICYpF9$>G#8)=Pr_iOPj?BLrxz$9*JDH&^^xJ_m7T zpCd9-IJ~jRn*)HqMTK$~sr}uT*k!EcWYf3i@v?T^;YS!W-iQw?*JkYrhV=p)Xs@Y- zYCF{KbbdiJk7j=2VRRxv)eMjM<2Qz1DCY(zpGBM^MMGxEy~toC!8ELO`M^8YQ!0}PeCoF>pQ4+WD`|u+uR)O#&Imy~5T}%yQxVi17#kD8 zn%yg(cBI_qcS@W-c6#oL-VUn-*AzVlZBB)qcfh52iM(t_{3atfD`9-pm-dJG5BMyd zO3{*hxS3;W zY{Tkd9$TKgv;{VO=871}I&r$-hj&kgh@cH5} zbW+S2+&vjV+o4yCw7UDK zF~E}Q(4=6xwUooykDt@+LJunnFLzL&e9>5gA=FIpx&ABLJGtcl680ao&mWk}Ff9{{ zy%FjVMEP4AN=yD))w~=I-kw!0*WSO3TjM5Dxmpj=xS6Eg0n@A`(1{&^$C#CujQ~p> zy-vAV)C&??(KxNo1qqU5S;d+KH%+GP4Pgl@R|dg|0hwxSmzxY#urU@Ig_j$Xd5UDy zMp^i#7L2(PKi@5lJmHCu?0V822?@ilizy^-cdo7nHrYH+eY|!X`ajy-d87VmrD$(x zu=?>&0~i;H?X&yO{9=^Kah;q->W}8S_L+~&GRca?Tx!+n!&(aLb9HS1=>n|T;phNz zAG`-uwG1(1RQwa*AUc!hng21V=&{NPp#b$ixcutbz>dS@aEI5{eeIPOyhqw!s9SQy z8J#Dmw;aJHCCjS7M1=RSzd{|M-FH^(lU=A#-xyS0Jb>(`iCc((SijEo`vN zTq7RKd7r$$?PokCHr5mZ_y&OgT5ni^+q18u*dVxx1-g*6z=-+q~4Qrc-A+A2}b6>z|*V2fJaAV5oWP-)fL zu_hzp#JR{Y&ZSV$tCw19Ls=K5%zPb$)0F9IS1agP2rH_h3fu{zS|H;=aA3~ej5 ze~4w#pVQFLek8SQD~(W`JY%~7P8tB5{)q?V5KCXGvbd5vy<-*NQuLfatT5*_LEy9% znX|8TGujdSZ()4dW$g7;Zl3D-22C}7e>i5@$*wY9D`P38!3Nd=h=o=IyePT@!V(CX z(-5f=c-p4JFviNm2q3=~9aD~WsFS{N-E0k5ur#FIKs5Fw(AnB0Cs(~Adp{EVmcpf_ zZuLe6L|p{M=St9$QhNMS#w$NV5fpC32t!+cLbhcF#P>7iXOBa#?HIu#H8KBEW13?-6rLM7{jrN<$1{{Jgnjlqs{sF+ zm5YcGJ8=q%!@2B(#XpZ1TN~d>>u-v+Nzt~PP=X`IbZAA$wmydE!){2!2E%GRHqj}A zu=tVh8GFyK((hePR&2ZlTZNXvD~T5p(k93d;cz*cKTZhv;kmI^zO+B?p>IbV#zlh%%c1zK*t&pluidvJ`pr0syEU*BWfc93fJQ^>88+ju6JLT30Q4rO$s+(l^bDY`KO2R}jq z@F)`gH|2yl@!8-|@~)jlI}U=!h$!v@n!A-T<_)z$x#Cx7GZ04~_^kSSUWZRG??1|c ztl)e^Db^79i@xzsR(_d_X3EprPM`X#Q20mQ8CiZ{3un^>)^JY_56MJ zTb!c)BZu^Z)S?hYibeLJ;ZLQzp8nJj?Urtpi>=LE^*i?&sO;|an|U`=%SQ9BxwtU5 zF>Sh-0r4njL`Y98^x(U=FT<3|QjDgbs3<`vmF#MSg{xuUkCosXN=?n5nXc8K{*PG? z7;kPWk*b@iqb$vz<MdESN>IA}A199b0}6y= z37t*qaBR4~0o^mPHybrg4ng0^eGpvUtxfG8dNoM+(2W&LRtqti^e@*|Wdzh}p2K4C z=hXpqX1F>ukKbUo+8mwsQ-z#^3XytI0&=U|De9-nQ7RK6LinksP7~a(70RXW;ZPwQ zgy%j8mTWBh+q-U?CSbasa^Ba!@qOSf^bdm2Ju_(`<|CsYgn6gP8okpb0YG<2Xwz_P zpy^5D_lsa`gxVc|1uDSq#tnY)!BTU2ZWJs0j-rb&1Iyx|jApdG1a^psIn{Z$5CVC* zhjeB`59YB5(4 zm)FRWb)FwDrAkx%5z)Cn%t-2!v4`K6TGyC#qc1b7xz3BCCyTefAQKZf5h=ymT__)kb*tgl_m|v2` z0VLk5>|oop^KhME6tLZ@e|~CppsfRfbOzlj`n`KNJh-cJ1nT1kvYdMBnjLh|<|=|C z8Wo^{`=?UPzZk+rAT87{RJ-9DaZgYa2u7xH3l8s*s^wuC$gi^BnPbP_6qT4JJPW$Efj%Ilj0&#EF`Zo90I|5sc z7}=ImR6dKWCqY_fy_^7rGidv@>yodgHqTTYO7ER9{LPmd$xn#++(nu^&vEJnZxyFO zuxPH?uSltziN$lB&nz4xtqZ?V#;IyWHc$Y>~&ATJy z^>+UK`d^MX-3He4tWGibin;Ff8Rxa3U8hqK)vcv=7BUa z7KF$h_#aB9e7q8QFvl!i)fp1aL^4Q}AAEhXMeqNn=4&<2LW=3G@3hGJ7VWeH(Ml5~ zoe^nq0|b@kQFOi*w5=xIE&R$1K2l%fNXVOQ&t%C8Bz&ZmhSujuWUxH?j(K$cY`0e} z;jkmsfw@b8VT)ku)~?2)wQ|*e{p0}5Fv(`L*o_5uwQaw!u|e<)y_(Y)HqjaMnC~@; z71y<%`7ai!JC)!D8N&^5VSX^nDX&T+p_ko#r6ERl{fGK~niZ^dmKq^1x4kD$U zR`-|8}2&T|i9tmg{v#Y!1;l(d{!Z ziIIM+UaXo!M!rcqga3xyjc-Mt=O<3b)^&$W87sykrCELxqtFL#&|wD)!snifQz0DZW?Iog}4|0 zWr#IKc~*5Mn>0>+oZ1S1%Q5%r&sc9$QzHFnXr~ucnE7J2_O#U9!u+9Dn$QQ=#kYQk z!z<06Mi42+iw?I%5Ol=>O65X%G5-&F`2{`k`=lqLUfGD)!TTAjZ$%SJMbF(8AY!3D zxl$(hKIL!n(r?O(y8?;x7!^8!YahM?wn-K)ya|_C33T6L0RRR@@ddCaM`>wesO@RQ z0aaP`$N%mr5jT})h8!S&fbl!9$%hZ?OKrc*pP8zuIpB=KA8Om?ViKULMXtqt-S|Zh zk`^)^NSlgPy^O6a=|z6L7y3~5#W;AIrr=XLrN&Zg6=mO@zR@gw6JzHcHCf)Xt?}gDy;ChF5^E+appnC!Tv@Bt$!J>!@;9G9&X+W zzs&OeB4P)E55dJ&g_eFwRWBv(7bFS;gQR!duREiE5sl*;-JHdduMsU-U#I^e*rWus zhTNtb2@s}Z^H!_uF$dGyiJ-?gUHy|K=m!-cm$5=|Z}ddUZ7KTG;O|9FZ&TIVo~V6x zHX5Jr^5%V+#5Fznn;l}#D~Dxb9jlDzx+RCUDUaZzn>8$9XkH>8P5bx*7QcRb)o}kg z7gkx=3k0~A1)>w<9R6}Jwy}_Y%mV^Pf}0P$Hp`>CZIUD2v*TTxgtW{>DGS2ETw+pa z3~dblW`dFi|3m))6|O*R-DRO@x+`3o;QSluD_&hXaG%tl_3rnCoAzwa&H>*d%7#z) z-YiR5nn-8tb$(uu3L5sSId6Ruxj@kB)9RzOnDmaxpg?0twzWzIiZ3IsJ3E$GiPS%? zg%Lw&TY_LIt9aT|l)uS%ZAlb|BTSFgJ`iNK{o$$%2XC>TU4$v*^?Wr(xW^WZQt z@lW4-v8%ox|1&@%h8|tOISC80%C1HKO~mP5J#vD2w&*x48)X?kVEO(`$}AZ4OekHe z-aFq>5SM$bUVz?Bf2y5c*B_OLz|8lGAGO|i$yQ=a+Q8%w&n$+N2pFdeT0eWKE}?Q_ zjRZ@Y(0rHdOoRVW)EVPyETH2gX-RQ$c}>TUY?R41xY4>Z8}izkOq7pWSy!+ZL$ARE z$4}-%ec?3nkd+u8@TL+Q>6Pu}nSFkn;~0U2ct@-dD@~g1v2ZeyKh`M#S41qKQ>hH$ ze|DUt;2E=HCBg~4vmR@$+Il-7I$|#5Hg~^D!eU`~iEiR*<_ThIu-PR{j^xICVV0Jk zaPW;p-3boXpFn+4tpo{tX4*mcO*Gc@>mT(U8T3$RbIl`D3@07?LG4sQ!Wr}jg&fXs zK}a||Da#WsKJ~JYGym{RO&nCA9!T`3-F7Dw!j>mQ&|%{xh6EHkdh8k8C`ksGRMy?T z=f-b*y(5KQLRS&G@zO`{J1J2IVn|>}urx7|WQX)U;iau2o=u&9QCwLrgT|JRNKuA)Nm zHhOQ%E99|@jF}HUH)LlPY%=e`_`Z?@k%8+S;->lLCBH9aoS5=H@#PJ~bn~P) zmGcjRxTzWj~A-xnP~JCrNN9h?38hS z-fvx0_#KRe?80EA7j+~oGJUhsuTZs^gFbaF__-0BZ_tn91gLYOPB3*M%>Ughjer zzkyXMABu~p0)f1T9&}MO@kL@{bUjmt;_H{hk3^)}&hUw&uAeq)NHMvI>TABOep@4N}&Xh#VA&E2ok?TdMF1o!kV%tj)@?9U|Y11)M= zeTfi<0K~^TamJU5bUpboLD0yap8%ObQr;x2z#mQXG-|7Ke^3^lSx8+3>0Al&YX`+9 z=)vEj_;qv-1>3pg6FG?tvFg&?iO_xP!2eN;(Kew}h}F>b^{r)XhFMF+6c8Bv869p^2<~ z8Ly>2&=~{?EE)BF-}Sb@-6|pYr&hkI0nIef%rw>_k%VLvCI3&^V&I0kQ0kuNK%m2? zO>FKp#t8v&NMNJHk*n!4?@!^eXOKa z2H{;%SCWjc*!xh5l4sDC+a}%&x;Xo_C(5`n8I%%FN;r*MFgC^ zbQOIq&~K5e;X1%9hWvn$9E4XXb|NEai7oyd9HhQ=&80Muop3Gui*#P|t3~NNh?wHz z?m8HNQ=#UXuI!M)rom7F`pNM*%UcRwwYjW4c<@y_Jr*p$bQaN8Jg ztNeJQ)*y4wlNijj?O#2_@Px0id=3B`m=;$N-vfu^h7zrDE{Qgi_biUszvN0dOYfqk zP2lv9r%&LrpYz>v-{tDT7i`0Zpo?1b{cGs^SZa-XiB%jOxMSLwbze?|gno@{WTL`MdP~ z+33}89|8mZA5UK$7Dd#)J-bVHcY}a*cPx!4C8acy(p`cq-6bW`pwcZZAxO7KDM&Zc zUHgsi`~JS`nm_iRopa{Qnddxl-}m$V8*0>7`f1Gx`!|myjvJ)+P~P|U38Mr5IXH{r z&Ie=MN{>mU1Q=8i*>gk@0fTof*pi&W$<36jvE zw+yYk9JfsSCXv6yjg99Y{Fj@eOrv9~6wlO{s?H*0n{EM3xC z*E|Rr0`U<0SUDH0N70i%CvOEnjOT6vMj|Sn{CP*z87USem5Z@4u0JKmQt-B9)GeJW z^~6;FYvf1+*S@U9yViJ?1cbmE{nukiM@M9{G}wtCx#zg$I#KBb|Aem$>E`o^L#9g2 zq5~$B+1rj&Lh`}eRdTE9^OxxG@vFmjgN#M=#lN)AxSbi@MW^DOqiKPUohrR~8Q~5| z2F+HM$!;+y^GQA^-_3^VMr<=*%W>k>?HwfSv6tjaVH7G5N|S78cRgTSmS~8#qo$d` zWTDr1s~w?3i3B!xZmLYOU%(yq$8D6nz$9*VSSp%0f8=U>6_$b$O$WR2T;`il30?5| zc$~Jx5J0<5?YYkRWg%_nlE&I|ND?vvA()D%iywcp=XS$%u1X1FlKa7u2r)TzWtHe8 z!t-Dy8aI61PtOiBbu>uR$fV`hw-<2zds&d0G~xf9=@4t@W%5y8#7atI;B2=j097$d=q5IGQ-fXp7<;NoW z`a#zs!9j$(s3K`$mbSB8GvyU*_btka$K7B~OMlD4r3n!#(5v_7J0wZ;p&)}UNosZ| zAU8oP6K0-`gOuvuDwL|j12T?(v)~~dWm@gLJL-tY7xSX-wc2G1-xRHmD_D?nloK>T z(-y&LdT1YX4bn!nk)}CBoy{^B^y48s`*-}_FKpemQd;5JJp^P66(gg6-_nn70!jPv zY!Br+x5Qrb@YzND=!3}ZKV536dQm(IMLGGBUlk>_+W$x@ZP*)DmV^1d%YL&}kI=XM zo*-E-Awg&p06?dy@1ypB7g~_*kTF+V=Fjso8ep+`+vMX0tuoo^C|`?rTlR`j7Ra>G zK)B@wP%7DsO7pk@XO*hyV!*qkWp7T?_Ov;8G}~n$@vo6}nau5Ff!{15@HJu`($-^? zmIlrugJ>AAI=BO+1u#>-#272fd?CHDjS$Slbp8WLBFWfKqSFZa`p!Y#-9$AZ)9O#& zd~Ow5|KXqnz|bKxz6z+mP@8>!H{>Q13rSaYV;M``zkV{F&$^2pwi8~+*5~t*P?X&m zf75;=U6Be5SHN8fY%Ag%08uX;HMR`$OcwNA0V`SCF-SgTWSsqG+mAmDhQ_?=Owve! z-}U9!pYRY?vh`vBQRB0#>*fN-1j>|qyeakpF;s!w8>UR6^91XgwuGQPmMyDI!%ULr zp2xk7DdOlLzltHw`g}bfEkh#PEx)m z7{JwQPsA=rWbqaqE_xbT@*4t3vvt+rA$s@o%R}mZ5!8zVbvcj9+yR8>!zQQ}66Yb= z$vUU&E9c~j$CfEI+@?0vZ_(46@P*Mkuc zQ8zL#-ray>Suyvb5h}2WhX{LOmQxmDkVcVF01h`LVvv=F`>4Mh4;|jk-uI`EXw6#$ z{8_4kEBl81NM8omL0=?H+|n;X+?km~3l^ew)xWGOfAnHLQdt;@2MJ-{w=;Vn1ZjR# z8CSdMwZ7sVEU68c;K-Db1~+bTIBcoU+{--ymS_IEN{gwL&+tIr|54g$yt|q~erX>h z$voo0HFessv>TGd_o}iKbSL5Tuz*gNl>25}%Yebi%ceTcJ#lpS3}RaxK=H+BRtcp; zp#f+RLok8^y_~`u72UVcyD-(DkXWRO3#Sv&z1aSx~&%m8x$;H6Fc_u34EtxWO- zw#3)U?W^ezwk-;bZ|4N}Ao?MRDvJdeZ?22oEw0m;E)`$EDt7jiDvWUhh{dqQsIN?x zeiw?9JUKBn7Eq5P=p)>dbB z4_bbRdJxuu9_a{qp&4WBZ?`vB#F*N65XlL@Mok2L!ha!y@_$^LmBFpcj0QgrePket ztI*=uBnCca(u5EfzLGCHQ1CJc#;3VTcBIhy0cNUg~a|gWqC!R>_H+Ke71cd4rN-NXZS7mHtq?cEQ3~%0 zZVW#s0TCr3q58$hmRI4(;@~!vGr_Ku58AFc zgcr)b*o6iVB>B<`gH3u!@WdX~zeM!Ei*f|aNh{x{Qhuy_hlM<%$Zi9?%%I}U&JASv z?golWX#S)}66T3#r2*7(@dyl&oWtIK%MV_!{`1d3N13HqMS_i|3C$J6%W$T9v2Gs% zebMrDCi!OS?m-L#2z9Xj(y6r^#N(6k|HgcZ^)IH?#?Kmi?3~j5$TlYZ4KcS5<-lo@ zlG$wMBP2sD0;)v!5rqES+^m zdPmo3AUZ)1NH;a%=MTCruL$VErw$9Uu_|`oXgqz+HS&+~0X}c-$Cv(~QRCZv^!iDL zvFV8h@Ls-9RN>5InQ-*6NI}#W1Tj1aN`vaV7Su=;oKEz=e_0K(ZGadloc_LN1E8?h zCs}B=4^$QV>o=suG9lT%neYkVYFlu}RzufZdi2iv_!06usEPBqochP;e`igB&IRoP z2~3ZNFEte=rc@D~a*IthdPsjlQ@|KUinXad$_w*<5BU6_6A5yi2&j{-(LVb?6$#pj z*JXp#MjtnZ5niyZ?sXj>TfQGc4e)w}KC0{i{qLf3k*4Rux}ul=)i^B0h}I^17q?@I zr%X$31-7Gc?X7*&32Sk|T64}#?vps=M~$w&Q!2fY{|<@K^W#4+1UJi_6RE`36&ifV zbdsukE^tGb0O>$mpk}8+4$5x+M^FZ*=e_U07vHz+7!}|9eU4WGLWg^TtN2jydOD6M zOl5K>AS>YwyOz)c@&7G4|KtmDpohXh4f~6i9IA_(gQIVG)0$fSO98T038d&-&Z_T7 zpPLV;+YPkFE`CH3KL-+i%m|k>Ynjv;vMUs=g8g+JKKRG8XafOD%gxCd*}N-0&?o_luEylvNyR_n$~?xad58HrUeX}o;H-#Bua}QZI?FvG zXz;Zj_M-2fFESlyxo%bpFzP-l3ve+@VsR;pWKG#^tLz@qLk}t<9!QS1y5zdr5pRIY zxDC3lMVt9Hm^13b3nl}IEX+GKVcT1WN`C_KOAwjYE|{26R7Qln)V+LzdCN1v6y`+Y zrbY5SFNE?PLNG=E#uK$FaPV%u9f2iQBskP8Z%U-3NZ9uc*BT|_0-HY(Sr*b1# zhitFd5NQ2Cxko(mSeVa(xZ($2N7Z?4XX|;x7tUV1I7Qs)6$ks*4f(=Y<`S$9 zIlZijmBFOm@d9xg{9vT)nUc=2>qfCaVo)Cgm-g8Sq+cdj-sys9bO#38Glz9r51RgJ zHn-o4$zP_y84xHauF4SDG>HG%jfbtQ
kW7a!xJ#5ycU? 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 blrSCxZI7D Date: Sun, 28 Sep 2025 17:00:56 +0200 Subject: [PATCH 34/84] Add the "soft_retro" theme --- project.godot | 2 +- soft_retro/Righteous/OFL.txt | 94 +++++ soft_retro/Righteous/Righteous-Regular.ttf | Bin 0 -> 40892 bytes .../Righteous/Righteous-Regular.ttf.import | 36 ++ soft_retro/art/checked.svg | 48 +++ soft_retro/art/checked.svg.import | 43 +++ soft_retro/art/slider_grabber.svg | 45 +++ soft_retro/art/slider_grabber.svg.import | 43 +++ soft_retro/art/toggle_off.svg | 53 +++ soft_retro/art/toggle_off.svg.import | 43 +++ soft_retro/art/toggle_on.svg | 50 +++ soft_retro/art/toggle_on.svg.import | 43 +++ soft_retro/art/unchecked.svg | 53 +++ soft_retro/art/unchecked.svg.import | 43 +++ soft_retro/soft_retro.tres | 341 ++++++++++++++++++ 15 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 soft_retro/Righteous/OFL.txt create mode 100644 soft_retro/Righteous/Righteous-Regular.ttf create mode 100644 soft_retro/Righteous/Righteous-Regular.ttf.import create mode 100644 soft_retro/art/checked.svg create mode 100644 soft_retro/art/checked.svg.import create mode 100644 soft_retro/art/slider_grabber.svg create mode 100644 soft_retro/art/slider_grabber.svg.import create mode 100644 soft_retro/art/toggle_off.svg create mode 100644 soft_retro/art/toggle_off.svg.import create mode 100644 soft_retro/art/toggle_on.svg create mode 100644 soft_retro/art/toggle_on.svg.import create mode 100644 soft_retro/art/unchecked.svg create mode 100644 soft_retro/art/unchecked.svg.import create mode 100644 soft_retro/soft_retro.tres diff --git a/project.godot b/project.godot index 332cb88a..3c69d8a3 100644 --- a/project.godot +++ b/project.godot @@ -40,7 +40,7 @@ reload=false [gui] -theme/custom="res://Assets/playfab_theme.tres" +theme/custom="uid://b3klx57slj0l3" [physics] diff --git a/soft_retro/Righteous/OFL.txt b/soft_retro/Righteous/OFL.txt new file mode 100644 index 00000000..4f26bf95 --- /dev/null +++ b/soft_retro/Righteous/OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011 by Brian J. Bonislawsky DBA Astigmatic (AOETI) +(astigma@astigmatic.com), with Reserved Font Names "Righteous" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/soft_retro/Righteous/Righteous-Regular.ttf b/soft_retro/Righteous/Righteous-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fc9c0a84cdd5eeb260f6a4559ec2356d3cff816b GIT binary patch literal 40892 zcmb^a3t*JR)d!5vJiD9a8gkogF1y*?Y&M%svPsC2Y%U}rA%vKO5N-hiMy@fU2ndJ> zh)Uz#QbiCEky1qKOR1$+sjru!MMR~D)KW?n!CFNTC|YVEdG`CA^XzUeVBh}#-xuBA z&YpdC=FFKhXU@!-8H5x<3>J3^ooE_6AvgcVcS?0awEqs&8O_yWrYwBywiqF9unS@M zboTt23&pc3_-X-Oy4$Xwxp<*4i~d49y%U6}>u*}#cFLFk3etZSqIGBc+?jJa@BL=2 z5VIZwt*{-0A&ZT#BLB_!oYp>n$!)c3+6LnL_mTexH_pA~=I^I|J5GpOZWF?B$4v`n z&)oIx_`Ut+ne*qipL*CM#JcZ=&^Iq!uz1Pfj&%ox z*w`e5ak`M=ex*ToST`ND6EfNntvlSgM>kzPFQbGa=ID+pCk>nMoe|$Ubq;<_Kje`P z={uAm?Yg7#EwxO(g?#X=?BHj$C;2J*9^cgke9G>KHep2_9O>>HUBR$&#b4-mL>MD= zrnnTv9IxzF-)pY6`m>Ufvi#QSW?3-3)>fKhvE-E6Y7Z`3U-9@a-e_O&=CcnKt!Z8O zVCmXzznM90_j8-7ASNlS$iG9&U&ci$#t5UM&|i*Hauj#E+YxV0(J38C*~oX2`rfqe ze5c8RNhmWrNjcsA#xEYPSikK4%~hM8+dXaOZ?>&1eQ;sxnxcoEeUnOPl%={2`ZWSA zAsrA?@IPgRdQSbDw8&_gtbQ&_@g61J>M`{^ezP2s-P=R5d&82Iq8Qq-Ta%VYLYgzw zW6F5x21OA|(NgD;8btDv(WrzoHBvD}2MdPieEBJgNq5-WctdIF4UJyiwd%>PH`P-u zPaa;j?C|CmrA*q0PT%93x1>!yshdvUv}``S46WILb`U5bUN}V_%7DzAs1fB#;!DvP z9Qox6iI2&lq(M|K-8{9+ryQKRd1-ZgMp3G>D8tmbUG7R5=8lhd4@*(U_fFGuhE4RV zjERjSOtF=XlU}Ch=y_?2cNcI9vhoK`>Q%!o;S$0XDV?b~dZ>urmmhC7=HMf$4=wTK z7y9+;obNl@UVG-gnFR$i?|bI8wvO-TY^>PXdfkqdwY4jET-Um@VqNoe?W?xe>9J{( zFI8VtyW-i_)@N4MUQ>;3!u3~B|0tGlT%=yBz21NtXUS+EDsNDHt#@yIdA8alUzq*! z*1KDM>dBEypPIUM?bN51j#LiLe);J&tv5)m(3TEE>%+dnu6Tw8nq-z6gEeA(=~E-(P})e_NXrbHy53aEFSRLUT8-p()Cg9EYNS1h z&r-yp0m!DnX0qO;x}#GhNxUH1p`0WYuC2A#rIY^36s2t0;U`;SWo7j4jMLH?f`cy zgoucP9%l3R;jZWC?uAHF2aGqDgKKn{h&c6>IjGX=$&hku%F^^$Y3aHqQW(7mb&*cd zJ5Apf7Zw(ia`mho>jJ9}z#!CvT}L5ppW=}=-3y&fhE1Jgb-#ixqvu2@hMgi=L61T`iY!Xd zW`)fM*{u8&J((3Jv;cOgo$OLO?2>v?DIfdj>Kp1kuu8Y8C-p13-V9l^%NR*K^*7x4 z=-69fwWyY=R!f`kp_UF^L~74!vek+WwH8|n^x!J>m7S4^gRZPa=}k|x-qp9d5U!$x z>_(`|yxunNonJhZt8D69WzB|YHMWBz)G}x#d(_w%>(Ci~sM4-cbIPAq53N$)xO&Cw z9=cXfq^sH-* zvMNMXPm=M{ogobBZ-G0Wo+1hvY0X?$!?;Q6wfcv_2mFDMrBY6)r_^!!73z5X3bF)v zDl~$@FVXsV4)yNnV8bA(KTjhO^=+9FYICwAL?IKPySDRZAx%N2m(T#AM;{dtLQrQ% zkJlemJy4=7StC=V&9LcG8@j|u-D@PR6@fG8#ha)XYvrfAc~I%YMGuA|;;6<=>q<)> zhj_DW%xZt31MML!)59vz-St$bpzB>*FAkpg|vlb zm{M`emh0r+gn8?JJ(~uOkQ+eh*}q;lF9A72hxFvBaT}7lN{7DV^7G1^uFd-9t}V(e zYDcz)cfqgP3%?3ay6_zRC$~vk*YA`O>dEoyNuy6aMRo1t&FFTk$GUd0HzP#@@_10o z!w}3vd9Z7*Oi@oM6$hnFe^J6;Rzz;#jDD$pib$YVb2#0RI`nY(Aey&h_WNX?{;gkH zHP1bH#@u0x_OEYlUcZ0Qu(lbK)8|#G-(=OzE-IRRO=jjb^j?>>&ROP7YIx+sdsZFZ zFd-?YJZ*K?cSSdh8=77>zouq>UHZ^*HxLKo!NEquF?dCpA-&P`wp>S+o}7c8wp72d z`LX@CP~kErzT%oj*Dd=WYYr7&mzl{-)MY7-J3d%He&U8hD|LI+L6&UGkVzX4tth{- zF&o7emCqw{I1e)PqVy=#AHKRY=AiN+p9h|@xwkO%QSS^HR@smds$)i7r7!B8^x2D7 zj||CTsk(Ej?78OnQ+UfqFHsB@t)GO1BK1QZId2*d(nzNJv}|OvK6(aau=i4 zKOlFsc;9+2?j!fKG1rWUm$9L$_mTUg#}D1U{DY0ngOaoD^1IGj5`0GeP1U9InsP|+ zTFb|v=2nq~x@*or-zu98-EN>+iHWjDU9Hl0E}oOFJ{0SUBgyED1NzAbgDzZeYJ=4u zJ~rMyd~#u-QSWKEapaJ6cUmNAuowP#YeWU+YGl`((LPceuX= zeehs*z27d!s=w0NWR!3AuDj~h^EQ8dwhvxEYNNKX9>)qlGVUWgwnpw~GX@KP?>2>k z>t^*;Tp3GxegC~9TFPAmqlRBIHZ|0~L0wlIAw3>>V}l3cRJSa8`>|$4Mk|iuI!}_r z)UWZ84}V1RXT3*F+tK@7XzwJhsUte*#-MNeLd?qL>Ny!bw7rn)_KjjMs>{r3D6$)l zscx68e$6jucWqYYWY!ni?M3yOsBX*t92;7 z=Ua2tZ)B`te0mj;s?uHBSC{VB=L$tVthUQtoL~Bt`AHTZnL}r!!6|nkN0%Epq69Z( z)8%~J-#RKonic7;owNMG`YTHL{vR?{@z&2O)a~eOsefqs?9p(hHOVwUG9fRQZMM!R zR}ic6aap21C134p~Z4=bmP=+g2!!+YUWGdGh0jZoB=1$D7BkKYVA^ zjbl8XF*jC`iOg)ci7a%x+OB^=->lgx^h(r3lNq^}7%kja^f$Cb)qK~K>)OJjMB3<4 zBW;Q^t8NaD4cz7C)McsdhW&Rr%e_hC9y`4HM@QB*MkS}l$Ya$v)n|~ks=TQolbE3X z-H!g<&BGPlxGxX-rRq1f%2A`!!hLt{jLGh+jZ@J2Ph(P(qb6)Pe8=j;kBv+6mOFz( z6&b5h^XuGXFAlou zBZtr@-I+5#%m3d`Drz5vU>C6RH3;?aNb2f?>; z*X&(u>N`4Ql-#1uB32KPnU&=r>{KVJ=hZi}>WdsOuE?zgqm>yoPo~+g<{@Yq1!s`a zPTdMe{rJgV%2P%;it4k-PRZ!1RdChJHFy1XvC`D_LgA#66!(a6SxVF8!^o9a;9EbLV!=Q8wd2JlPsJV|W1){O>(W!wVx8te@2IP;t?NVF}5j9OiM!{i-R$KZy z#t^NXw=tuxB5#QI+T1>}jFrvn-(Cdy#41TS6|UIykW`B&6AjqOuU_KcL+|PLMt8aN!;23NigHi1t2A@I#nLZjSbBVNZkaQ!A~&fm zd+zM1wg!8#H6q1ZY!77ytFs8)?AJA3-QLn?fR2W#CJIHupPA1_9T25=A> zGJ`9D=HQerC{a@!R(%V4(&EhpzGz>$bit%)rYmM9zE^vAC z>YYnIcxw9OCq7(p`-e|VwmH=rbM}zpYpc?nRo9jb$u_^TJVLK}JUo;+039Xr8?3!xdFaMnw(k@2bAGsJOM-)qkK@eZ-t; zN_D$a(=sytKi3NAm~K3D)J*;CDq3dvZxv5&qNcjZk_>f3Q?WYcBwt+}omX!zDP|}R z`I2rK^_afeB448B5A_&QG38)L;er}SeQtvisZ{A+xmpfNRcc9NmbbAa74P0Gyepq) zkEKszv$Mz0`xwe$h0>s4t(&ZfgrQJQc5MnqGx$40ZTEME+RMK1cMZA@xy`USC*!?LxBXli$_a<*G%P(qN%DN}lFL)cK&frL^@ON7$@*MU2|EkIMs}-= zEKeCL$`w>{^d97f(Tuc4MVTFEQBt8ZW zl3VT?IKVl)$(uW|!etz|(_lzUFempj80IIYC;94zCRmHc`r;h1ktva8dzvXKA&P1x z^mS4g8m`t$u)OZfNlMCfnoQ2zq@)~Yoc_J!TxVRIGdDR%B|lo|t@1&`bI8Gl4EtDi zBYZ;556mx=584WAJsGuyHoRwe@ZOQ0Y)MPAB&X|7dPW!6Yz3n|AlvPDjNHN`my7d{ z)Wt<8nxtagv1WR$S@Zqx9Ub5QUSGOe*1n{^xms4gM72@Z$rrKOg}F&=&=ZA>pqeBj z(v1Dqn&BDF!pfA6$m|SjTy%VTzlZZ~Q`aTr)Og+1!-vHW&MzsBiAuD_t;wT_C~B9e zI1M{!jbFN`Wo4rJ#iE~XSfNi;*FO5tz5I;olh0$_%8pWVB!Yx~1PQUA&mM8AG1eFA z9edKMl2VfVW^-I-bi6e)(VleeiacA9C&4j%!cf(xBn^o%ZHOK;FveoGBt%Q)UcJRz zmX_aGU?EO)XXQb|M$*4nw=^$WBt6@8XV*&;d`W7BQl;HcidKL>-nSLVMD!ffmG)SN zH!&$UH7+hSH!0EUh*dXU(blg}ixV|5hn~wZhudDGYtqS5)*0^SH7=@iNH?`MHyaV2 zmC#eHcp>}-ak!OL5eB_6#Wy;`J7Q>7LTtj|fv*neXB=?TwQj#BB{4B7Iws0+e~c+6 zCnaG2yJeeIDWkVKuL zD*Ft}uPzB=uW`RzQTF9eUQ$`LWKw?qq$O3AOD5+ZjkdTk54J`{Sur1WTcY))l}noQ z^PA~0ey@s8w?s!<(&K|q^==zziIlDCwjI)~JA3S~`n4t}J+$Z%(xPaW!RR(hblgQ- zAJ?x_p+!Fi*ZttBdSGbcC~ArJqbMSKnGl_eQo)b_B8mo&7AI7jwr{6+BE=57>UMA} zk(QFzh60L8gN#zj`QdELe2#?H;jyd^p3B6mx&?;C#8ZE1>@UsoBe`*Z*W=2xqq;%r zk3UhWmBsJjxjA@lgZ5mkzrS?LIY*UgU61dV8}-k<*Y$AMtDneQD8E`|v3^0s4na#` zvBrVY?MHusQFEDfp7Wm`J9+fDy01gUl3eU!eQ`j%Ein;%Tm8+s4%wu>&~XVlz#!?j zLI#7O;rO9H+6Z(2UzE1(+m(Zzaocr~%0p`86!rWR^wY0U&aefKYo z(R*a<3ooeO(68R>I?$?~zpiUR_&3Yt8jh$_&(=8#V14)M)bm~MVHkHGK(-VoiBeAM zV|qyFVcGJ0*LRB8qx4hvy(Z5paXazccoC)Rr#+`hX#7ix==$zC*`jOP*>y&Zc}>EW zD3qU}iJt?Xn#_Ze59ccfURi>=EnF7Xm66ASP##=bf61*9E>jM88Kr;Qt904g&4*4j zi?g~d`UB`WW?m(QjQd)QzOw+`md-tjpTak`!VI%HqtF%#`Skp(c|fX@X2)iV$7}D( zrfvfLxBxP1Q-5a{TedzTefaSo2`;Vb>sE(Fh_5>8mA!>*Nf5E|F%NK zs8GLPg>q4$vU*JbCjP>cT(qcqP=Ztkm%M}IcOY&E9)VZ4iqCgg?a~7%#cPVQhkWq4g zdQm<902v(VML*Joqsk6Qu?SLRUlF0@bhpvrPp7a9Z_e;Lbi>l!5<_*lf@zjnYOC}H zr*A@uEf_4`)#z5gxp&S|Ew(!Ce;RUS%zMSYu8ZTYrKoD>0{>8@zh}IcV$b@c`OEHE z*QkYTvorm#TrUScsRgLB(?@i9Cv6zsRp6w(3lkqLRSpb^Midv^QVfG)N68pT^NHKw zsK(sVa7Jw%Z}vN64<3iPgRutP#nLKTpsZY0n4o^Ms-l)wDXZ@&N|3Rg2cw6`;jwNG zC@3iw_1!2(G=-FO>uO(FNGq0>M8|XWuP>xUOPj^s^|y>`Y;s40krpm%%hVYL9jzKl zu~&8K`vrV7@)^)~_5C`3dyrj4=<71wM*S4Sc6gPvs^pAzVrX>YUGu{*kaQ=sdIXp3 zLS~WOS`Z&!kZdo`Ow?_ZPhEe#I$JJ`w_4-XpLD;| zx2Cu_Q(|T@Tp+z@R9@Bj;WG_DTp&kOfe#ap{s?*LXe6VZK@mo?(`}?_W>3eY@9->F zO>q@OCOTrKOnD(~M4lx(J-5X(OTJPv_r5V>?wea$s=b#?&mPlOT+%iMv$wXAV!Z2L z+Fjs^u_r{TTQObCt4PafGB2K@bcBA?c;DO-WYSb}eM5G3!}TS_bLo99Oal7(z534$ zF~T7V#5Lf(_bNc29Yp_E1$6i4-C9w1_spWAnRnM!+?qGqHr|^vZg^_y@Nqfb@izIh zXunmI z>a=g^`9oxMJ3UXoZ+ZIGQKN2snrgFDey)o!yoeDpL(_;*@H(W%K3yM|DAwdC(COV3 zxl1almrWTqY|66gid(%E?(Iq0;JY*{F)^zY{AVZa=vtsloRV3ZGx?5j^{ZO)OS2l( zw;dIEDdz0T%*-mUIb~>tI(q9?aIqL%=)nbfr&n43>D%ngg5sOHY*dJYQOlQM+>buwm1&8Ee%v-_WV6#<(lJiHY7yw`)XRa#BvE+5iU7FR+WixP$#G ztRV+?8;}UdwBdy{LTwX7v~fqLr|b9H{x|)1UA2n*NX=4S>h!3e@iwCH_BY;eby7Wr zjYZ|!<~Wi!BG1NMupT}!T1;Z=LFP=0K&HD*R|!R%v>s^gy(Cqx1kE4sXf*8NAms>O z)B%pnE_|b|Puw&Z!>fB^;YbQxUVgD}_)j?+L!rz%gfa*A4rMluzU#`McKhfC3T>*p zLovZ%Y+E+<40f`$G}fTL9{7{>SJbz6-}!GUS~J8kB%_`x>ZTC%%0D*Lf4DLf?9e_@ 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 literal 0 HcmV?d00001 diff --git a/soft_retro/Righteous/Righteous-Regular.ttf.import b/soft_retro/Righteous/Righteous-Regular.ttf.import new file mode 100644 index 00000000..c3ed3266 --- /dev/null +++ b/soft_retro/Righteous/Righteous-Regular.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://i1eetfry6s4h" +path="res://.godot/imported/Righteous-Regular.ttf-d2f7bd177f19b40bc2c26b0c0a91d998.fontdata" + +[deps] + +source_file="res://soft_retro/Righteous/Righteous-Regular.ttf" +dest_files=["res://.godot/imported/Righteous-Regular.ttf-d2f7bd177f19b40bc2c26b0c0a91d998.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=4 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/soft_retro/art/checked.svg b/soft_retro/art/checked.svg new file mode 100644 index 00000000..ec251121 --- /dev/null +++ b/soft_retro/art/checked.svg @@ -0,0 +1,48 @@ + + + + + + + diff --git a/soft_retro/art/checked.svg.import b/soft_retro/art/checked.svg.import new file mode 100644 index 00000000..d462924f --- /dev/null +++ b/soft_retro/art/checked.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://udawlj7nryla" +path="res://.godot/imported/checked.svg-919178c345fa3bb12eedbdefa9339aae.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://soft_retro/art/checked.svg" +dest_files=["res://.godot/imported/checked.svg-919178c345fa3bb12eedbdefa9339aae.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/soft_retro/art/slider_grabber.svg b/soft_retro/art/slider_grabber.svg new file mode 100644 index 00000000..d35192b7 --- /dev/null +++ b/soft_retro/art/slider_grabber.svg @@ -0,0 +1,45 @@ + + + + + + diff --git a/soft_retro/art/slider_grabber.svg.import b/soft_retro/art/slider_grabber.svg.import new file mode 100644 index 00000000..dc23d6e7 --- /dev/null +++ b/soft_retro/art/slider_grabber.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bbl50qp6b1eq8" +path="res://.godot/imported/slider_grabber.svg-7efe12275a78af246fbf5c7b8b608956.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://soft_retro/art/slider_grabber.svg" +dest_files=["res://.godot/imported/slider_grabber.svg-7efe12275a78af246fbf5c7b8b608956.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/soft_retro/art/toggle_off.svg b/soft_retro/art/toggle_off.svg new file mode 100644 index 00000000..03df99cf --- /dev/null +++ b/soft_retro/art/toggle_off.svg @@ -0,0 +1,53 @@ + + + + + + + + + diff --git a/soft_retro/art/toggle_off.svg.import b/soft_retro/art/toggle_off.svg.import new file mode 100644 index 00000000..c7d48be5 --- /dev/null +++ b/soft_retro/art/toggle_off.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://gameaex7fm1g" +path="res://.godot/imported/toggle_off.svg-ac8c30b12a7d3fcece7eb5f25e48107d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://soft_retro/art/toggle_off.svg" +dest_files=["res://.godot/imported/toggle_off.svg-ac8c30b12a7d3fcece7eb5f25e48107d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/soft_retro/art/toggle_on.svg b/soft_retro/art/toggle_on.svg new file mode 100644 index 00000000..664454b8 --- /dev/null +++ b/soft_retro/art/toggle_on.svg @@ -0,0 +1,50 @@ + + + + + + + diff --git a/soft_retro/art/toggle_on.svg.import b/soft_retro/art/toggle_on.svg.import new file mode 100644 index 00000000..260b98ab --- /dev/null +++ b/soft_retro/art/toggle_on.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dl5qlemud0d4j" +path="res://.godot/imported/toggle_on.svg-6577369a3687d758cfc6e33c9af512b8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://soft_retro/art/toggle_on.svg" +dest_files=["res://.godot/imported/toggle_on.svg-6577369a3687d758cfc6e33c9af512b8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/soft_retro/art/unchecked.svg b/soft_retro/art/unchecked.svg new file mode 100644 index 00000000..71d79b8f --- /dev/null +++ b/soft_retro/art/unchecked.svg @@ -0,0 +1,53 @@ + + + + + + + diff --git a/soft_retro/art/unchecked.svg.import b/soft_retro/art/unchecked.svg.import new file mode 100644 index 00000000..a97ef433 --- /dev/null +++ b/soft_retro/art/unchecked.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbwip815aqfhq" +path="res://.godot/imported/unchecked.svg-a69bdf3a510d8bf5cdf63fcefbc7512b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://soft_retro/art/unchecked.svg" +dest_files=["res://.godot/imported/unchecked.svg-a69bdf3a510d8bf5cdf63fcefbc7512b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/soft_retro/soft_retro.tres b/soft_retro/soft_retro.tres new file mode 100644 index 00000000..22d1cf5e --- /dev/null +++ b/soft_retro/soft_retro.tres @@ -0,0 +1,341 @@ +[gd_resource type="Theme" load_steps=52 format=3 uid="uid://b3klx57slj0l3"] + +[ext_resource type="FontFile" uid="uid://i1eetfry6s4h" path="res://soft_retro/Righteous/Righteous-Regular.ttf" id="1_lopid"] +[ext_resource type="Texture2D" uid="uid://udawlj7nryla" path="res://soft_retro/art/checked.svg" id="2_k0c1y"] +[ext_resource type="Texture2D" uid="uid://dl5qlemud0d4j" path="res://soft_retro/art/toggle_on.svg" id="2_myavk"] +[ext_resource type="Texture2D" uid="uid://gameaex7fm1g" path="res://soft_retro/art/toggle_off.svg" id="3_208pi"] +[ext_resource type="Texture2D" uid="uid://cbwip815aqfhq" path="res://soft_retro/art/unchecked.svg" id="3_cwkfx"] +[ext_resource type="Texture2D" uid="uid://bbl50qp6b1eq8" path="res://soft_retro/art/slider_grabber.svg" id="6_cwkfx"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k0c1y"] +bg_color = Color(1, 0.690196, 0.639216, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_lopid"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_myavk"] +bg_color = Color(1, 0.933333, 0.8, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +expand_margin_left = 5.0 +expand_margin_top = 2.0 +expand_margin_right = 5.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lopid"] +bg_color = Color(0.27451, 0.258824, 0.368627, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +expand_margin_left = 5.0 +expand_margin_top = 2.0 +expand_margin_right = 5.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_208pi"] +bg_color = Color(1, 0.411765, 0.45098, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +expand_margin_left = 5.0 +expand_margin_right = 5.0 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_fmcpx"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_nas17"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_adq26"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_wjpxi"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_k0c1y"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_cwkfx"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1b0k0"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_14r85"] + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_hbuoo"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nas17"] +bg_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_adq26"] +bg_color = Color(1, 0.690196, 0.639216, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wjpxi"] +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_s6qsw"] +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxLine" id="StyleBoxLine_k0c1y"] +color = Color(1, 0.933333, 0.8, 1) +thickness = 2 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cwkfx"] +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1b0k0"] +bg_color = Color(1, 0.690196, 0.639216, 1) +corner_radius_top_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_14r85"] +content_margin_top = 5.0 +content_margin_bottom = 5.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_b3cdw"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rga2m"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 0.784314) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_41273"] +bg_color = Color(0.0823529, 0.470588, 0.54902, 0.392157) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_uaw8g"] +bg_color = Color(0.27451, 0.258824, 0.368627, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbuoo"] +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fmcpx"] +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_rnlxr"] +bg_color = Color(0.27451, 0.258824, 0.368627, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mdn0p"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 0.588235) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_g2lif"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_pltt4"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +expand_margin_top = 3.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yhwjn"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +expand_margin_top = 3.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l8cky"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_l34ym"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_7o6pk"] +content_margin_left = 10.0 +content_margin_right = 10.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 0.784314) +corner_radius_top_left = 10 +corner_radius_top_right = 10 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_y214y"] +bg_color = Color(1, 0.933333, 0.8, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_h653x"] +bg_color = Color(1, 0.690196, 0.639216, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_erami"] +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_c7d7l"] +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_s6qsw"] + +[sub_resource type="StyleBoxLine" id="StyleBoxLine_cwkfx"] +color = Color(1, 0.933333, 0.8, 1) +thickness = 2 +vertical = true + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g2lif"] +bg_color = Color(1, 0.411765, 0.45098, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_b3cdw"] +bg_color = Color(1, 0.690196, 0.639216, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l34ym"] +content_margin_left = 5.0 +content_margin_right = 5.0 +bg_color = Color(0.0823529, 0.470588, 0.54902, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 + +[resource] +default_font_size = 0 +Button/colors/font_color = Color(1, 0.933333, 0.8, 1) +Button/colors/font_disabled_color = Color(0.27451, 0.258824, 0.368627, 0.392157) +Button/colors/font_focus_color = Color(1, 0.933333, 0.8, 1) +Button/colors/font_hover_color = Color(0.27451, 0.258824, 0.368627, 1) +Button/colors/font_hover_pressed_color = Color(1, 0.933333, 0.8, 1) +Button/colors/font_pressed_color = Color(1, 0.933333, 0.8, 1) +Button/fonts/font = ExtResource("1_lopid") +Button/styles/disabled = SubResource("StyleBoxFlat_k0c1y") +Button/styles/focus = SubResource("StyleBoxEmpty_lopid") +Button/styles/hover = SubResource("StyleBoxFlat_myavk") +Button/styles/normal = SubResource("StyleBoxFlat_lopid") +Button/styles/pressed = SubResource("StyleBoxFlat_208pi") +CheckBox/colors/font_color = Color(1, 0.933333, 0.8, 1) +CheckBox/colors/font_focus_color = Color(1, 0.933333, 0.8, 1) +CheckBox/colors/font_hover_color = Color(1, 0.933333, 0.8, 1) +CheckBox/colors/font_hover_pressed_color = Color(1, 0.933333, 0.8, 1) +CheckBox/colors/font_pressed_color = Color(1, 0.933333, 0.8, 1) +CheckBox/icons/checked = ExtResource("2_k0c1y") +CheckBox/icons/unchecked = ExtResource("3_cwkfx") +CheckBox/styles/hover = SubResource("StyleBoxEmpty_fmcpx") +CheckBox/styles/hover_pressed = SubResource("StyleBoxEmpty_nas17") +CheckBox/styles/normal = SubResource("StyleBoxEmpty_adq26") +CheckBox/styles/pressed = SubResource("StyleBoxEmpty_wjpxi") +CheckButton/colors/font_color = Color(1, 0.933333, 0.8, 1) +CheckButton/colors/font_focus_color = Color(1, 0.933333, 0.8, 1) +CheckButton/colors/font_hover_color = Color(1, 0.933333, 0.8, 1) +CheckButton/colors/font_hover_pressed_color = Color(1, 0.933333, 0.8, 1) +CheckButton/colors/font_pressed_color = Color(1, 0.933333, 0.8, 1) +CheckButton/fonts/font = ExtResource("1_lopid") +CheckButton/icons/checked = ExtResource("2_myavk") +CheckButton/icons/unchecked = ExtResource("3_208pi") +CheckButton/styles/focus = SubResource("StyleBoxEmpty_k0c1y") +CheckButton/styles/hover = SubResource("StyleBoxEmpty_cwkfx") +CheckButton/styles/hover_pressed = SubResource("StyleBoxEmpty_1b0k0") +CheckButton/styles/normal = SubResource("StyleBoxEmpty_14r85") +CheckButton/styles/pressed = SubResource("StyleBoxEmpty_hbuoo") +HScrollBar/styles/grabber = SubResource("StyleBoxFlat_nas17") +HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_adq26") +HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_wjpxi") +HScrollBar/styles/scroll = SubResource("StyleBoxFlat_s6qsw") +HSeparator/styles/separator = SubResource("StyleBoxLine_k0c1y") +HSlider/icons/grabber = ExtResource("6_cwkfx") +HSlider/styles/grabber_area = SubResource("StyleBoxFlat_cwkfx") +HSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_1b0k0") +HSlider/styles/slider = SubResource("StyleBoxFlat_14r85") +Label/colors/font_color = Color(1, 0.933333, 0.8, 1) +Label/fonts/font = ExtResource("1_lopid") +LineEdit/colors/caret_color = Color(1, 0.933333, 0.8, 1) +LineEdit/colors/clear_button_color_pressed = Color(1, 0.933333, 0.8, 1) +LineEdit/colors/font_color = Color(1, 0.933333, 0.8, 1) +LineEdit/colors/font_placeholder_color = Color(1, 0.933333, 0.8, 0.392157) +LineEdit/colors/font_selected_color = Color(1, 0.933333, 0.8, 1) +LineEdit/colors/selection_color = Color(1, 0.690196, 0.639216, 1) +LineEdit/fonts/font = ExtResource("1_lopid") +LineEdit/styles/focus = SubResource("StyleBoxEmpty_b3cdw") +LineEdit/styles/normal = SubResource("StyleBoxFlat_rga2m") +LineEdit/styles/read_only = SubResource("StyleBoxFlat_41273") +Panel/styles/panel = SubResource("StyleBoxFlat_uaw8g") +ProgressBar/colors/font_color = Color(1, 0.933333, 0.8, 1) +ProgressBar/fonts/font = ExtResource("1_lopid") +ProgressBar/styles/background = SubResource("StyleBoxFlat_hbuoo") +ProgressBar/styles/fill = SubResource("StyleBoxFlat_fmcpx") +TabContainer/colors/font_disabled_color = Color(1, 0.933333, 0.8, 0.392157) +TabContainer/colors/font_hovered_color = Color(1, 0.933333, 0.8, 1) +TabContainer/colors/font_selected_color = Color(1, 0.933333, 0.8, 1) +TabContainer/colors/font_unselected_color = Color(1, 0.933333, 0.8, 1) +TabContainer/fonts/font = ExtResource("1_lopid") +TabContainer/styles/panel = SubResource("StyleBoxFlat_rnlxr") +TabContainer/styles/tab_disabled = SubResource("StyleBoxFlat_mdn0p") +TabContainer/styles/tab_focus = SubResource("StyleBoxEmpty_g2lif") +TabContainer/styles/tab_hovered = SubResource("StyleBoxFlat_pltt4") +TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_yhwjn") +TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_l8cky") +TextEdit/colors/background_color = Color(0, 0, 0, 0) +TextEdit/colors/caret_color = Color(1, 0.933333, 0.8, 1) +TextEdit/colors/font_color = Color(1, 0.933333, 0.8, 1) +TextEdit/colors/font_placeholder_color = Color(1, 0.933333, 0.8, 0.392157) +TextEdit/colors/font_readonly_color = Color(1, 0.933333, 0.8, 0.392157) +TextEdit/colors/font_selected_color = Color(1, 0.933333, 0.8, 1) +TextEdit/colors/search_result_color = Color(1, 0.690196, 0.639216, 1) +TextEdit/colors/selection_color = Color(1, 0.690196, 0.639216, 1) +TextEdit/fonts/font = ExtResource("1_lopid") +TextEdit/styles/focus = SubResource("StyleBoxEmpty_l34ym") +TextEdit/styles/normal = SubResource("StyleBoxFlat_7o6pk") +VScrollBar/styles/grabber = SubResource("StyleBoxFlat_y214y") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_h653x") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_erami") +VScrollBar/styles/scroll = SubResource("StyleBoxFlat_c7d7l") +VScrollBar/styles/scroll_focus = SubResource("StyleBoxEmpty_s6qsw") +VSeparator/styles/separator = SubResource("StyleBoxLine_cwkfx") +VSlider/icons/grabber = ExtResource("6_cwkfx") +VSlider/styles/grabber_area = SubResource("StyleBoxFlat_g2lif") +VSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_b3cdw") +VSlider/styles/slider = SubResource("StyleBoxFlat_l34ym") From 02ddc80da854e01f8b4b431b2cf26d757a8fad86 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Fri, 3 Oct 2025 22:20:14 +0200 Subject: [PATCH 35/84] Made the catalog scene tabbed --- Scenes/Catalog.tscn | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Scenes/Catalog.tscn b/Scenes/Catalog.tscn index 711eb8b0..3d7d7375 100644 --- a/Scenes/Catalog.tscn +++ b/Scenes/Catalog.tscn @@ -29,13 +29,31 @@ text = "Catalog" custom_minimum_size = Vector2(0, 20) layout_mode = 2 -[node name="CardGridContainer" type="GridContainer" parent="VBoxContainer"] +[node name="TabContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +current_tab = 0 + +[node name="Items" type="TabBar" parent="VBoxContainer/TabContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="CardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Items"] unique_name_in_owner = true custom_minimum_size = Vector2(100, 100) -layout_mode = 2 +layout_mode = 0 +offset_left = -2.0 +offset_top = 6.0 +offset_right = 1150.0 +offset_bottom = 530.0 size_flags_vertical = 3 columns = 4 +[node name="Currencies" type="TabBar" parent="VBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 1 + [node name="BackButton" type="Button" parent="."] layout_mode = 1 anchors_preset = 12 From c2b157a9906fbfd4226a43c93365c7afe7a00fbe Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Fri, 3 Oct 2025 23:33:25 +0200 Subject: [PATCH 36/84] Added currency tab in Economy --- Scenes/Catalog.gd | 15 ++++++++++++++- Scenes/Catalog.tscn | 12 +++++++++++- addons/godot-playfab/PlayFabCatalog.gd | 21 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Scenes/Catalog.gd b/Scenes/Catalog.gd index 527a7350..ab0a54da 100644 --- a/Scenes/Catalog.gd +++ b/Scenes/Catalog.gd @@ -7,6 +7,11 @@ func _ready() -> void: PlayFabManager.catalog.search_complete.connect(_on_search_complete) PlayFabManager.catalog._search_all_items() + var catalog: PlayFabCatalog = PlayFabCatalog.new() + add_child(catalog) + catalog.search_currency_complete.connect(_on_search_currency_complete) + catalog.search_currency() + func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: print("All item IDs:", PlayFabManager.catalog.search_results) @@ -15,8 +20,16 @@ func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) %LoadingIndicator.hide() - %CardGridContainer.add_child(card) + %ItemCardGridContainer.add_child(card) +func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> void: + print("Currency item IDs:", PlayFabManager.catalog.search_results) + for key in result.keys(): + var catalog_item: CatalogItem = result[key] + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %CurrencyCardGridContainer.add_child(card) func _on_back_button_pressed() -> void: SceneManager.goto_scene("res://Scenes/LoggedIn.tscn") diff --git a/Scenes/Catalog.tscn b/Scenes/Catalog.tscn index 3d7d7375..38f0262f 100644 --- a/Scenes/Catalog.tscn +++ b/Scenes/Catalog.tscn @@ -38,7 +38,7 @@ current_tab = 0 layout_mode = 2 metadata/_tab_index = 0 -[node name="CardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Items"] +[node name="ItemCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Items"] unique_name_in_owner = true custom_minimum_size = Vector2(100, 100) layout_mode = 0 @@ -54,6 +54,16 @@ visible = false layout_mode = 2 metadata/_tab_index = 1 +[node name="CurrencyCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Currencies"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 100) +layout_mode = 0 +offset_top = 29.0 +offset_right = 1152.0 +offset_bottom = 553.0 +size_flags_vertical = 3 +columns = 4 + [node name="BackButton" type="Button" parent="."] layout_mode = 1 anchors_preset = 12 diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 1f00b58a..4da2841a 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -35,6 +35,27 @@ func _search_page(token: String) -> void: search_items(request_data, _on_search_page_ok) +## Searches for currencies. +## Not paginated! +func search_currency(callback: Callable = func(): pass) -> void: + var request_data: SearchItemsRequest = SearchItemsRequest.new() + request_data.Search = "" + request_data.Filter = "type eq 'currency'" + request_data.OrderBy = "CreationDate asc" + request_data.Count = PAGE_SIZE + + search_items(request_data, _on_search_currency_complete) + +func _on_search_currency_complete(result: Dictionary) -> void: + var res = SearchItemsResponse.new() + res.from_dict(result.data, res) + var results_items : Dictionary[String, CatalogItem] = {} + for item: CatalogItem in res.Items: + results_items[item.Id] = item + + search_currency_complete.emit(results_items) + + func _on_search_page_ok(result: Dictionary) -> void: var res = SearchItemsResponse.new() res.from_dict(result.data, res) From ecea2fff2901e2fcee2d19126d6e7dd801f80b90 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 12:52:43 +0200 Subject: [PATCH 37/84] Add new theme --- addons/godot-playfab/ObjectPool.gd | 35 ++ .../theme/godot-playfab_theme.tres | 433 ++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 addons/godot-playfab/ObjectPool.gd create mode 100644 addons/godot-playfab/theme/godot-playfab_theme.tres diff --git a/addons/godot-playfab/ObjectPool.gd b/addons/godot-playfab/ObjectPool.gd new file mode 100644 index 00000000..9090875c --- /dev/null +++ b/addons/godot-playfab/ObjectPool.gd @@ -0,0 +1,35 @@ +extends Node +class_name ObjectPool + + +var _prototype: Object +var _idle_pool: Array = [] +var _busy_pool: Array = [] + + +func _init(prototype: Object) -> void: + _prototype = prototype + + +func get_instance() -> Object: + if _idle_pool.size() > 0: + var instance = _idle_pool.pop_back() + _busy_pool.append(instance) + return instance + else: + var instance: Object = _create_instance() + _busy_pool.append(instance) + return instance + + +func _create_instance() -> Object: + var instance: Object = _prototype.duplicate() + add_child(instance) + return instance + +func release_instance(instance: Object) -> void: + if instance in _busy_pool: + _busy_pool.erase(instance) + _idle_pool.append(instance) + else: + push_error("Instance not found in busy pool.") diff --git a/addons/godot-playfab/theme/godot-playfab_theme.tres b/addons/godot-playfab/theme/godot-playfab_theme.tres new file mode 100644 index 00000000..b877d5e9 --- /dev/null +++ b/addons/godot-playfab/theme/godot-playfab_theme.tres @@ -0,0 +1,433 @@ +[gd_resource type="Theme" load_steps=36 format=3 uid="uid://cax55m47g78g"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_disabled"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.17, 0.2, 0.25, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_focus"] +bg_color = Color(0, 0.470588, 0.831373, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_hover"] +bg_color = Color(0.1, 0.41, 0.6, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_normal"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_pressed"] +bg_color = Color(0, 0.380392, 0.678431, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 2.0 +expand_margin_top = 2.0 +expand_margin_right = 2.0 +expand_margin_bottom = 2.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_check_button_off"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_right = 2 +corner_radius_bottom_left = 2 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_check_button_on"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0, 0.470588, 0.831373, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_right = 2 +corner_radius_bottom_left = 2 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_comment"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.73, 0.73, 0.73, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_selected"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_grabber"] +bg_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_grabber_hover"] +bg_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_slider_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_slider_grabber"] +bg_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 5.0 +expand_margin_top = 5.0 +expand_margin_right = 5.0 +expand_margin_bottom = 5.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_line_edit_focus"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 5.0 +expand_margin_top = 3.0 +expand_margin_right = 5.0 +expand_margin_bottom = 3.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_line_edit_normal"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 +expand_margin_left = 5.0 +expand_margin_top = 3.0 +expand_margin_right = 5.0 +expand_margin_bottom = 3.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_menu_panel"] +bg_color = Color(0.09, 0.11, 0.14, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0.470588, 0.831373, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.09, 0.11, 0.14, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_fill"] +bg_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_separator"] +content_margin_left = 5.0 +content_margin_top = 2.0 +content_margin_right = 5.0 +content_margin_bottom = 2.0 +bg_color = Color(0.09, 0.11, 0.14, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v_separator"] +content_margin_left = 2.0 +content_margin_top = 5.0 +content_margin_right = 2.0 +content_margin_bottom = 5.0 +bg_color = Color(0.09, 0.11, 0.14, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_bar_bg"] +bg_color = Color(0.09, 0.11, 0.14, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_selected"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +expand_margin_left = 10.0 +expand_margin_top = 5.0 +expand_margin_right = 10.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_unselected"] +bg_color = Color(0.17, 0.2, 0.25, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +expand_margin_left = 10.0 +expand_margin_top = 5.0 +expand_margin_right = 10.0 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tooltip"] +bg_color = Color(0.17, 0.2, 0.25, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_cursor"] +bg_color = Color(0, 0.470588, 0.831373, 0.2) +border_width_left = 2 +border_color = Color(1, 0.54902, 0, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_selected"] +bg_color = Color(0, 0.470588, 0.831373, 0.5) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_selected_focus"] +bg_color = Color(0, 0.470588, 0.831373, 0.7) +border_width_left = 2 +border_color = Color(1, 0.54902, 0, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_grabber"] +bg_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_grabber_hover"] +bg_color = Color(1, 0.54902, 0, 1) +corner_radius_top_left = 3 +corner_radius_top_right = 3 +corner_radius_bottom_right = 3 +corner_radius_bottom_left = 3 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_bg"] +bg_color = Color(0.17, 0.2, 0.25, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_window"] +bg_color = Color(0.12, 0.14, 0.17, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0, 0.470588, 0.831373, 1) +corner_radius_top_left = 5 +corner_radius_top_right = 5 +corner_radius_bottom_right = 5 +corner_radius_bottom_left = 5 + +[resource] +resource_local_to_scene = true +resource_name = "godot-playfab_theme" +default_font_size = 14 +Button/colors/font_color = Color(1, 1, 1, 1) +Button/colors/font_disabled_color = Color(0.45098, 0.45098, 0.45098, 1) +Button/colors/font_focus_color = Color(1, 1, 1, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(1, 1, 1, 1) +Button/styles/disabled = SubResource("StyleBoxFlat_bg_disabled") +Button/styles/focus = SubResource("StyleBoxFlat_bg_focus") +Button/styles/hover = SubResource("StyleBoxFlat_bg_hover") +Button/styles/normal = SubResource("StyleBoxFlat_bg_normal") +Button/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") +CheckButton/colors/font_color = Color(1, 1, 1, 1) +CheckButton/colors/font_disabled_color = Color(0.45098, 0.45098, 0.45098, 1) +CheckButton/colors/font_focus_color = Color(1, 1, 1, 1) +CheckButton/colors/font_hover_color = Color(1, 1, 1, 1) +CheckButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1) +CheckButton/colors/font_pressed_color = Color(1, 1, 1, 1) +CheckButton/styles/normal = SubResource("StyleBoxFlat_check_button_off") +CheckButton/styles/pressed = SubResource("StyleBoxFlat_check_button_on") +ColorPickerButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") +GraphNode/styles/comment = SubResource("StyleBoxFlat_graph_node_comment") +GraphNode/styles/frame = SubResource("StyleBoxFlat_graph_node_bg") +GraphNode/styles/selectedframe = SubResource("StyleBoxFlat_graph_node_selected") +HScrollBar/styles/grabber = SubResource("StyleBoxFlat_hscroll_grabber") +HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_hscroll_grabber_hover") +HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_hscroll_grabber_hover") +HScrollBar/styles/scroll = SubResource("StyleBoxFlat_hscroll_bg") +HSlider/styles/grabber_area = SubResource("StyleBoxFlat_slider_bg") +HSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_slider_bg") +HSlider/styles/slider = SubResource("StyleBoxFlat_slider_grabber") +Label/colors/font_color = Color(1, 1, 1, 1) +LineEdit/colors/font_color = Color(1, 1, 1, 1) +LineEdit/styles/focus = SubResource("StyleBoxFlat_line_edit_focus") +LineEdit/styles/normal = SubResource("StyleBoxFlat_line_edit_normal") +MarginContainer/constants/margin_bottom = 5 +MarginContainer/constants/margin_left = 5 +MarginContainer/constants/margin_right = 5 +MarginContainer/constants/margin_top = 5 +MenuButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") +MenuButton/styles/hover = SubResource("StyleBoxFlat_bg_hover") +MenuButton/styles/normal = SubResource("StyleBoxFlat_bg_normal") +MenuButton/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") +OptionButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") +OptionButton/styles/hover = SubResource("StyleBoxFlat_bg_hover") +OptionButton/styles/normal = SubResource("StyleBoxFlat_bg_normal") +OptionButton/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") +Panel/styles/panel = SubResource("StyleBoxFlat_panel") +PanelContainer/styles/panel = SubResource("StyleBoxFlat_panel") +PopupMenu/styles/panel = SubResource("StyleBoxFlat_menu_panel") +ProgressBar/styles/background = SubResource("StyleBoxFlat_progress_bg") +ProgressBar/styles/fill = SubResource("StyleBoxFlat_progress_fill") +RichTextLabel/colors/default_color = Color(1, 1, 1, 1) +Separator/styles/h_separator = SubResource("StyleBoxFlat_separator") +Separator/styles/v_separator = SubResource("StyleBoxFlat_v_separator") +TabContainer/styles/panel = SubResource("StyleBoxFlat_tab_bar_bg") +TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_tab_selected") +TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_tab_unselected") +TextEdit/colors/background_color = Color(0.12, 0.14, 0.17, 1) +TextEdit/colors/current_line_color = Color(0, 0.470588, 0.831373, 0.2) +TextEdit/colors/font_color = Color(1, 1, 1, 1) +TextEdit/styles/focus = SubResource("StyleBoxFlat_line_edit_focus") +TextEdit/styles/normal = SubResource("StyleBoxFlat_line_edit_normal") +TooltipPanel/styles/panel = SubResource("StyleBoxFlat_tooltip") +Tree/colors/font_color = Color(1, 1, 1, 1) +Tree/colors/guide_color = Color(0.45098, 0.45098, 0.45098, 0.1) +Tree/colors/relation_line_color = Color(0.45098, 0.45098, 0.45098, 0.1) +Tree/styles/bg = SubResource("StyleBoxFlat_tree_bg") +Tree/styles/cursor = SubResource("StyleBoxFlat_tree_cursor") +Tree/styles/selected = SubResource("StyleBoxFlat_tree_selected") +Tree/styles/selected_focus = SubResource("StyleBoxFlat_tree_selected_focus") +VScrollBar/styles/grabber = SubResource("StyleBoxFlat_vscroll_grabber") +VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_vscroll_grabber_hover") +VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_vscroll_grabber_hover") +VScrollBar/styles/scroll = SubResource("StyleBoxFlat_vscroll_bg") +VSlider/styles/grabber_area = SubResource("StyleBoxFlat_slider_bg") +VSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_slider_bg") +VSlider/styles/slider = SubResource("StyleBoxFlat_slider_grabber") +Window/styles/panel = SubResource("StyleBoxFlat_window") From 1787d9de3f68ef3db82185944f381e2a507b3ae4 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 16:55:02 +0200 Subject: [PATCH 38/84] updated theme --- .../theme/godot-playfab_theme.tres | 441 ++---------------- project.godot | 2 +- 2 files changed, 52 insertions(+), 391 deletions(-) diff --git a/addons/godot-playfab/theme/godot-playfab_theme.tres b/addons/godot-playfab/theme/godot-playfab_theme.tres index b877d5e9..0539f6f6 100644 --- a/addons/godot-playfab/theme/godot-playfab_theme.tres +++ b/addons/godot-playfab/theme/godot-playfab_theme.tres @@ -1,76 +1,16 @@ -[gd_resource type="Theme" load_steps=36 format=3 uid="uid://cax55m47g78g"] +[gd_resource type="Theme" load_steps=6 format=3 uid="uid://bcyfaw335q2be"] -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_disabled"] -bg_color = Color(0.12, 0.14, 0.17, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.17, 0.2, 0.25, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 2.0 -expand_margin_top = 2.0 -expand_margin_right = 2.0 -expand_margin_bottom = 2.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_focus"] -bg_color = Color(0, 0.470588, 0.831373, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(1, 0.54902, 0, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 2.0 -expand_margin_top = 2.0 -expand_margin_right = 2.0 -expand_margin_bottom = 2.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_hover"] -bg_color = Color(0.1, 0.41, 0.6, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 2.0 -expand_margin_top = 2.0 -expand_margin_right = 2.0 -expand_margin_bottom = 2.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_normal"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 2.0 -expand_margin_top = 2.0 -expand_margin_right = 2.0 -expand_margin_bottom = 2.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg_pressed"] -bg_color = Color(0, 0.380392, 0.678431, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(1, 0.54902, 0, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k71x6"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.109804, 0.109804, 0.109804, 0.6) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.94902, 0.313726, 0.133333, 1) corner_radius_top_left = 3 corner_radius_top_right = 3 corner_radius_bottom_right = 3 @@ -80,354 +20,75 @@ expand_margin_top = 2.0 expand_margin_right = 2.0 expand_margin_bottom = 2.0 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_check_button_off"] +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q26xb"] content_margin_left = 4.0 content_margin_top = 4.0 content_margin_right = 4.0 content_margin_bottom = 4.0 -bg_color = Color(0.12, 0.14, 0.17, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 2 -corner_radius_top_right = 2 -corner_radius_bottom_right = 2 -corner_radius_bottom_left = 2 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_check_button_on"] -content_margin_left = 4.0 -content_margin_top = 4.0 -content_margin_right = 4.0 -content_margin_bottom = 4.0 -bg_color = Color(0, 0.470588, 0.831373, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 2 -corner_radius_top_right = 2 -corner_radius_bottom_right = 2 -corner_radius_bottom_left = 2 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_comment"] -bg_color = Color(0.12, 0.14, 0.17, 1) +bg_color = Color(0.94902, 0.313726, 0.133333, 1) border_width_left = 2 border_width_top = 2 border_width_right = 2 border_width_bottom = 2 -border_color = Color(0.73, 0.73, 0.73, 1) -corner_radius_top_left = 5 -corner_radius_top_right = 5 -corner_radius_bottom_right = 5 -corner_radius_bottom_left = 5 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 2 -border_width_top = 2 -border_width_right = 2 -border_width_bottom = 2 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 5 -corner_radius_top_right = 5 -corner_radius_bottom_right = 5 -corner_radius_bottom_left = 5 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_graph_node_selected"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 2 -border_width_top = 2 -border_width_right = 2 -border_width_bottom = 2 -border_color = Color(1, 0.54902, 0, 1) -corner_radius_top_left = 5 -corner_radius_top_right = 5 -corner_radius_bottom_right = 5 -corner_radius_bottom_left = 5 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_grabber"] -bg_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_grabber_hover"] -bg_color = Color(1, 0.54902, 0, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hscroll_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_slider_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_slider_grabber"] -bg_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 5.0 -expand_margin_top = 5.0 -expand_margin_right = 5.0 -expand_margin_bottom = 5.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_line_edit_focus"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0, 0.470588, 0.831373, 1) +border_color = Color(1, 1, 1, 1) corner_radius_top_left = 3 corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -expand_margin_left = 5.0 -expand_margin_top = 3.0 -expand_margin_right = 5.0 -expand_margin_bottom = 3.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_line_edit_normal"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 -expand_margin_left = 5.0 -expand_margin_top = 3.0 -expand_margin_right = 5.0 -expand_margin_bottom = 3.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"] -bg_color = Color(0.12, 0.14, 0.17, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_menu_panel"] -bg_color = Color(0.09, 0.11, 0.14, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0, 0.470588, 0.831373, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0.09, 0.11, 0.14, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_fill"] -bg_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_separator"] -content_margin_left = 5.0 -content_margin_top = 2.0 -content_margin_right = 5.0 -content_margin_bottom = 2.0 -bg_color = Color(0.09, 0.11, 0.14, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v_separator"] -content_margin_left = 2.0 -content_margin_top = 5.0 -content_margin_right = 2.0 -content_margin_bottom = 5.0 -bg_color = Color(0.09, 0.11, 0.14, 1) -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_bar_bg"] -bg_color = Color(0.09, 0.11, 0.14, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_selected"] -bg_color = Color(0.12, 0.14, 0.17, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_color = Color(0, 0.470588, 0.831373, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -expand_margin_left = 10.0 -expand_margin_top = 5.0 -expand_margin_right = 10.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_unselected"] -bg_color = Color(0.17, 0.2, 0.25, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -expand_margin_left = 10.0 -expand_margin_top = 5.0 -expand_margin_right = 10.0 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tooltip"] -bg_color = Color(0.17, 0.2, 0.25, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(1, 0.54902, 0, 1) -corner_radius_top_left = 3 -corner_radius_top_right = 3 -corner_radius_bottom_right = 3 -corner_radius_bottom_left = 3 - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_cursor"] -bg_color = Color(0, 0.470588, 0.831373, 0.2) -border_width_left = 2 -border_color = Color(1, 0.54902, 0, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_selected"] -bg_color = Color(0, 0.470588, 0.831373, 0.5) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tree_selected_focus"] -bg_color = Color(0, 0.470588, 0.831373, 0.7) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hjf3w"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.94902, 0.313726, 0.133333, 0.7) border_width_left = 2 -border_color = Color(1, 0.54902, 0, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_grabber"] -bg_color = Color(0, 0.470588, 0.831373, 1) +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(1, 1, 1, 1) corner_radius_top_left = 3 corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_grabber_hover"] -bg_color = Color(1, 0.54902, 0, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_yksrt"] +content_margin_left = 4.0 +content_margin_top = 4.0 +content_margin_right = 4.0 +content_margin_bottom = 4.0 +bg_color = Color(0.109804, 0.109804, 0.109804, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.45098, 0.45098, 0.45098, 1) corner_radius_top_left = 3 corner_radius_top_right = 3 corner_radius_bottom_right = 3 corner_radius_bottom_left = 3 -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vscroll_bg"] -bg_color = Color(0.17, 0.2, 0.25, 1) - -[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_window"] -bg_color = Color(0.12, 0.14, 0.17, 1) -border_width_left = 1 -border_width_top = 1 -border_width_right = 1 -border_width_bottom = 1 -border_color = Color(0, 0.470588, 0.831373, 1) +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wveey"] +bg_color = Color(0.109804, 0.109804, 0.109804, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.45098, 0.45098, 0.45098, 1) corner_radius_top_left = 5 corner_radius_top_right = 5 corner_radius_bottom_right = 5 corner_radius_bottom_left = 5 [resource] -resource_local_to_scene = true -resource_name = "godot-playfab_theme" -default_font_size = 14 -Button/colors/font_color = Color(1, 1, 1, 1) -Button/colors/font_disabled_color = Color(0.45098, 0.45098, 0.45098, 1) +Button/colors/font_color = Color(0.878431, 0.878431, 0.878431, 1) +Button/colors/font_disabled_color = Color(0.34902, 0.34902, 0.34902, 1) Button/colors/font_focus_color = Color(1, 1, 1, 1) Button/colors/font_hover_color = Color(1, 1, 1, 1) -Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1) Button/colors/font_pressed_color = Color(1, 1, 1, 1) -Button/styles/disabled = SubResource("StyleBoxFlat_bg_disabled") -Button/styles/focus = SubResource("StyleBoxFlat_bg_focus") -Button/styles/hover = SubResource("StyleBoxFlat_bg_hover") -Button/styles/normal = SubResource("StyleBoxFlat_bg_normal") -Button/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") -CheckButton/colors/font_color = Color(1, 1, 1, 1) -CheckButton/colors/font_disabled_color = Color(0.45098, 0.45098, 0.45098, 1) -CheckButton/colors/font_focus_color = Color(1, 1, 1, 1) -CheckButton/colors/font_hover_color = Color(1, 1, 1, 1) -CheckButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1) -CheckButton/colors/font_pressed_color = Color(1, 1, 1, 1) -CheckButton/styles/normal = SubResource("StyleBoxFlat_check_button_off") -CheckButton/styles/pressed = SubResource("StyleBoxFlat_check_button_on") -ColorPickerButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") -GraphNode/styles/comment = SubResource("StyleBoxFlat_graph_node_comment") -GraphNode/styles/frame = SubResource("StyleBoxFlat_graph_node_bg") -GraphNode/styles/selectedframe = SubResource("StyleBoxFlat_graph_node_selected") -HScrollBar/styles/grabber = SubResource("StyleBoxFlat_hscroll_grabber") -HScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_hscroll_grabber_hover") -HScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_hscroll_grabber_hover") -HScrollBar/styles/scroll = SubResource("StyleBoxFlat_hscroll_bg") -HSlider/styles/grabber_area = SubResource("StyleBoxFlat_slider_bg") -HSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_slider_bg") -HSlider/styles/slider = SubResource("StyleBoxFlat_slider_grabber") -Label/colors/font_color = Color(1, 1, 1, 1) -LineEdit/colors/font_color = Color(1, 1, 1, 1) -LineEdit/styles/focus = SubResource("StyleBoxFlat_line_edit_focus") -LineEdit/styles/normal = SubResource("StyleBoxFlat_line_edit_normal") -MarginContainer/constants/margin_bottom = 5 -MarginContainer/constants/margin_left = 5 -MarginContainer/constants/margin_right = 5 -MarginContainer/constants/margin_top = 5 -MenuButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") -MenuButton/styles/hover = SubResource("StyleBoxFlat_bg_hover") -MenuButton/styles/normal = SubResource("StyleBoxFlat_bg_normal") -MenuButton/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") -OptionButton/styles/focus = SubResource("StyleBoxFlat_bg_focus") -OptionButton/styles/hover = SubResource("StyleBoxFlat_bg_hover") -OptionButton/styles/normal = SubResource("StyleBoxFlat_bg_normal") -OptionButton/styles/pressed = SubResource("StyleBoxFlat_bg_pressed") -Panel/styles/panel = SubResource("StyleBoxFlat_panel") -PanelContainer/styles/panel = SubResource("StyleBoxFlat_panel") -PopupMenu/styles/panel = SubResource("StyleBoxFlat_menu_panel") -ProgressBar/styles/background = SubResource("StyleBoxFlat_progress_bg") -ProgressBar/styles/fill = SubResource("StyleBoxFlat_progress_fill") -RichTextLabel/colors/default_color = Color(1, 1, 1, 1) -Separator/styles/h_separator = SubResource("StyleBoxFlat_separator") -Separator/styles/v_separator = SubResource("StyleBoxFlat_v_separator") -TabContainer/styles/panel = SubResource("StyleBoxFlat_tab_bar_bg") -TabContainer/styles/tab_selected = SubResource("StyleBoxFlat_tab_selected") -TabContainer/styles/tab_unselected = SubResource("StyleBoxFlat_tab_unselected") -TextEdit/colors/background_color = Color(0.12, 0.14, 0.17, 1) -TextEdit/colors/current_line_color = Color(0, 0.470588, 0.831373, 0.2) -TextEdit/colors/font_color = Color(1, 1, 1, 1) -TextEdit/styles/focus = SubResource("StyleBoxFlat_line_edit_focus") -TextEdit/styles/normal = SubResource("StyleBoxFlat_line_edit_normal") -TooltipPanel/styles/panel = SubResource("StyleBoxFlat_tooltip") -Tree/colors/font_color = Color(1, 1, 1, 1) -Tree/colors/guide_color = Color(0.45098, 0.45098, 0.45098, 0.1) -Tree/colors/relation_line_color = Color(0.45098, 0.45098, 0.45098, 0.1) -Tree/styles/bg = SubResource("StyleBoxFlat_tree_bg") -Tree/styles/cursor = SubResource("StyleBoxFlat_tree_cursor") -Tree/styles/selected = SubResource("StyleBoxFlat_tree_selected") -Tree/styles/selected_focus = SubResource("StyleBoxFlat_tree_selected_focus") -VScrollBar/styles/grabber = SubResource("StyleBoxFlat_vscroll_grabber") -VScrollBar/styles/grabber_highlight = SubResource("StyleBoxFlat_vscroll_grabber_hover") -VScrollBar/styles/grabber_pressed = SubResource("StyleBoxFlat_vscroll_grabber_hover") -VScrollBar/styles/scroll = SubResource("StyleBoxFlat_vscroll_bg") -VSlider/styles/grabber_area = SubResource("StyleBoxFlat_slider_bg") -VSlider/styles/grabber_area_highlight = SubResource("StyleBoxFlat_slider_bg") -VSlider/styles/slider = SubResource("StyleBoxFlat_slider_grabber") -Window/styles/panel = SubResource("StyleBoxFlat_window") +Button/styles/disabled = SubResource("StyleBoxFlat_k71x6") +Button/styles/focus = SubResource("StyleBoxFlat_q26xb") +Button/styles/hover = SubResource("StyleBoxFlat_hjf3w") +Button/styles/normal = SubResource("StyleBoxFlat_yksrt") +Button/styles/pressed = SubResource("StyleBoxFlat_q26xb") +Panel/styles/panel = SubResource("StyleBoxFlat_wveey") diff --git a/project.godot b/project.godot index 3c69d8a3..6adb4d9f 100644 --- a/project.godot +++ b/project.godot @@ -40,7 +40,7 @@ reload=false [gui] -theme/custom="uid://b3klx57slj0l3" +theme/custom="uid://bcyfaw335q2be" [physics] From fe5fb9492b89b5cd944faaefceb314a10bdb2439 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 17:01:42 +0200 Subject: [PATCH 39/84] Add missing signal `search_currency_complete` --- addons/godot-playfab/PlayFabCatalog.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 4da2841a..fba16ead 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -4,6 +4,7 @@ extends PlayFab class_name PlayFabCatalog signal search_complete +signal search_currency_complete const PAGE_SIZE := 50 From 696e3faf56fc67b9661c213139e2bf18ccd6a332 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 17:02:10 +0200 Subject: [PATCH 40/84] Add http request pooling --- addons/godot-playfab/ObjectPool.gd.uid | 1 + 1 file changed, 1 insertion(+) create mode 100644 addons/godot-playfab/ObjectPool.gd.uid diff --git a/addons/godot-playfab/ObjectPool.gd.uid b/addons/godot-playfab/ObjectPool.gd.uid new file mode 100644 index 00000000..f1a77f34 --- /dev/null +++ b/addons/godot-playfab/ObjectPool.gd.uid @@ -0,0 +1 @@ +uid://bx7jt16irkvka From a0641d5c5ed09eee850dd34547770045ce124ad1 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 17:03:01 +0200 Subject: [PATCH 41/84] Add filter to not query for currencies --- addons/godot-playfab/PlayFabCatalog.gd | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index fba16ead..b998c41c 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -25,6 +25,7 @@ func _search_all_items(): func _search_page(token: String) -> void: var request_data: SearchItemsRequest = SearchItemsRequest.new() request_data.Search = "" + request_data.Filter = "type ne 'currency'" # request_data.Filter "tags/any(t:t eq 'desert') and contentType eq 'gameitem'" request_data.OrderBy = "CreationDate asc" request_data.ContinuationToken = token From 13d67bde394ad810f1a9828ac6b396b053da68f0 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 4 Oct 2025 22:49:24 +0200 Subject: [PATCH 42/84] fix alignment of Tab bar content --- Scenes/Catalog.tscn | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Scenes/Catalog.tscn b/Scenes/Catalog.tscn index 38f0262f..b11ddb90 100644 --- a/Scenes/Catalog.tscn +++ b/Scenes/Catalog.tscn @@ -41,11 +41,12 @@ metadata/_tab_index = 0 [node name="ItemCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Items"] unique_name_in_owner = true custom_minimum_size = Vector2(100, 100) -layout_mode = 0 -offset_left = -2.0 -offset_top = 6.0 -offset_right = 1150.0 -offset_bottom = 530.0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 3 columns = 4 @@ -57,10 +58,12 @@ metadata/_tab_index = 1 [node name="CurrencyCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Currencies"] unique_name_in_owner = true custom_minimum_size = Vector2(100, 100) -layout_mode = 0 -offset_top = 29.0 -offset_right = 1152.0 -offset_bottom = 553.0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 size_flags_vertical = 3 columns = 4 From bdf2af277ee6ffa10be7fa64cddddc27dbb46a57 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 5 Oct 2025 12:50:09 +0200 Subject: [PATCH 43/84] Add PlayFabInventory.gd --- addons/godot-playfab/PlayFabInventory.gd | 16 ++++++++++++++++ addons/godot-playfab/PlayFabInventory.gd.uid | 1 + 2 files changed, 17 insertions(+) create mode 100644 addons/godot-playfab/PlayFabInventory.gd create mode 100644 addons/godot-playfab/PlayFabInventory.gd.uid diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd new file mode 100644 index 00000000..bc4bbded --- /dev/null +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -0,0 +1,16 @@ +@icon("res://addons/godot-playfab/icon.png") + +extends PlayFab +class_name PlayFabInventory + +const PAGE_SIZE := 50 +const TURBOLOAD_PAGE_SIZE := 10000 + + + + +## Get current inventory items. +## Callback receives a [GetInventoryItemsResponse] +## @tutorial(Request Documentation): https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/get-inventory-items?view=playfab-rest +#func get_inventory_items(request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new(), callback: Callable = func(): pass): +# _post_with_entity_auth(request_data, "/Inventory/GetInventoryItems", callback) diff --git a/addons/godot-playfab/PlayFabInventory.gd.uid b/addons/godot-playfab/PlayFabInventory.gd.uid new file mode 100644 index 00000000..52218fcf --- /dev/null +++ b/addons/godot-playfab/PlayFabInventory.gd.uid @@ -0,0 +1 @@ +uid://cv1wacridumt2 From c9356b021b70b24546337f6493c53f0135cbb2de Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 5 Oct 2025 12:53:27 +0200 Subject: [PATCH 44/84] Add inventory models --- .../Models/GetInventoryItemsRequest.gd | 39 +++++++++++++++++++ .../Models/GetInventoryItemsRequest.gd.uid | 1 + .../Models/GetInventoryItemsResponse.gd | 26 +++++++++++++ .../Models/GetInventoryItemsResponse.gd.uid | 1 + addons/godot-playfab/Models/InventoryItem.gd | 38 ++++++++++++++++++ .../godot-playfab/Models/InventoryItem.gd.uid | 1 + 6 files changed, 106 insertions(+) create mode 100644 addons/godot-playfab/Models/GetInventoryItemsRequest.gd create mode 100644 addons/godot-playfab/Models/GetInventoryItemsRequest.gd.uid create mode 100644 addons/godot-playfab/Models/GetInventoryItemsResponse.gd create mode 100644 addons/godot-playfab/Models/GetInventoryItemsResponse.gd.uid create mode 100644 addons/godot-playfab/Models/InventoryItem.gd create mode 100644 addons/godot-playfab/Models/InventoryItem.gd.uid diff --git a/addons/godot-playfab/Models/GetInventoryItemsRequest.gd b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd new file mode 100644 index 00000000..dc92e891 --- /dev/null +++ b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd @@ -0,0 +1,39 @@ +extends JsonSerializable +class_name GetInventoryItemsRequest + +## Number of items to retrieve. This value is optional. Maximum page size is 50. The default value is 10 +## REQUIRED: true +var Count: float + + +## The id of the entity's collection to perform this action on. (Default="default") +var CollectionId: String + + +## An opaque token used to retrieve the next page of items in the inventory, if any are available. Should be null on initial request. +var ContinuationToken: String + + +## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). +var CustomTags: Dictionary[String, Variant] + + +## The entity to perform this action on. +var Entity: EntityKey + + +## OData Filter to refine the items returned. InventoryItem properties 'type', 'id', and 'stackId' can be used in the filter. For example: "type eq 'currency'" +var Filter: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/GetInventoryItemsRequest.gd.uid b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd.uid new file mode 100644 index 00000000..7bcf6f6a --- /dev/null +++ b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd.uid @@ -0,0 +1 @@ +uid://cr15lf8hphbi2 diff --git a/addons/godot-playfab/Models/GetInventoryItemsResponse.gd b/addons/godot-playfab/Models/GetInventoryItemsResponse.gd new file mode 100644 index 00000000..068f5bc8 --- /dev/null +++ b/addons/godot-playfab/Models/GetInventoryItemsResponse.gd @@ -0,0 +1,26 @@ +extends JsonSerializable +class_name GetInventoryItemsResponse + +## An opaque token used to retrieve the next page of items, if any are available. +var ContinuationToken: String + + +## ETags are used for concurrency checking when updating resources. More information about using ETags can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/etags +var ETag: String + + +## The requested inventory items. +var Items: Array[InventoryItem] + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/GetInventoryItemsResponse.gd.uid b/addons/godot-playfab/Models/GetInventoryItemsResponse.gd.uid new file mode 100644 index 00000000..04ec2c55 --- /dev/null +++ b/addons/godot-playfab/Models/GetInventoryItemsResponse.gd.uid @@ -0,0 +1 @@ +uid://ckswgkf75ei1b diff --git a/addons/godot-playfab/Models/InventoryItem.gd b/addons/godot-playfab/Models/InventoryItem.gd new file mode 100644 index 00000000..71403e60 --- /dev/null +++ b/addons/godot-playfab/Models/InventoryItem.gd @@ -0,0 +1,38 @@ +extends JsonSerializable +class_name InventoryItem + +## The amount of the item. +var Amount: float + + +## Game specific properties for display purposes. This is an arbitrary JSON blob. The Display Properties field has a 1000 byte limit. +var DisplayProperties: Dictionary[String, Variant] + + +## Only used for subscriptions. The date of when the item will expire in UTC. +var ExpirationDate: String + + +## The id of the item. This should correspond to the item id in the catalog. +var Id: String + + +## The stack id of the item. +var StackId: String + + +## The type of the item. This should correspond to the item type in the catalog. +var Type: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/InventoryItem.gd.uid b/addons/godot-playfab/Models/InventoryItem.gd.uid new file mode 100644 index 00000000..b2bb0730 --- /dev/null +++ b/addons/godot-playfab/Models/InventoryItem.gd.uid @@ -0,0 +1 @@ +uid://b3ldgqvqu630p From 80b76ea892526d55806235d5a18d34eeb7112e02 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 5 Oct 2025 13:00:45 +0200 Subject: [PATCH 45/84] Add inventory to PlayFabManager.gd --- addons/godot-playfab/PlayFabManager.gd | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/godot-playfab/PlayFabManager.gd b/addons/godot-playfab/PlayFabManager.gd index c66c80a5..61c375d8 100644 --- a/addons/godot-playfab/PlayFabManager.gd +++ b/addons/godot-playfab/PlayFabManager.gd @@ -26,6 +26,10 @@ var event: PlayFabEvent = PlayFabEvent.new() ## @tutorial: https://learn.microsoft.com/en-us/rest/api/playfab/economy/catalog?view=playfab-rest var catalog: PlayFabCatalog = PlayFabCatalog.new() +## Represents the PlayFab `Inventory` (Economy V2) API +## @tutorial: https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory?view=playfab-rest +var inventory: PlayFabInventory = PlayFabInventory.new() + # Retrieves the `title_id` from `ProjectSettings` func _init(): From 59965538d8e3c26db13c8a46c248acecf872051e Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 5 Oct 2025 13:13:23 +0200 Subject: [PATCH 46/84] Refactored economy scenes --- Scenes/Economy.gd | 13 ++++ Scenes/Economy.gd.uid | 1 + Scenes/Economy.tscn | 37 +++++++++++ Scenes/{ => Economy}/Catalog.gd | 2 +- Scenes/{ => Economy}/Catalog.gd.uid | 0 Scenes/{ => Economy}/Catalog.tscn | 4 +- Scenes/Economy/Inventory.gd | 35 +++++++++++ Scenes/Economy/Inventory.gd.uid | 1 + Scenes/Economy/Inventory.tscn | 97 +++++++++++++++++++++++++++++ Scenes/LoggedIn.gd | 4 +- Scenes/LoggedIn.tscn | 4 +- 11 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 Scenes/Economy.gd create mode 100644 Scenes/Economy.gd.uid create mode 100644 Scenes/Economy.tscn rename Scenes/{ => Economy}/Catalog.gd (95%) rename Scenes/{ => Economy}/Catalog.gd.uid (100%) rename Scenes/{ => Economy}/Catalog.tscn (97%) create mode 100644 Scenes/Economy/Inventory.gd create mode 100644 Scenes/Economy/Inventory.gd.uid create mode 100644 Scenes/Economy/Inventory.tscn diff --git a/Scenes/Economy.gd b/Scenes/Economy.gd new file mode 100644 index 00000000..6e4a1981 --- /dev/null +++ b/Scenes/Economy.gd @@ -0,0 +1,13 @@ +extends Control + + +func _on_catalog_button_pressed() -> void: + SceneManager.goto_scene("res://Scenes/Economy/Catalog.tscn") + + +func _on_inventory_button_pressed() -> void: + SceneManager.goto_scene("res://Scenes/Economy/Inventory.tscn") + + +func _on_back_button_pressed() -> void: + SceneManager.goto_scene("res://Scenes/LoggedIn.tscn") diff --git a/Scenes/Economy.gd.uid b/Scenes/Economy.gd.uid new file mode 100644 index 00000000..3aafd1d2 --- /dev/null +++ b/Scenes/Economy.gd.uid @@ -0,0 +1 @@ +uid://vn8hsu4l0ml8 diff --git a/Scenes/Economy.tscn b/Scenes/Economy.tscn new file mode 100644 index 00000000..73aa74a2 --- /dev/null +++ b/Scenes/Economy.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=2 format=3 uid="uid://csavdi738j7na"] + +[ext_resource type="Script" uid="uid://vn8hsu4l0ml8" path="res://Scenes/Economy.gd" id="1_ngxwe"] + +[node name="Economy" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ngxwe") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="Label" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Economy V2" + +[node name="CatalogButton" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Catalog" + +[node name="InventoryButton" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Inventory" + +[node name="BackButton" type="Button" parent="VBoxContainer"] +layout_mode = 2 +text = "Back to Main Menu" + +[connection signal="pressed" from="VBoxContainer/CatalogButton" to="." method="_on_catalog_button_pressed"] +[connection signal="pressed" from="VBoxContainer/InventoryButton" to="." method="_on_inventory_button_pressed"] +[connection signal="pressed" from="VBoxContainer/BackButton" to="." method="_on_back_button_pressed"] diff --git a/Scenes/Catalog.gd b/Scenes/Economy/Catalog.gd similarity index 95% rename from Scenes/Catalog.gd rename to Scenes/Economy/Catalog.gd index ab0a54da..6276c5a0 100644 --- a/Scenes/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -32,4 +32,4 @@ func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> vo %CurrencyCardGridContainer.add_child(card) func _on_back_button_pressed() -> void: - SceneManager.goto_scene("res://Scenes/LoggedIn.tscn") + SceneManager.goto_scene("res://Scenes/Economy.tscn") diff --git a/Scenes/Catalog.gd.uid b/Scenes/Economy/Catalog.gd.uid similarity index 100% rename from Scenes/Catalog.gd.uid rename to Scenes/Economy/Catalog.gd.uid diff --git a/Scenes/Catalog.tscn b/Scenes/Economy/Catalog.tscn similarity index 97% rename from Scenes/Catalog.tscn rename to Scenes/Economy/Catalog.tscn index b11ddb90..f0a18bfd 100644 --- a/Scenes/Catalog.tscn +++ b/Scenes/Economy/Catalog.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://cq24viuorfsjc"] -[ext_resource type="Script" uid="uid://cvn4peu38pqpe" path="res://Scenes/Catalog.gd" id="1_lnqhp"] +[ext_resource type="Script" uid="uid://cvn4peu38pqpe" path="res://Scenes/Economy/Catalog.gd" id="1_lnqhp"] [ext_resource type="PackedScene" uid="uid://wwvhjjuojn77" path="res://Scenes/Widgets/LoadingIndicator.tscn" id="2_8nk0b"] [node name="Catalog" type="Control"] @@ -76,7 +76,7 @@ anchor_bottom = 1.0 offset_top = -29.0 grow_horizontal = 2 grow_vertical = 0 -text = "Back" +text = "Back to Economy" [node name="ProgressCenter" type="CenterContainer" parent="."] custom_minimum_size = Vector2(100, 100) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd new file mode 100644 index 00000000..6276c5a0 --- /dev/null +++ b/Scenes/Economy/Inventory.gd @@ -0,0 +1,35 @@ +extends Control + +@onready var card_scene: PackedScene = preload("res://Scenes/Widgets/ItemCard.tscn") + +func _ready() -> void: + %LoadingIndicator.show() + PlayFabManager.catalog.search_complete.connect(_on_search_complete) + PlayFabManager.catalog._search_all_items() + + var catalog: PlayFabCatalog = PlayFabCatalog.new() + add_child(catalog) + catalog.search_currency_complete.connect(_on_search_currency_complete) + catalog.search_currency() + + +func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: + print("All item IDs:", PlayFabManager.catalog.search_results) + for key in result.keys(): + var catalog_item: CatalogItem = result[key] + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %ItemCardGridContainer.add_child(card) + +func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> void: + print("Currency item IDs:", PlayFabManager.catalog.search_results) + for key in result.keys(): + var catalog_item: CatalogItem = result[key] + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %CurrencyCardGridContainer.add_child(card) + +func _on_back_button_pressed() -> void: + SceneManager.goto_scene("res://Scenes/Economy.tscn") diff --git a/Scenes/Economy/Inventory.gd.uid b/Scenes/Economy/Inventory.gd.uid new file mode 100644 index 00000000..0caf6176 --- /dev/null +++ b/Scenes/Economy/Inventory.gd.uid @@ -0,0 +1 @@ +uid://bdlfoyj3jgt24 diff --git a/Scenes/Economy/Inventory.tscn b/Scenes/Economy/Inventory.tscn new file mode 100644 index 00000000..fdb9ad5b --- /dev/null +++ b/Scenes/Economy/Inventory.tscn @@ -0,0 +1,97 @@ +[gd_scene load_steps=3 format=3 uid="uid://dq043cwwny6ak"] + +[ext_resource type="Script" uid="uid://bdlfoyj3jgt24" path="res://Scenes/Economy/Inventory.gd" id="1_ersrd"] +[ext_resource type="PackedScene" uid="uid://wwvhjjuojn77" path="res://Scenes/Widgets/LoadingIndicator.tscn" id="2_s30pp"] + +[node name="Invenory" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_ersrd") + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_bottom = -46.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Header" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Catalog" + +[node name="HSeparator" type="HSeparator" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 20) +layout_mode = 2 + +[node name="TabContainer" type="TabContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_vertical = 3 +current_tab = 0 + +[node name="Items" type="TabBar" parent="VBoxContainer/TabContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="ItemCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Items"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 100) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +columns = 4 + +[node name="Currencies" type="TabBar" parent="VBoxContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 1 + +[node name="CurrencyCardGridContainer" type="GridContainer" parent="VBoxContainer/TabContainer/Currencies"] +unique_name_in_owner = true +custom_minimum_size = Vector2(100, 100) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_vertical = 3 +columns = 4 + +[node name="BackButton" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -29.0 +grow_horizontal = 2 +grow_vertical = 0 +text = "Back to Economy" + +[node name="ProgressCenter" type="CenterContainer" parent="."] +custom_minimum_size = Vector2(100, 100) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="LoadingIndicator" parent="ProgressCenter" instance=ExtResource("2_s30pp")] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(100, 100) +layout_mode = 2 + +[connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"] diff --git a/Scenes/LoggedIn.gd b/Scenes/LoggedIn.gd index 499682a6..2787d00d 100644 --- a/Scenes/LoggedIn.gd +++ b/Scenes/LoggedIn.gd @@ -37,8 +37,8 @@ func _on_EventsPlayStream_pressed(): SceneManager.goto_scene("res://Scenes/Events.tscn") -func _on_CatalogButton_pressed(): - SceneManager.goto_scene("res://Scenes/Catalog.tscn") +func _on_EconomyButton_pressed(): + SceneManager.goto_scene("res://Scenes/Economy.tscn") func _on_RequestBuilder_pressed(): diff --git a/Scenes/LoggedIn.tscn b/Scenes/LoggedIn.tscn index 0fdf6c87..e8d8eb96 100644 --- a/Scenes/LoggedIn.tscn +++ b/Scenes/LoggedIn.tscn @@ -105,7 +105,7 @@ text = "Get Title Data" layout_mode = 2 text = "Events & PlayStream API" -[node name="CatalogButton" type="Button" parent="VBoxContainer"] +[node name="EconomyButton" type="Button" parent="VBoxContainer"] layout_mode = 2 text = "Catalog (Economy V2)" @@ -128,7 +128,7 @@ text = "Logout" [connection signal="pressed" from="VBoxContainer/GetTitleDataButton" to="." method="_on_GetTitleDataButton_pressed"] [connection signal="pressed" from="VBoxContainer/EventsPlayStream" to="." method="_on_EventsPlayStream_pressed"] -[connection signal="pressed" from="VBoxContainer/CatalogButton" to="." method="_on_CatalogButton_pressed"] +[connection signal="pressed" from="VBoxContainer/EconomyButton" to="." method="_on_EconomyButton_pressed"] [connection signal="pressed" from="VBoxContainer/RequestBuilder" to="." method="_on_RequestBuilder_pressed"] [connection signal="pressed" from="VBoxContainer/MainMenuButton" to="." method="_on_MainMenuButton_pressed"] [connection signal="pressed" from="VBoxContainer/LogoutButton" to="." method="_on_LogoutButton_pressed"] From 9af380507548c0d624003e56c94e4238d0252204 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 5 Oct 2025 15:09:36 +0200 Subject: [PATCH 47/84] Separate individual HTTP requests for future flexibility --- addons/godot-playfab/PlayFabHttp.gd | 51 ++++++++--------- addons/godot-playfab/PlayFabHttpResult.gd | 55 +++++++++++++++++++ addons/godot-playfab/PlayFabHttpResult.gd.uid | 1 + 3 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 addons/godot-playfab/PlayFabHttpResult.gd create mode 100644 addons/godot-playfab/PlayFabHttpResult.gd.uid diff --git a/addons/godot-playfab/PlayFabHttp.gd b/addons/godot-playfab/PlayFabHttp.gd index 54ff3587..ff0e8f17 100644 --- a/addons/godot-playfab/PlayFabHttp.gd +++ b/addons/godot-playfab/PlayFabHttp.gd @@ -47,6 +47,26 @@ func _get_api_url() -> String: func _http_request(request_method: int, body: Dictionary, path: String, callback: Callable, additional_headers: Dictionary = {}): + var http_response: PlayFabHttpResult = await _individual_http_request(request_method, body, path, callback, additional_headers) + if http_response.response_code >= 200 and http_response.response_code < 400: + if callback != null: + if callback.is_valid(): + callback.call(http_response.json_parse_result) + else: + push_error("Response calback " + callback.get_method() + " is no longer valid! Make sure, a script is only removed after all requests returned!") + return + elif http_response.response_code >= 400: + var apiErrorWrapper = ApiErrorWrapper.new() + for key in http_response.json_parse_result.keys(): + apiErrorWrapper.set(key, http_response.json_parse_result[key]) + emit_signal("api_error", apiErrorWrapper) + return + if http_response.response_code >= 500: + emit_signal("server_error", path) + return + + +func _individual_http_request(request_method: int, body: Dictionary, path: String, callback: Callable, additional_headers: Dictionary = {}) -> PlayFabHttpResult: # Create a new HTTPRequest instance for each request var http_request = HTTPRequest.new() add_child(http_request) @@ -75,32 +95,5 @@ func _http_request(request_method: int, body: Dictionary, path: String, callback # After the request completes, remove the node http_request.queue_free() - var response_result = args[0] as int - var response_code = args[1] as int - var response_headers = args[2] as PackedStringArray - var response_body = args[3] as PackedByteArray - - var response_body_string = response_body.get_string_from_utf8() - var test_json_conv = JSON.new() - var parse_error = test_json_conv.parse(response_body_string) - var json_parse_result = test_json_conv.data - - if parse_error != OK: - emit_signal("json_parse_error", json_parse_result) - return - if response_code >= 200 and response_code < 400: - if callback != null: - if callback.is_valid(): - callback.call(json_parse_result) - else: - push_error("Response calback " + callback.get_method() + " is no longer valid! Make sure, a script is only removed after all requests returned!") - return - elif response_code >= 400: - var apiErrorWrapper = ApiErrorWrapper.new() - for key in json_parse_result.keys(): - apiErrorWrapper.set(key, json_parse_result[key]) - emit_signal("api_error", apiErrorWrapper) - return - if response_code >= 500: - emit_signal("server_error", path) - return + var http_result := PlayFabHttpResult.new(args) + return http_result diff --git a/addons/godot-playfab/PlayFabHttpResult.gd b/addons/godot-playfab/PlayFabHttpResult.gd new file mode 100644 index 00000000..7e5dc366 --- /dev/null +++ b/addons/godot-playfab/PlayFabHttpResult.gd @@ -0,0 +1,55 @@ +extends RefCounted +class_name PlayFabHttpResult + +const CONTINUATION_TOKEN_KEY_NAME := "ContinuationToken" + +var _json_parse_result: Dictionary + + +var response_result: int + +var response_code: int + +var response_headers: PackedStringArray + +var response_body: PackedByteArray + +var json_parse_result: Dictionary: + get: + return _json_parse_result + +var _continuation_token: String = "" +var continuation_token: String: + get: + return _continuation_token + +func _init(args) -> void: + response_result = args[0] as int + response_code = args[1] as int + response_headers = args[2] as PackedStringArray + response_body = args[3] as PackedByteArray + + _json_parse_result = _parse_response_body() + +func _parse_response_body() -> Dictionary: + var response_body_string: String = response_body.get_string_from_utf8() + var test_json_conv = JSON.new() + var parse_error = test_json_conv.parse(response_body_string) + var json = test_json_conv.data + + if parse_error != OK: + push_error("Failed to parse JSON response body.") + + return json + +## Checks, whether the result has a continuation token. +func has_continuation_token() -> bool: + var token: String = get_continuation_token() + if token == null or token == "": + return false + + return true + +func get_continuation_token() -> String: + _continuation_token = self._json_parse_result["data"].get(CONTINUATION_TOKEN_KEY_NAME, "") + return _continuation_token diff --git a/addons/godot-playfab/PlayFabHttpResult.gd.uid b/addons/godot-playfab/PlayFabHttpResult.gd.uid new file mode 100644 index 00000000..86c5bac5 --- /dev/null +++ b/addons/godot-playfab/PlayFabHttpResult.gd.uid @@ -0,0 +1 @@ +uid://c6jjnj8il6355 From 025a85b00ce3628689518368ef533056b4a066fc Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 21:50:15 +0200 Subject: [PATCH 48/84] wip inventory implementation --- Scenes/Economy/Inventory.gd | 30 +++++++------------ .../Models/GetInventoryItemsRequest.gd | 5 ++-- addons/godot-playfab/PlayFabInventory.gd | 4 +-- addons/godot-playfab/PlayFabManager.gd | 1 + 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index 6276c5a0..eb2f17fa 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -4,32 +4,24 @@ extends Control func _ready() -> void: %LoadingIndicator.show() - PlayFabManager.catalog.search_complete.connect(_on_search_complete) - PlayFabManager.catalog._search_all_items() + PlayFabManager.inventory.get_inventory_items(GetInventoryItemsRequest.new(), _on_get_inventory_complete) - var catalog: PlayFabCatalog = PlayFabCatalog.new() - add_child(catalog) - catalog.search_currency_complete.connect(_on_search_currency_complete) - catalog.search_currency() + #var catalog: PlayFabCatalog = PlayFabCatalog.new() + #add_child(catalog) + #catalog.search_currency_complete.connect(_on_search_currency_complete) + #catalog.search_currency() -func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: - print("All item IDs:", PlayFabManager.catalog.search_results) - for key in result.keys(): - var catalog_item: CatalogItem = result[key] +func _on_get_inventory_complete(result: Dictionary) -> void: + var res = GetInventoryItemsResponse.new() + res.from_dict(result.data, res) + + for item in res.Items: + var catalog_item: CatalogItem = item var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) %LoadingIndicator.hide() %ItemCardGridContainer.add_child(card) -func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> void: - print("Currency item IDs:", PlayFabManager.catalog.search_results) - for key in result.keys(): - var catalog_item: CatalogItem = result[key] - var card: ItemCard = card_scene.instantiate() - card.reset(catalog_item) - %LoadingIndicator.hide() - %CurrencyCardGridContainer.add_child(card) - func _on_back_button_pressed() -> void: SceneManager.goto_scene("res://Scenes/Economy.tscn") diff --git a/addons/godot-playfab/Models/GetInventoryItemsRequest.gd b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd index dc92e891..1b92290a 100644 --- a/addons/godot-playfab/Models/GetInventoryItemsRequest.gd +++ b/addons/godot-playfab/Models/GetInventoryItemsRequest.gd @@ -1,9 +1,11 @@ extends JsonSerializable class_name GetInventoryItemsRequest +const MAX_ITEM_COUNT := 50 + ## Number of items to retrieve. This value is optional. Maximum page size is 50. The default value is 10 ## REQUIRED: true -var Count: float +var Count: int = MAX_ITEM_COUNT ## The id of the entity's collection to perform this action on. (Default="default") @@ -36,4 +38,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index bc4bbded..d5973b5b 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -12,5 +12,5 @@ const TURBOLOAD_PAGE_SIZE := 10000 ## Get current inventory items. ## Callback receives a [GetInventoryItemsResponse] ## @tutorial(Request Documentation): https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/get-inventory-items?view=playfab-rest -#func get_inventory_items(request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new(), callback: Callable = func(): pass): -# _post_with_entity_auth(request_data, "/Inventory/GetInventoryItems", callback) +func get_inventory_items(request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new(), callback: Callable = func(): pass): + _post_with_entity_auth(request_data, "/Inventory/GetInventoryItems", callback) diff --git a/addons/godot-playfab/PlayFabManager.gd b/addons/godot-playfab/PlayFabManager.gd index 61c375d8..a4ee4214 100644 --- a/addons/godot-playfab/PlayFabManager.gd +++ b/addons/godot-playfab/PlayFabManager.gd @@ -48,6 +48,7 @@ func _ready(): add_child(client) add_child(event) add_child(catalog) + add_child(inventory) client_config = _client_config_loader.load(title_id) From 631031739f70540958fb9ca9781f607b2277dffc Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 22:10:41 +0200 Subject: [PATCH 49/84] renamed _search_all_tems to _fetch_catalog --- Scenes/Economy/Catalog.gd | 2 +- addons/godot-playfab/PlayFabCatalog.gd | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Scenes/Economy/Catalog.gd b/Scenes/Economy/Catalog.gd index 6276c5a0..eb41d08e 100644 --- a/Scenes/Economy/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -5,7 +5,7 @@ extends Control func _ready() -> void: %LoadingIndicator.show() PlayFabManager.catalog.search_complete.connect(_on_search_complete) - PlayFabManager.catalog._search_all_items() + PlayFabManager.catalog.fetch_catalog() var catalog: PlayFabCatalog = PlayFabCatalog.new() add_child(catalog) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index b998c41c..ea129b05 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -18,14 +18,14 @@ var search_results : Dictionary[String, CatalogItem] = {} var continuation_token := "" -func _search_all_items(): +func fetch_catalog(): search_results.clear() _search_page("") func _search_page(token: String) -> void: var request_data: SearchItemsRequest = SearchItemsRequest.new() request_data.Search = "" - request_data.Filter = "type ne 'currency'" +# request_data.Filter = "type ne 'currency'" # request_data.Filter "tags/any(t:t eq 'desert') and contentType eq 'gameitem'" request_data.OrderBy = "CreationDate asc" request_data.ContinuationToken = token From f14c914f0504eebda5ab72a2470a3a442048daa7 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 22:20:08 +0200 Subject: [PATCH 50/84] cleanup --- Scenes/Economy/Catalog.gd | 2 -- 1 file changed, 2 deletions(-) diff --git a/Scenes/Economy/Catalog.gd b/Scenes/Economy/Catalog.gd index eb41d08e..94787dd7 100644 --- a/Scenes/Economy/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -14,7 +14,6 @@ func _ready() -> void: func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: - print("All item IDs:", PlayFabManager.catalog.search_results) for key in result.keys(): var catalog_item: CatalogItem = result[key] var card: ItemCard = card_scene.instantiate() @@ -23,7 +22,6 @@ func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: %ItemCardGridContainer.add_child(card) func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> void: - print("Currency item IDs:", PlayFabManager.catalog.search_results) for key in result.keys(): var catalog_item: CatalogItem = result[key] var card: ItemCard = card_scene.instantiate() From a884741d7d7583effde0f9f48ddd520fb22593fc Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 22:20:59 +0200 Subject: [PATCH 51/84] Hydrate the full catalog and cache it in memory instead of each time fetching it anew when the scene is opened --- addons/godot-playfab/PlayFabCatalog.gd | 42 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index ea129b05..9c708779 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -7,22 +7,35 @@ signal search_complete signal search_currency_complete const PAGE_SIZE := 50 +const FULL_CATALOG_FETCH_TIMEOUT := 3600 # seconds -var _catalog: Dictionary[String, Dictionary] = {} # item_id -> ShopItem -var _last_catalog_fetch_time: float = 0 +var _catalog: Dictionary[String, CatalogItem] = {} # item_id -> ShopItem +var _last_catalog_fetch_time: int = 0 var _fetching_catalog := false var _has_full_catalog := false # Search for all items using PlayFabManager.catalog.search_items() with pagination -var search_results : Dictionary[String, CatalogItem] = {} +var _search_results : Dictionary[String, CatalogItem] = {} var continuation_token := "" - -func fetch_catalog(): - search_results.clear() - _search_page("") - -func _search_page(token: String) -> void: +## Returns the cached catalog, fetching it if it's older than 5 minutes. +func get_catalog() -> Dictionary[String, CatalogItem]: + if _last_catalog_fetch_time == 0 or Time.get_unix_time_from_system() - _last_catalog_fetch_time > FULL_CATALOG_FETCH_TIMEOUT: + fetch_catalog() + return _catalog + +## Fetches the entire catalog with pagination. +## Emits [search_complete] when done. +func fetch_catalog() -> void: + if _fetching_catalog: + return + _fetching_catalog = true + _last_catalog_fetch_time = Time.get_unix_time_from_system() + _search_results.clear() + _fetch_catalog_page() + +## Internal function to fetch a page of search results. +func _fetch_catalog_page(token: String = "") -> void: var request_data: SearchItemsRequest = SearchItemsRequest.new() request_data.Search = "" # request_data.Filter = "type ne 'currency'" @@ -39,6 +52,7 @@ func _search_page(token: String) -> void: ## Searches for currencies. ## Not paginated! +## Emits [search_currency_complete] when done. func search_currency(callback: Callable = func(): pass) -> void: var request_data: SearchItemsRequest = SearchItemsRequest.new() request_data.Search = "" @@ -48,6 +62,7 @@ func search_currency(callback: Callable = func(): pass) -> void: search_items(request_data, _on_search_currency_complete) +## Internal callback for search_currency func _on_search_currency_complete(result: Dictionary) -> void: var res = SearchItemsResponse.new() res.from_dict(result.data, res) @@ -62,13 +77,16 @@ func _on_search_page_ok(result: Dictionary) -> void: var res = SearchItemsResponse.new() res.from_dict(result.data, res) for item: CatalogItem in res.Items: - search_results[item.Id] = item + _search_results[item.Id] = item var next_token: String = res.ContinuationToken if next_token != null and next_token != "": - _search_page(next_token) + _fetch_catalog_page(next_token) else: - search_complete.emit(search_results) + _fetching_catalog = false + _catalog.clear() + _catalog = _search_results + search_complete.emit(_search_results) From e19aeaa60dd9c8c9fb88049a578850151de243b2 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 22:29:34 +0200 Subject: [PATCH 52/84] Use cached catalog --- Scenes/Economy/Catalog.gd | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Scenes/Economy/Catalog.gd b/Scenes/Economy/Catalog.gd index 94787dd7..7324c32a 100644 --- a/Scenes/Economy/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -2,10 +2,14 @@ extends Control @onready var card_scene: PackedScene = preload("res://Scenes/Widgets/ItemCard.tscn") +var _catalog: Dictionary[String, CatalogItem] = {} # item_id -> ShopItem + func _ready() -> void: %LoadingIndicator.show() PlayFabManager.catalog.search_complete.connect(_on_search_complete) - PlayFabManager.catalog.fetch_catalog() + _catalog = PlayFabManager.catalog.get_catalog() + if _catalog.size() > 0: + _on_search_complete(_catalog) var catalog: PlayFabCatalog = PlayFabCatalog.new() add_child(catalog) @@ -14,7 +18,8 @@ func _ready() -> void: func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: - for key in result.keys(): + _catalog = PlayFabManager.catalog.get_catalog() + for key in _catalog: var catalog_item: CatalogItem = result[key] var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) From 9556be5c7cc401f81e766c49ed6c022c6c597222 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 22:40:27 +0200 Subject: [PATCH 53/84] Make loading of catalog items less complex and more robust --- Scenes/Economy/Catalog.gd | 38 ++++++++++---------------- addons/godot-playfab/PlayFabCatalog.gd | 3 +- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Scenes/Economy/Catalog.gd b/Scenes/Economy/Catalog.gd index 7324c32a..25ce5cee 100644 --- a/Scenes/Economy/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -2,37 +2,29 @@ extends Control @onready var card_scene: PackedScene = preload("res://Scenes/Widgets/ItemCard.tscn") -var _catalog: Dictionary[String, CatalogItem] = {} # item_id -> ShopItem - func _ready() -> void: %LoadingIndicator.show() PlayFabManager.catalog.search_complete.connect(_on_search_complete) - _catalog = PlayFabManager.catalog.get_catalog() - if _catalog.size() > 0: - _on_search_complete(_catalog) - - var catalog: PlayFabCatalog = PlayFabCatalog.new() - add_child(catalog) - catalog.search_currency_complete.connect(_on_search_currency_complete) - catalog.search_currency() + if PlayFabManager.catalog._has_full_catalog: + _on_search_complete() + else: + PlayFabManager.catalog.fetch_catalog() -func _on_search_complete(result: Dictionary[String, CatalogItem]) -> void: - _catalog = PlayFabManager.catalog.get_catalog() - for key in _catalog: - var catalog_item: CatalogItem = result[key] +func _on_search_complete() -> void: + var catalog := PlayFabManager.catalog.get_catalog() + for key in catalog: + var catalog_item: CatalogItem = catalog[key] var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) - %LoadingIndicator.hide() - %ItemCardGridContainer.add_child(card) + if catalog_item.Type == "currency": + %LoadingIndicator.hide() + %CurrencyCardGridContainer.add_child(card) + else: + %LoadingIndicator.hide() + %ItemCardGridContainer.add_child(card) + -func _on_search_currency_complete(result: Dictionary[String, CatalogItem]) -> void: - for key in result.keys(): - var catalog_item: CatalogItem = result[key] - var card: ItemCard = card_scene.instantiate() - card.reset(catalog_item) - %LoadingIndicator.hide() - %CurrencyCardGridContainer.add_child(card) func _on_back_button_pressed() -> void: SceneManager.goto_scene("res://Scenes/Economy.tscn") diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 9c708779..29d07306 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -84,9 +84,10 @@ func _on_search_page_ok(result: Dictionary) -> void: _fetch_catalog_page(next_token) else: _fetching_catalog = false + _has_full_catalog = true _catalog.clear() _catalog = _search_results - search_complete.emit(_search_results) + search_complete.emit() From 27371913c2d26a4f4c8a327e840f1495a268ca20 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Tue, 7 Oct 2025 23:33:13 +0200 Subject: [PATCH 54/84] Turboloading and showing inventory items --- Scenes/Economy/Inventory.gd | 37 ++++++++++++------------ addons/godot-playfab/PlayFabInventory.gd | 13 ++++++++- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index eb2f17fa..7d86ce44 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -2,26 +2,27 @@ extends Control @onready var card_scene: PackedScene = preload("res://Scenes/Widgets/ItemCard.tscn") +var inventory_items: Array = [] + func _ready() -> void: %LoadingIndicator.show() - PlayFabManager.inventory.get_inventory_items(GetInventoryItemsRequest.new(), _on_get_inventory_complete) - - #var catalog: PlayFabCatalog = PlayFabCatalog.new() - #add_child(catalog) - #catalog.search_currency_complete.connect(_on_search_currency_complete) - #catalog.search_currency() - - -func _on_get_inventory_complete(result: Dictionary) -> void: - var res = GetInventoryItemsResponse.new() - res.from_dict(result.data, res) - - for item in res.Items: - var catalog_item: CatalogItem = item - var card: ItemCard = card_scene.instantiate() - card.reset(catalog_item) - %LoadingIndicator.hide() - %ItemCardGridContainer.add_child(card) + + var catalog: Dictionary[String, CatalogItem] = PlayFabManager.catalog.get_catalog() + + var turboload_complete: Callable = func(res: GetInventoryItemsResponse) -> void: + for item in res.Items: + var catalog_item: CatalogItem = resolve_catalog_item(item.Id, catalog) + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %ItemCardGridContainer.add_child(card) + + PlayFabManager.inventory.turboload_inventory(turboload_complete) + +func resolve_catalog_item(item_id: String, catalog: Dictionary[String, CatalogItem]) -> CatalogItem: + var item = catalog.get(item_id, CatalogItem.new()) + return item + func _on_back_button_pressed() -> void: SceneManager.goto_scene("res://Scenes/Economy.tscn") diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index d5973b5b..3ad39e72 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -7,10 +7,21 @@ const PAGE_SIZE := 50 const TURBOLOAD_PAGE_SIZE := 10000 +func turboload_inventory(callback: Callable = func(): pass) -> void: + var request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new() + request_data.Count = TURBOLOAD_PAGE_SIZE + get_inventory_items(request_data, func(result: GetInventoryItemsResponse) -> void: + callback.call(result) + ) + ## Get current inventory items. ## Callback receives a [GetInventoryItemsResponse] ## @tutorial(Request Documentation): https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/get-inventory-items?view=playfab-rest func get_inventory_items(request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new(), callback: Callable = func(): pass): - _post_with_entity_auth(request_data, "/Inventory/GetInventoryItems", callback) + _post_with_entity_auth(request_data, "/Inventory/GetInventoryItems", func(result: Dictionary) -> void: + var res = GetInventoryItemsResponse.new() + res.from_dict(result.data, res) + callback.call(res) + ) From 6204369031de3620a13c6591010209ba24ecfaaa Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 00:11:43 +0200 Subject: [PATCH 55/84] Load catalog & inventory on playfab init --- Scenes/Economy/Inventory.gd | 17 +++++------ addons/godot-playfab/PlayFabCatalog.gd | 6 ++++ addons/godot-playfab/PlayFabInventory.gd | 37 +++++++++++++++++++++++- addons/godot-playfab/PlayFabManager.gd | 3 ++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index 7d86ce44..ec40536b 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -9,15 +9,14 @@ func _ready() -> void: var catalog: Dictionary[String, CatalogItem] = PlayFabManager.catalog.get_catalog() - var turboload_complete: Callable = func(res: GetInventoryItemsResponse) -> void: - for item in res.Items: - var catalog_item: CatalogItem = resolve_catalog_item(item.Id, catalog) - var card: ItemCard = card_scene.instantiate() - card.reset(catalog_item) - %LoadingIndicator.hide() - %ItemCardGridContainer.add_child(card) - - PlayFabManager.inventory.turboload_inventory(turboload_complete) + var inventory: Dictionary[String, InventoryItem] = PlayFabManager.inventory.get_inventory() + for id in inventory: + var item: InventoryItem = inventory[id] + var catalog_item: CatalogItem = resolve_catalog_item(item.Id, catalog) + var card: ItemCard = card_scene.instantiate() + card.reset(catalog_item) + %LoadingIndicator.hide() + %ItemCardGridContainer.add_child(card) func resolve_catalog_item(item_id: String, catalog: Dictionary[String, CatalogItem]) -> CatalogItem: var item = catalog.get(item_id, CatalogItem.new()) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index 29d07306..cebbfef2 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -18,6 +18,12 @@ var _has_full_catalog := false var _search_results : Dictionary[String, CatalogItem] = {} var continuation_token := "" +func _ready(): + PlayFabManager.playfab_initialized.connect(func(): + get_catalog() + ) + + ## Returns the cached catalog, fetching it if it's older than 5 minutes. func get_catalog() -> Dictionary[String, CatalogItem]: if _last_catalog_fetch_time == 0 or Time.get_unix_time_from_system() - _last_catalog_fetch_time > FULL_CATALOG_FETCH_TIMEOUT: diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index 3ad39e72..239b2add 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -7,11 +7,46 @@ const PAGE_SIZE := 50 const TURBOLOAD_PAGE_SIZE := 10000 +var _inventory_items: Dictionary[String, InventoryItem] = {} # item_id -> ShopItem +var _last_inventory_fetch_time: int = 0 +var _fetching_inventory := false +var _has_full_inventory := false + + +func _ready(): + PlayFabManager.playfab_initialized.connect(func(): + turboload_inventory() + ) + + +## Get current inventory items stored locally. +## Returns a dictionary of InventoryItem objects, keyed by their item IDs. +func get_inventory() -> Dictionary[String, InventoryItem]: + return _inventory_items + +## Get current inventory items and store them locally. +## Callback receives a [GetInventoryItemsResponse] +## @tutorial: https://learn.microsoft.com/en-us/gaming/playfab/economy-monetization/economy-v2/inventory/turboloading func turboload_inventory(callback: Callable = func(): pass) -> void: + if _fetching_inventory or _has_full_inventory: + callback.call(GetInventoryItemsResponse.new()) + return + + _fetching_inventory = true + var request_data: GetInventoryItemsRequest = GetInventoryItemsRequest.new() request_data.Count = TURBOLOAD_PAGE_SIZE get_inventory_items(request_data, func(result: GetInventoryItemsResponse) -> void: - callback.call(result) + _inventory_items.clear() + for item in result.Items: + _inventory_items[item.Id] = item + + _fetching_inventory = false + _has_full_inventory = true + _last_inventory_fetch_time = Time.get_unix_time_from_system() + + if callback.is_valid(): + callback.call() ) diff --git a/addons/godot-playfab/PlayFabManager.gd b/addons/godot-playfab/PlayFabManager.gd index a4ee4214..18b86720 100644 --- a/addons/godot-playfab/PlayFabManager.gd +++ b/addons/godot-playfab/PlayFabManager.gd @@ -2,6 +2,8 @@ extends Node # This is script must be auto-loaded as `PlayFabManager`. # Use it as a global state/config manager for PlayFab data, like login persistence. +signal playfab_initialized + # Handles saving/loading of the `PlayFabClientConfig` var _client_config_loader = PlayFabClientConfigLoader.new() @@ -50,6 +52,7 @@ func _ready(): add_child(catalog) add_child(inventory) client_config = _client_config_loader.load(title_id) + playfab_initialized.emit() # Saves the client config to a file From a8fcb1e6bc2102e4e74bdc5ded6692788088dbce Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 00:27:14 +0200 Subject: [PATCH 56/84] always turboload inventory if called (unless it's already doing that) --- addons/godot-playfab/PlayFabInventory.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index 239b2add..cc57fe07 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -28,7 +28,7 @@ func get_inventory() -> Dictionary[String, InventoryItem]: ## Callback receives a [GetInventoryItemsResponse] ## @tutorial: https://learn.microsoft.com/en-us/gaming/playfab/economy-monetization/economy-v2/inventory/turboloading func turboload_inventory(callback: Callable = func(): pass) -> void: - if _fetching_inventory or _has_full_inventory: + if _fetching_inventory: callback.call(GetInventoryItemsResponse.new()) return From 7b1117ae63dd376e2e976375de81d809a25be3fc Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 00:27:53 +0200 Subject: [PATCH 57/84] Removed support for Godot versions < 4.5 --- .github/workflows/main-godot4.yml | 8 -------- README.md | 2 -- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/main-godot4.yml b/.github/workflows/main-godot4.yml index 7f70e77d..72ee5bf2 100644 --- a/.github/workflows/main-godot4.yml +++ b/.github/workflows/main-godot4.yml @@ -37,14 +37,6 @@ jobs: max-parallel: 10 matrix: include: - - os: ubuntu-latest - godot-version: '4.3' - godot-status: 'stable' - gdunit-version: 'v5.1' - - os: ubuntu-latest - godot-version: '4.4' - godot-status: 'stable' - gdunit-version: 'v5.1' - os: ubuntu-latest godot-version: '4.5' godot-status: 'stable' diff --git a/README.md b/README.md index 16f8e98b..8e8fadea 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ [![Discord](https://img.shields.io/discord/1020665079668166677?color=rgb%2888%2C%20101%2C%20242%29&label=Discord&logo=discord)](https://discord.gg/7K7q2YuNXe) -![Godot Version - 4.3](https://img.shields.io/badge/Godot_Version-4.3-2ea44f?logo=godotengine) -![Godot Version - 4.4](https://img.shields.io/badge/Godot_Version-4.4-2ea44f?logo=godotengine) ![Godot Version - 4.5](https://img.shields.io/badge/Godot_Version-4.5-2ea44f?logo=godotengine) From 30fbd874bcecf3d395fb680bb42f9debadf89b03 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 22:49:16 +0200 Subject: [PATCH 58/84] Load inventory if enventory timeout --- addons/godot-playfab/PlayFabInventory.gd | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index cc57fe07..cf01e44f 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -5,6 +5,7 @@ class_name PlayFabInventory const PAGE_SIZE := 50 const TURBOLOAD_PAGE_SIZE := 10000 +const FULL_INVENTORY_CACHE_DURATION := 3600 # seconds var _inventory_items: Dictionary[String, InventoryItem] = {} # item_id -> ShopItem @@ -22,6 +23,8 @@ func _ready(): ## Get current inventory items stored locally. ## Returns a dictionary of InventoryItem objects, keyed by their item IDs. func get_inventory() -> Dictionary[String, InventoryItem]: + if _last_inventory_fetch_time == 0 or Time.get_unix_time_from_system() - _last_inventory_fetch_time > FULL_INVENTORY_CACHE_DURATION: + turboload_inventory() return _inventory_items ## Get current inventory items and store them locally. From 6b8dc7753418b9b98a13b4f5bd59c4ccc779bbcd Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 22:50:16 +0200 Subject: [PATCH 59/84] cleanup --- addons/godot-playfab/PlayFabInventory.gd | 2 +- addons/godot-playfab/PlayFabManager.gd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index cf01e44f..c5578454 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -8,7 +8,7 @@ const TURBOLOAD_PAGE_SIZE := 10000 const FULL_INVENTORY_CACHE_DURATION := 3600 # seconds -var _inventory_items: Dictionary[String, InventoryItem] = {} # item_id -> ShopItem +var _inventory_items: Dictionary[String, InventoryItem] = {} # item_id -> InventoryItem var _last_inventory_fetch_time: int = 0 var _fetching_inventory := false var _has_full_inventory := false diff --git a/addons/godot-playfab/PlayFabManager.gd b/addons/godot-playfab/PlayFabManager.gd index 18b86720..3b595e30 100644 --- a/addons/godot-playfab/PlayFabManager.gd +++ b/addons/godot-playfab/PlayFabManager.gd @@ -5,7 +5,7 @@ extends Node signal playfab_initialized # Handles saving/loading of the `PlayFabClientConfig` -var _client_config_loader = PlayFabClientConfigLoader.new() +var _client_config_loader := PlayFabClientConfigLoader.new() # **READONLY** # The Tile ID to use for this project. Will be pulled from ProjectSettings. From 4c1135fd828e5c9cfc054e88dc69f5a4e403d3f5 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 22:53:13 +0200 Subject: [PATCH 60/84] Fixced title and layout of Inventory scene --- Scenes/Economy.tscn | 9 ++++++--- Scenes/Economy/Inventory.tscn | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Scenes/Economy.tscn b/Scenes/Economy.tscn index 73aa74a2..08f11d26 100644 --- a/Scenes/Economy.tscn +++ b/Scenes/Economy.tscn @@ -12,9 +12,12 @@ grow_vertical = 2 script = ExtResource("1_ngxwe") [node name="VBoxContainer" type="VBoxContainer" parent="."] -layout_mode = 0 -offset_right = 40.0 -offset_bottom = 40.0 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 [node name="Label" type="Label" parent="VBoxContainer"] layout_mode = 2 diff --git a/Scenes/Economy/Inventory.tscn b/Scenes/Economy/Inventory.tscn index fdb9ad5b..65be861c 100644 --- a/Scenes/Economy/Inventory.tscn +++ b/Scenes/Economy/Inventory.tscn @@ -23,7 +23,7 @@ grow_vertical = 2 [node name="Header" type="Label" parent="VBoxContainer"] layout_mode = 2 -text = "Catalog" +text = "Inventory" [node name="HSeparator" type="HSeparator" parent="VBoxContainer"] custom_minimum_size = Vector2(0, 20) From 5697816ed512ab8a93741713d8acbd3293d5ac0d Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 22:53:29 +0200 Subject: [PATCH 61/84] Currencies in inventories now go into the currencies tab --- Scenes/Economy/Inventory.gd | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index ec40536b..d3a48b58 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -16,7 +16,11 @@ func _ready() -> void: var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) %LoadingIndicator.hide() - %ItemCardGridContainer.add_child(card) + + if item.Type == "currency": + %CurrencyCardGridContainer.add_child(card) + else: + %ItemCardGridContainer.add_child(card) func resolve_catalog_item(item_id: String, catalog: Dictionary[String, CatalogItem]) -> CatalogItem: var item = catalog.get(item_id, CatalogItem.new()) From d7317ca797e530dad6fdda303915ddc1856581b6 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 22:58:44 +0200 Subject: [PATCH 62/84] rename item to inventory_item for clarity --- Scenes/Economy/Inventory.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index d3a48b58..6d96f202 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -11,13 +11,13 @@ func _ready() -> void: var inventory: Dictionary[String, InventoryItem] = PlayFabManager.inventory.get_inventory() for id in inventory: - var item: InventoryItem = inventory[id] - var catalog_item: CatalogItem = resolve_catalog_item(item.Id, catalog) + var inventory_item: InventoryItem = inventory[id] + var catalog_item: CatalogItem = resolve_catalog_item(inventory_item.Id, catalog) var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) %LoadingIndicator.hide() - if item.Type == "currency": + if inventory_item.Type == "currency": %CurrencyCardGridContainer.add_child(card) else: %ItemCardGridContainer.add_child(card) From 7118772407892fc22d6cafce68ec541b48fe7ed4 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 23:25:23 +0200 Subject: [PATCH 63/84] Implemented force refresh of Inventory --- Scenes/Economy/Inventory.gd | 26 +++++++++++++++++++++++--- Scenes/Economy/Inventory.tscn | 6 ++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index 6d96f202..52350583 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -5,27 +5,47 @@ extends Control var inventory_items: Array = [] func _ready() -> void: + _update_inventory() + +func _update_inventory() -> void: %LoadingIndicator.show() var catalog: Dictionary[String, CatalogItem] = PlayFabManager.catalog.get_catalog() - var inventory: Dictionary[String, InventoryItem] = PlayFabManager.inventory.get_inventory() for id in inventory: var inventory_item: InventoryItem = inventory[id] var catalog_item: CatalogItem = resolve_catalog_item(inventory_item.Id, catalog) var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) - %LoadingIndicator.hide() if inventory_item.Type == "currency": %CurrencyCardGridContainer.add_child(card) else: %ItemCardGridContainer.add_child(card) + + %LoadingIndicator.hide() + +func _clear_inventory() -> void: + for child in %CurrencyCardGridContainer.get_children(): + %CurrencyCardGridContainer.remove_child(child) + child.queue_free() + + for child in %ItemCardGridContainer.get_children(): + %ItemCardGridContainer.remove_child(child) + child.queue_free() + func resolve_catalog_item(item_id: String, catalog: Dictionary[String, CatalogItem]) -> CatalogItem: var item = catalog.get(item_id, CatalogItem.new()) return item - func _on_back_button_pressed() -> void: SceneManager.goto_scene("res://Scenes/Economy.tscn") + + +func _on_referesh_button_pressed() -> void: + %LoadingIndicator.show() + _clear_inventory() + PlayFabManager.inventory.turboload_inventory(func(): + _update_inventory() + ) diff --git a/Scenes/Economy/Inventory.tscn b/Scenes/Economy/Inventory.tscn index 65be861c..067d414e 100644 --- a/Scenes/Economy/Inventory.tscn +++ b/Scenes/Economy/Inventory.tscn @@ -67,6 +67,11 @@ grow_vertical = 2 size_flags_vertical = 3 columns = 4 +[node name="RefereshButton" type="Button" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Refresh Inventory" + [node name="BackButton" type="Button" parent="."] layout_mode = 1 anchors_preset = 12 @@ -94,4 +99,5 @@ visible = false custom_minimum_size = Vector2(100, 100) layout_mode = 2 +[connection signal="pressed" from="VBoxContainer/RefereshButton" to="." method="_on_referesh_button_pressed"] [connection signal="pressed" from="BackButton" to="." method="_on_back_button_pressed"] From c2fbac9e00e5b07ae268f7625bf7038d721f792f Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 23:25:47 +0200 Subject: [PATCH 64/84] Added CatalogItem type constants --- Scenes/Economy/Catalog.gd | 2 +- Scenes/Economy/Inventory.gd | 2 +- addons/godot-playfab/Models/CatalogItem.gd | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Scenes/Economy/Catalog.gd b/Scenes/Economy/Catalog.gd index 25ce5cee..3ec021d0 100644 --- a/Scenes/Economy/Catalog.gd +++ b/Scenes/Economy/Catalog.gd @@ -17,7 +17,7 @@ func _on_search_complete() -> void: var catalog_item: CatalogItem = catalog[key] var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) - if catalog_item.Type == "currency": + if catalog_item.Type == CatalogItem.TYPE_CURRENCY: %LoadingIndicator.hide() %CurrencyCardGridContainer.add_child(card) else: diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index 52350583..cb1ae436 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -18,7 +18,7 @@ func _update_inventory() -> void: var card: ItemCard = card_scene.instantiate() card.reset(catalog_item) - if inventory_item.Type == "currency": + if inventory_item.Type == CatalogItem.TYPE_CURRENCY: %CurrencyCardGridContainer.add_child(card) else: %ItemCardGridContainer.add_child(card) diff --git a/addons/godot-playfab/Models/CatalogItem.gd b/addons/godot-playfab/Models/CatalogItem.gd index 17b141bb..d0803345 100644 --- a/addons/godot-playfab/Models/CatalogItem.gd +++ b/addons/godot-playfab/Models/CatalogItem.gd @@ -1,6 +1,14 @@ extends JsonSerializable class_name CatalogItem + +const TYPE_CATALOG_ITEM: String = "catalogItem" +const TYPE_CURRENCY: String = "currency" +const TYPE_BUNDLE: String = "bundle" +const TYPE_STORE: String = "store" +const TYPE_UGC: String = "ugc" +const TYPE_SUBSCRIPTION: String = "subscription" + ## The alternate IDs associated with this item. An alternate ID can be set to 'FriendlyId' or any of the supported marketplace names. var AlternateIds: Array[CatalogAlternateId] From abd28fb3bceca6f97960d8a49dde1c0f50f7a2e5 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Wed, 8 Oct 2025 23:27:57 +0200 Subject: [PATCH 65/84] renamed button for EconomyV2 --- Scenes/LoggedIn.tscn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenes/LoggedIn.tscn b/Scenes/LoggedIn.tscn index e8d8eb96..a931257e 100644 --- a/Scenes/LoggedIn.tscn +++ b/Scenes/LoggedIn.tscn @@ -107,7 +107,7 @@ text = "Events & PlayStream API" [node name="EconomyButton" type="Button" parent="VBoxContainer"] layout_mode = 2 -text = "Catalog (Economy V2)" +text = "Economy V2" [node name="RequestBuilder" type="Button" parent="VBoxContainer"] layout_mode = 2 From 1f177c32206e026e889c598104d6cf352a42bd43 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Thu, 9 Oct 2025 00:27:38 +0200 Subject: [PATCH 66/84] Added amount to Inventory --- Scenes/Economy/Inventory.gd | 2 +- Scenes/Widgets/ItemCard.gd | 7 +++++++ Scenes/Widgets/ItemCard.tscn | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Scenes/Economy/Inventory.gd b/Scenes/Economy/Inventory.gd index cb1ae436..053f65fd 100644 --- a/Scenes/Economy/Inventory.gd +++ b/Scenes/Economy/Inventory.gd @@ -16,7 +16,7 @@ func _update_inventory() -> void: var inventory_item: InventoryItem = inventory[id] var catalog_item: CatalogItem = resolve_catalog_item(inventory_item.Id, catalog) var card: ItemCard = card_scene.instantiate() - card.reset(catalog_item) + card.reset_inventory(catalog_item, inventory_item) if inventory_item.Type == CatalogItem.TYPE_CURRENCY: %CurrencyCardGridContainer.add_child(card) diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd index d6403b9a..e90ba5cd 100644 --- a/Scenes/Widgets/ItemCard.gd +++ b/Scenes/Widgets/ItemCard.gd @@ -14,3 +14,10 @@ func reset(catalog_item: CatalogItem) -> void: var buy_button = Button.new() buy_button.text = text %VirtualPrices.add_child(buy_button) + + +func reset_inventory(catalog_item: CatalogItem, inventory_item: InventoryItem) -> void: + %Title.text = catalog_item.Title[PlayFab.LANG_NEUTRAL] + %Description.text = catalog_item.Description[PlayFab.LANG_NEUTRAL] + %Type.text = catalog_item.Type + %Amount.text = "%dx" % inventory_item.Amount diff --git a/Scenes/Widgets/ItemCard.tscn b/Scenes/Widgets/ItemCard.tscn index 4496def0..7819d2b8 100644 --- a/Scenes/Widgets/ItemCard.tscn +++ b/Scenes/Widgets/ItemCard.tscn @@ -41,3 +41,7 @@ text = "Type" [node name="VirtualPrices" type="VBoxContainer" parent="VBoxContainer"] unique_name_in_owner = true layout_mode = 2 + +[node name="Amount" type="Label" parent="VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 From 9a5e43e761dd3cf42476afbcf83ed71c175e8d63 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Fri, 10 Oct 2025 22:24:34 +0200 Subject: [PATCH 67/84] Add PlayFab models for PurchaseItems implementation --- addons/godot-playfab/Models/InitialValues.gd | 18 ++++++ .../godot-playfab/Models/InitialValues.gd.uid | 1 + .../Models/InventoryAlternateId.gd | 22 +++++++ .../Models/InventoryAlternateId.gd.uid | 1 + .../Models/InventoryItemReference.gd | 25 ++++++++ .../Models/InventoryItemReference.gd.uid | 1 + .../Models/PurchaseInventoryItemsRequest.gd | 61 +++++++++++++++++++ .../PurchaseInventoryItemsRequest.gd.uid | 1 + .../Models/PurchaseInventoryItemsResponse.gd | 25 ++++++++ .../PurchaseInventoryItemsResponse.gd.uid | 1 + .../Models/PurchasePriceAmount.gd | 25 ++++++++ .../Models/PurchasePriceAmount.gd.uid | 1 + 12 files changed, 182 insertions(+) create mode 100644 addons/godot-playfab/Models/InitialValues.gd create mode 100644 addons/godot-playfab/Models/InitialValues.gd.uid create mode 100644 addons/godot-playfab/Models/InventoryAlternateId.gd create mode 100644 addons/godot-playfab/Models/InventoryAlternateId.gd.uid create mode 100644 addons/godot-playfab/Models/InventoryItemReference.gd create mode 100644 addons/godot-playfab/Models/InventoryItemReference.gd.uid create mode 100644 addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd create mode 100644 addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd.uid create mode 100644 addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd create mode 100644 addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd.uid create mode 100644 addons/godot-playfab/Models/PurchasePriceAmount.gd create mode 100644 addons/godot-playfab/Models/PurchasePriceAmount.gd.uid diff --git a/addons/godot-playfab/Models/InitialValues.gd b/addons/godot-playfab/Models/InitialValues.gd new file mode 100644 index 00000000..69b65bd3 --- /dev/null +++ b/addons/godot-playfab/Models/InitialValues.gd @@ -0,0 +1,18 @@ +extends JsonSerializable +class_name InitialValues + +## Game specific properties for display purposes. The Display Properties field has a 1000 byte limit. +var DisplayProperties: Dictionary[String, Variant] + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/InitialValues.gd.uid b/addons/godot-playfab/Models/InitialValues.gd.uid new file mode 100644 index 00000000..e8d02121 --- /dev/null +++ b/addons/godot-playfab/Models/InitialValues.gd.uid @@ -0,0 +1 @@ +uid://d2tjtptwjrbma diff --git a/addons/godot-playfab/Models/InventoryAlternateId.gd b/addons/godot-playfab/Models/InventoryAlternateId.gd new file mode 100644 index 00000000..645cc3cb --- /dev/null +++ b/addons/godot-playfab/Models/InventoryAlternateId.gd @@ -0,0 +1,22 @@ +extends JsonSerializable +class_name InventoryAlternateId + +## Type of the alternate ID. +var Type: String + + +## Value of the alternate ID. +var Value: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) + diff --git a/addons/godot-playfab/Models/InventoryAlternateId.gd.uid b/addons/godot-playfab/Models/InventoryAlternateId.gd.uid new file mode 100644 index 00000000..f7317451 --- /dev/null +++ b/addons/godot-playfab/Models/InventoryAlternateId.gd.uid @@ -0,0 +1 @@ +uid://bglbhxuakewmv diff --git a/addons/godot-playfab/Models/InventoryItemReference.gd b/addons/godot-playfab/Models/InventoryItemReference.gd new file mode 100644 index 00000000..c3f31219 --- /dev/null +++ b/addons/godot-playfab/Models/InventoryItemReference.gd @@ -0,0 +1,25 @@ +extends JsonSerializable +class_name InventoryItemReference + +## The inventory item alternate id the request applies to. +var AlternateId: InventoryAlternateId + + +## The inventory item id the request applies to. +var Id: String + + +## The inventory stack id the request should redeem to. (Default="default") +var StackId: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) diff --git a/addons/godot-playfab/Models/InventoryItemReference.gd.uid b/addons/godot-playfab/Models/InventoryItemReference.gd.uid new file mode 100644 index 00000000..fba2d154 --- /dev/null +++ b/addons/godot-playfab/Models/InventoryItemReference.gd.uid @@ -0,0 +1 @@ +uid://bhplyquxguh6v diff --git a/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd new file mode 100644 index 00000000..676deee7 --- /dev/null +++ b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd @@ -0,0 +1,61 @@ +extends JsonSerializable +class_name PurchaseInventoryItemsRequest + +## The amount to purchase. +var Amount: float + + +## The id of the entity's collection to perform this action on. (Default="default"). The number of inventory collections is unlimited. +var CollectionId: String + + +## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). +var CustomTags: Dictionary[String, Variant] + + +## Indicates whether stacks reduced to an amount of 0 during the request should be deleted from the inventory. (Default=false) +var DeleteEmptyStacks: bool + + +## The duration to purchase. +var DurationInSeconds: float + + +## ETags are used for concurrency checking when updating resources. More information about using ETags can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/etags +var ETag: String + + +## The entity to perform this action on. +var Entity: EntityKey + + +## The Idempotency ID for this request. Idempotency IDs can be used to prevent operation replay in the medium term but will be garbage collected eventually. +var IdempotencyId: String + + +## The inventory item the request applies to. +var Item: InventoryItemReference + + +## The values to apply to a stack newly created by this request. +var NewStackValues: InitialValues + + +## The per-item price the item is expected to be purchased at. This must match a value configured in the Catalog or specified Store. +var PriceAmounts: Array[PurchasePriceAmount] + + +## The id of the Store to purchase the item from. +var StoreId: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) diff --git a/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd.uid b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd.uid new file mode 100644 index 00000000..e42e34a6 --- /dev/null +++ b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd.uid @@ -0,0 +1 @@ +uid://biicttm14u1jv diff --git a/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd b/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd new file mode 100644 index 00000000..51d02251 --- /dev/null +++ b/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd @@ -0,0 +1,25 @@ +extends JsonSerializable +class_name PurchaseInventoryItemsResponse + +## ETags are used for concurrency checking when updating resources. More information about using ETags can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/etags +var ETag: String + + +## The idempotency id used in the request. +var IdempotencyId: String + + +## The ids of transactions that occurred as a result of the request. +var TransactionIds: Array[String] + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) diff --git a/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd.uid b/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd.uid new file mode 100644 index 00000000..2dd8c29b --- /dev/null +++ b/addons/godot-playfab/Models/PurchaseInventoryItemsResponse.gd.uid @@ -0,0 +1 @@ +uid://btrm2ufv2o4wl diff --git a/addons/godot-playfab/Models/PurchasePriceAmount.gd b/addons/godot-playfab/Models/PurchasePriceAmount.gd new file mode 100644 index 00000000..e8ccc74a --- /dev/null +++ b/addons/godot-playfab/Models/PurchasePriceAmount.gd @@ -0,0 +1,25 @@ +extends JsonSerializable +class_name PurchasePriceAmount + +## The amount of the inventory item to use in the purchase . +var Amount: float + + +## The inventory item id to use in the purchase . +var ItemId: String + + +## The inventory stack id the to use in the purchase. Set to "default" by default +var StackId: String + + + +func _get_type_for_property(property_name: String) -> String: + match property_name: +# "": +# return "" + _: + pass + + push_error("Could not find mapping for property: " + property_name) + return super._get_type_for_property(property_name) diff --git a/addons/godot-playfab/Models/PurchasePriceAmount.gd.uid b/addons/godot-playfab/Models/PurchasePriceAmount.gd.uid new file mode 100644 index 00000000..6706a3cb --- /dev/null +++ b/addons/godot-playfab/Models/PurchasePriceAmount.gd.uid @@ -0,0 +1 @@ +uid://dt70qytxnkmvp From d4685fe38efcf2c0ff4dda0ce71264e6102541d6 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Fri, 10 Oct 2025 22:27:58 +0200 Subject: [PATCH 68/84] Implemented Purchase Inventory Items --- addons/godot-playfab/PlayFabInventory.gd | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/addons/godot-playfab/PlayFabInventory.gd b/addons/godot-playfab/PlayFabInventory.gd index c5578454..4e700915 100644 --- a/addons/godot-playfab/PlayFabInventory.gd +++ b/addons/godot-playfab/PlayFabInventory.gd @@ -63,3 +63,16 @@ func get_inventory_items(request_data: GetInventoryItemsRequest = GetInventoryIt res.from_dict(result.data, res) callback.call(res) ) + + +## Purchase an item or bundle. +## Up to 10,000 stacks of items can be added to a single inventory collection. +## Stack size is uncapped. +## @turorial(Quickstart): https://learn.microsoft.com/en-us/gaming/playfab/economy-monetization/economy-v2/inventory/quickstart#purchase-the-item +## @tutorial(Request Documentation): https://learn.microsoft.com/en-us/rest/api/playfab/economy/inventory/purchase-inventory-items?view=playfab-rest +func purchase_inventory_items(request_data: PurchaseInventoryItemsRequest, callback: Callable = func(): pass) -> void: + _post_with_entity_auth(request_data, "/Inventory/PurchaseInventoryItems", func(result: Dictionary) -> void: + var res = PurchaseInventoryItemsResponse.new() + res.from_dict(result.data, res) + callback.call(res) + ) From 68a6e77bb1c2f1194d2c3d5d7fdfe6ea750682bb Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 10:47:50 +0200 Subject: [PATCH 69/84] Fixed types for amounts --- .../Models/PurchaseInventoryItemsRequest.gd | 84 +++++++++++++------ .../Models/PurchasePriceAmount.gd | 2 +- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd index 676deee7..3252e06f 100644 --- a/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd +++ b/addons/godot-playfab/Models/PurchaseInventoryItemsRequest.gd @@ -1,52 +1,52 @@ extends JsonSerializable class_name PurchaseInventoryItemsRequest -## The amount to purchase. -var Amount: float +## The amount to purchase. +var Amount: int -## The id of the entity's collection to perform this action on. (Default="default"). The number of inventory collections is unlimited. -var CollectionId: String +## The id of the entity's collection to perform this action on. (Default="default"). The number of inventory collections is unlimited. +var CollectionId: String -## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). -var CustomTags: Dictionary[String, Variant] +## The optional custom tags associated with the request (e.g. build number, external trace identifiers, etc.). +var CustomTags: Dictionary[String, Variant] -## Indicates whether stacks reduced to an amount of 0 during the request should be deleted from the inventory. (Default=false) -var DeleteEmptyStacks: bool +## Indicates whether stacks reduced to an amount of 0 during the request should be deleted from the inventory. (Default=false) +var DeleteEmptyStacks: bool -## The duration to purchase. -var DurationInSeconds: float +## The duration to purchase. +var DurationInSeconds: float -## ETags are used for concurrency checking when updating resources. More information about using ETags can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/etags -var ETag: String +## ETags are used for concurrency checking when updating resources. More information about using ETags can be found here: https://learn.microsoft.com/en-us/gaming/playfab/features/economy-v2/catalog/etags +var ETag: String -## The entity to perform this action on. -var Entity: EntityKey +## The entity to perform this action on. +var Entity: EntityKey -## The Idempotency ID for this request. Idempotency IDs can be used to prevent operation replay in the medium term but will be garbage collected eventually. -var IdempotencyId: String +## The Idempotency ID for this request. Idempotency IDs can be used to prevent operation replay in the medium term but will be garbage collected eventually. +var IdempotencyId: String -## The inventory item the request applies to. -var Item: InventoryItemReference +## The inventory item the request applies to. +var Item: InventoryItemReference -## The values to apply to a stack newly created by this request. -var NewStackValues: InitialValues +## The values to apply to a stack newly created by this request. +var NewStackValues: InitialValues -## The per-item price the item is expected to be purchased at. This must match a value configured in the Catalog or specified Store. -var PriceAmounts: Array[PurchasePriceAmount] +## The per-item price the item is expected to be purchased at. This must match a value configured in the Catalog or specified Store. +var PriceAmounts: Array[PurchasePriceAmount] -## The id of the Store to purchase the item from. -var StoreId: String +## The id of the Store to purchase the item from. +var StoreId: String @@ -59,3 +59,39 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) + +static func from_catalog_item(purchase_amount: float, catalog_item: CatalogItem) -> PurchaseInventoryItemsRequest: + + var request_data := PurchaseInventoryItemsRequest.new() + request_data.Amount = purchase_amount + + var item_reference = InventoryItemReference.new() + item_reference.Id = catalog_item.Id +# item_reference.AlternateId = catalog_item.AlternateIds[0] +# item_reference.StackId = catalog_item.DefaultStackId + request_data.Item = item_reference + + var price_amounts : Array[PurchasePriceAmount] + + # Find the price option that matches the purchase amount + var catalog_item_price_key = catalog_item.PriceOptions.Prices.find_custom(func(item: CatalogPrice) -> bool: + if item.UnitAmount == purchase_amount: + return true + + return false + ) + + if catalog_item_price_key == -1: + push_error("Could not find price option for item: " + catalog_item.Id + " with amount: " + str(purchase_amount)) + return request_data + + + for catalog_price_amount in catalog_item.PriceOptions.Prices[catalog_item_price_key].Amounts: + var price_amount = PurchasePriceAmount.new() + price_amount.ItemId = catalog_price_amount.ItemId + price_amount.Amount = catalog_price_amount.Amount + price_amounts.append(price_amount) + + request_data.PriceAmounts = price_amounts + + return request_data diff --git a/addons/godot-playfab/Models/PurchasePriceAmount.gd b/addons/godot-playfab/Models/PurchasePriceAmount.gd index e8ccc74a..b61d2cb6 100644 --- a/addons/godot-playfab/Models/PurchasePriceAmount.gd +++ b/addons/godot-playfab/Models/PurchasePriceAmount.gd @@ -2,7 +2,7 @@ extends JsonSerializable class_name PurchasePriceAmount ## The amount of the inventory item to use in the purchase . -var Amount: float +var Amount: int ## The inventory item id to use in the purchase . From be0f9b280569d871124f5b144dbea79034475068 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 10:47:55 +0200 Subject: [PATCH 70/84] cleanup --- addons/godot-playfab/Models/CatalogPriceAmount.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/godot-playfab/Models/CatalogPriceAmount.gd b/addons/godot-playfab/Models/CatalogPriceAmount.gd index cdc65193..35b9460c 100644 --- a/addons/godot-playfab/Models/CatalogPriceAmount.gd +++ b/addons/godot-playfab/Models/CatalogPriceAmount.gd @@ -17,4 +17,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - From 0a57c0bc951094f684b2afdd20d3aca2e4b2f65c Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 10:48:15 +0200 Subject: [PATCH 71/84] Inmproved (de)serialisation - Fixed deserialisation bug for arrays - Added serialization for typed arrays --- addons/godot-playfab/JsonSerializable.gd | 30 +++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/addons/godot-playfab/JsonSerializable.gd b/addons/godot-playfab/JsonSerializable.gd index 5870c9fe..7e6b299d 100644 --- a/addons/godot-playfab/JsonSerializable.gd +++ b/addons/godot-playfab/JsonSerializable.gd @@ -44,6 +44,34 @@ func to_dict() -> Dictionary: #push_error("If '%s' is not a builtin class, please implement a to_dict() method! If it IS a builtin class, a special handler needs to be implemented in JsonSerializable." % type_name) print_debug("If '%s' is not a builtin class, please implement a to_dict() method! If it IS a builtin class, a special handler needs to be implemented in JsonSerializable." % type_name) dict[name] = type_name + elif type == TYPE_ARRAY: + var arr = get(name) + if arr.size() == 0: + continue + + var is_typed = arr.is_typed() + if is_typed: + var script = arr.get_typed_script() + if script == null: + # Builtin type, just set it and continue with the next element + dict[name] = arr + continue + + var script_name: StringName = (script as Script).get_global_name() + var new_arr := [] + for i in arr.size(): + var element = arr[i] + if element == null: + new_arr.append(null) + elif element.has_method("to_dict"): + new_arr.append(element.to_dict()) + else: + push_error("If '%s' is not a builtin class, please implement a to_dict() method! If it IS a builtin class, a special handler needs to be implemented in JsonSerializable." % script_name) + new_arr.append(script_name) + dict[name] = new_arr + else: + # Untyped array - just set it + dict[name] = arr else: # Get the value of the property var value = get(name) @@ -79,7 +107,7 @@ func from_dict(data: Dictionary, instance: JsonSerializable): # Builtin type, just set it and continue with the next element for i in data[key].size(): var element = data[key][i] - field.append(key, element) + field.append(element) continue var script_name: StringName = (script as Script).get_global_name() From 03f1d7b971e03ec94439b1e288c453cf15d49d9b Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 10:49:03 +0200 Subject: [PATCH 72/84] using new style of emitting signals --- addons/godot-playfab/PlayFabHttp.gd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/godot-playfab/PlayFabHttp.gd b/addons/godot-playfab/PlayFabHttp.gd index ff0e8f17..75a2c4d8 100644 --- a/addons/godot-playfab/PlayFabHttp.gd +++ b/addons/godot-playfab/PlayFabHttp.gd @@ -59,10 +59,10 @@ func _http_request(request_method: int, body: Dictionary, path: String, callback var apiErrorWrapper = ApiErrorWrapper.new() for key in http_response.json_parse_result.keys(): apiErrorWrapper.set(key, http_response.json_parse_result[key]) - emit_signal("api_error", apiErrorWrapper) + api_error.emit(apiErrorWrapper) return if http_response.response_code >= 500: - emit_signal("server_error", path) + server_error.emit(path) return From 69a5cd296ad84ac5524d969c5c062928e133d219 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 10:50:05 +0200 Subject: [PATCH 73/84] Implemented Purchasing fo items with virtual currency --- Scenes/Widgets/ItemCard.gd | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd index e90ba5cd..bdcc665d 100644 --- a/Scenes/Widgets/ItemCard.gd +++ b/Scenes/Widgets/ItemCard.gd @@ -9,10 +9,11 @@ func reset(catalog_item: CatalogItem) -> void: for price in catalog_item.PriceOptions.Prices: var unit_amount: float = price.UnitAmount - var price_amount = price.Amounts[0]["Amount"] + var price_amount = price.Amounts[0]["Amount"] # TODO: Get all costs, not just the first one var text: String = "%s pieces: %sG" % [unit_amount, price_amount] var buy_button = Button.new() buy_button.text = text + buy_button.pressed.connect(_on_purchase_button_pressed.bindv([unit_amount, catalog_item])) %VirtualPrices.add_child(buy_button) @@ -21,3 +22,12 @@ func reset_inventory(catalog_item: CatalogItem, inventory_item: InventoryItem) - %Description.text = catalog_item.Description[PlayFab.LANG_NEUTRAL] %Type.text = catalog_item.Type %Amount.text = "%dx" % inventory_item.Amount + + +func _on_purchase_button_pressed(amount: int, catalog_item: CatalogItem) -> void: + var request_data := PurchaseInventoryItemsRequest.from_catalog_item(amount, catalog_item) + + PlayFabManager.inventory.purchase_inventory_items(request_data, func(response: PurchaseInventoryItemsResponse): + pass + # TODO: Update inventory, currency + ) From 0ff0ada62ae88af069d0d8c73b7fd52fb6e0735f Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 12:54:39 +0200 Subject: [PATCH 74/84] connect api and server errors on PlayFabHttp --- addons/godot-playfab/PlayFabHttp.gd | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/addons/godot-playfab/PlayFabHttp.gd b/addons/godot-playfab/PlayFabHttp.gd index 75a2c4d8..fa542546 100644 --- a/addons/godot-playfab/PlayFabHttp.gd +++ b/addons/godot-playfab/PlayFabHttp.gd @@ -29,6 +29,23 @@ func _ready(): _http = HTTPRequest.new() add_child(_http) + api_error.connect(func(api_error_wrapper: ApiErrorWrapper): + var text = "[b]%s[/b]\n\n" % api_error_wrapper.errorMessage + var error_details = api_error_wrapper.errorDetails + + if error_details: + for key in error_details.keys(): + text += "[color=red][b]%s[/b][/color]: " % key + for element in error_details[key]: + text += "%s\n" % element + + print_rich(text) + ) + + server_error.connect(func(path: String): + push_error("A server error occured while querying %s" % path) + ) + func _dict_to_header_array(dict: Dictionary): if dict.size() < 1: From 0f3d98b13e76cda5d8abb0a4acdb975e642749f7 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 16:28:44 +0200 Subject: [PATCH 75/84] Display all prices for an item --- Scenes/Widgets/ItemCard.gd | 8 ++++++-- addons/godot-playfab/PlayFabCatalog.gd | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd index bdcc665d..5cc5db25 100644 --- a/Scenes/Widgets/ItemCard.gd +++ b/Scenes/Widgets/ItemCard.gd @@ -9,8 +9,12 @@ func reset(catalog_item: CatalogItem) -> void: for price in catalog_item.PriceOptions.Prices: var unit_amount: float = price.UnitAmount - var price_amount = price.Amounts[0]["Amount"] # TODO: Get all costs, not just the first one - var text: String = "%s pieces: %sG" % [unit_amount, price_amount] + var text: String = "%s pieces: " % [unit_amount] + + for currency_amount in price.Amounts: + var currency := PlayFabManager.catalog.resolve_item(currency_amount.ItemId) + text += "| %s (%s)" % [currency_amount.Amount, currency.Title.get("NEUTRAL")] + var buy_button = Button.new() buy_button.text = text buy_button.pressed.connect(_on_purchase_button_pressed.bindv([unit_amount, catalog_item])) diff --git a/addons/godot-playfab/PlayFabCatalog.gd b/addons/godot-playfab/PlayFabCatalog.gd index cebbfef2..3a3ed6ea 100644 --- a/addons/godot-playfab/PlayFabCatalog.gd +++ b/addons/godot-playfab/PlayFabCatalog.gd @@ -96,6 +96,11 @@ func _on_search_page_ok(result: Dictionary) -> void: search_complete.emit() +## Resolves an item from the cached catalog by its item ID. +func resolve_item(item_id: String) -> CatalogItem: + if item_id in _catalog: + return _catalog[item_id] + return null ## Retrieves items from the public catalog. Up to 50 items can be returned at once. From 00cb8444140c027a6435d52bc13ecae654f6583d Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:33:53 +0200 Subject: [PATCH 76/84] Added ToastParty plugin --- addons/toastparty/LICENSE | 21 ++ addons/toastparty/README.md | 43 ++++ addons/toastparty/fonts/Light.ttf | Bin 0 -> 40104 bytes addons/toastparty/fonts/Light.ttf.import | 36 ++++ addons/toastparty/plugin.cfg | 7 + addons/toastparty/toast-autoload.gd | 130 ++++++++++++ addons/toastparty/toast-autoload.gd.uid | 1 + addons/toastparty/toast_label/ToastLabel.gd | 198 ++++++++++++++++++ .../toastparty/toast_label/ToastLabel.gd.uid | 1 + .../toastparty/toast_label/toast_label.tscn | 33 +++ .../toast_label/toast_label_style.tres | 19 ++ addons/toastparty/toastparty.gd | 9 + addons/toastparty/toastparty.gd.uid | 1 + project.godot | 3 +- 14 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 addons/toastparty/LICENSE create mode 100644 addons/toastparty/README.md create mode 100644 addons/toastparty/fonts/Light.ttf create mode 100644 addons/toastparty/fonts/Light.ttf.import create mode 100644 addons/toastparty/plugin.cfg create mode 100644 addons/toastparty/toast-autoload.gd create mode 100644 addons/toastparty/toast-autoload.gd.uid create mode 100644 addons/toastparty/toast_label/ToastLabel.gd create mode 100644 addons/toastparty/toast_label/ToastLabel.gd.uid create mode 100644 addons/toastparty/toast_label/toast_label.tscn create mode 100644 addons/toastparty/toast_label/toast_label_style.tres create mode 100644 addons/toastparty/toastparty.gd create mode 100644 addons/toastparty/toastparty.gd.uid diff --git a/addons/toastparty/LICENSE b/addons/toastparty/LICENSE new file mode 100644 index 00000000..f66db9ed --- /dev/null +++ b/addons/toastparty/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Francisco Pereira Alvarado (gammafp) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/toastparty/README.md b/addons/toastparty/README.md new file mode 100644 index 00000000..bd571726 --- /dev/null +++ b/addons/toastparty/README.md @@ -0,0 +1,43 @@ +# Toast Party + +Have fun with this marvelous plugin for generating small toast notifications. + +Toast Party is a versatile plugin for Godot that allows you to easily create toast-style notifications in your games and applications. Add an extra layer of interactivity and visual feedback to your Godot projects with ease. Bring your messages to life with Toast Party! + +Plugin created by Francisco Pereira Alvarado ([gammafp](https://twitter.com/gammafp)). + +Please follow me on my social networks to follow my jobs: [LINKTREE](https://linktr.ee/gammafp) + +![Main Screen](/no-copy-imgs/example.gif) + +## Installation: + +1. Clone this repository into addons folder. +2. Enabled ToastParty, go to: Project > Project Settings > Plugins + +![Drag Racing](/no-copy-imgs/toast-party-install.png) + +## Use: + +ToastParty is an autoload singleton and is used as follows: + +```python +> ToastParty.show({ + "text": "🥑Some Text🥑", # Text (emojis can be used) + "bgcolor": Color(0, 0, 0, 0.7), # Background Color + "color": Color(1, 1, 1, 1), # Text Color + "gravity": "top", # top or bottom + "direction": "right", # left or center or right + "text_size": 18, # [optional] Text (font) size // experimental (warning!) + "use_font": true # [optional] Use custom ToastParty font // experimental (warning!) +}) +``` + +## Thanks to +@davcri + +## License + +Copyright (c) 2024 Francisco Pereira Alvarado (gammafp) + +Unless otherwise specified, files in this repository are licensed under the MIT license. See [LICENSE](LICENSE) for more information. diff --git a/addons/toastparty/fonts/Light.ttf b/addons/toastparty/fonts/Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b3f384d10d87d9e63ef0ce221d4b04f26dbf011c GIT binary patch literal 40104 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 literal 0 HcmV?d00001 diff --git a/addons/toastparty/fonts/Light.ttf.import b/addons/toastparty/fonts/Light.ttf.import new file mode 100644 index 00000000..9737c773 --- /dev/null +++ b/addons/toastparty/fonts/Light.ttf.import @@ -0,0 +1,36 @@ +[remap] + +importer="font_data_dynamic" +type="FontFile" +uid="uid://ccprmt0qqd56p" +path="res://.godot/imported/Light.ttf-0f836f71a282c0ba99b5d80af15b4036.fontdata" + +[deps] + +source_file="res://addons/toastparty/fonts/Light.ttf" +dest_files=["res://.godot/imported/Light.ttf-0f836f71a282c0ba99b5d80af15b4036.fontdata"] + +[params] + +Rendering=null +antialiasing=1 +generate_mipmaps=false +disable_embedded_bitmaps=true +multichannel_signed_distance_field=false +msdf_pixel_range=8 +msdf_size=48 +allow_system_fallback=true +force_autohinter=false +modulate_color_glyphs=false +hinting=1 +subpixel_positioning=4 +keep_rounding_remainders=true +oversampling=0.0 +Fallbacks=null +fallbacks=[] +Compress=null +compress=true +preload=[] +language_support={} +script_support={} +opentype_features={} diff --git a/addons/toastparty/plugin.cfg b/addons/toastparty/plugin.cfg new file mode 100644 index 00000000..5ced36c2 --- /dev/null +++ b/addons/toastparty/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="ToastParty" +description="" +author="Francisco Pereira - Gammafp" +version="1.1.0" +script="toastparty.gd" diff --git a/addons/toastparty/toast-autoload.gd b/addons/toastparty/toast-autoload.gd new file mode 100644 index 00000000..8ecde6c2 --- /dev/null +++ b/addons/toastparty/toast-autoload.gd @@ -0,0 +1,130 @@ +extends Node + +const label_resource = preload("toast_label/toast_label.tscn") + +var label_top_left = [] +var label_top_right = [] +var label_bottom_left = [] +var label_bottom_right = [] + +var label_top_center = [] +var label_bottom_center = [] + +# parent node +var canvas_layer: CanvasLayer + +# Called when the node enters the scene tree for the first time. +func _ready(): + canvas_layer = CanvasLayer.new() + canvas_layer.set_name("ToastPartyLayer") + canvas_layer.layer = 128 + add_child(canvas_layer) + + # TODO: We need Debounce function + # Connect signal resize to _on_resize + # get_tree().get_root().connect("size_changed", _on_resize, 1) + +func _add_new_label(config): + # Create a new label + var label = label_resource.instantiate() + canvas_layer.add_child(label) + label.connect("remove_label", remove_label_from_array) + + if config.direction == "left": + if config.gravity == "top": + label_top_left.insert(0, label) + else: + label_bottom_left.insert(0, label) + elif config.direction == "center": + if config.gravity == "top": + label_top_center.insert(0, label) + else: + label_bottom_center.insert(0, label) + else: + if config.gravity == "top": + label_top_right.insert(0, label) + else: + label_bottom_right.insert(0, label) + + # Configuration of the label + label.init(config) + + # Move all labels to new positions when a new label is added + move_positions(config.direction, config.gravity) + +func move_positions(direction, gravity): + if direction == "left" and gravity == "bottom": + for index in label_bottom_left.size(): + var _label = label_bottom_left[index] + _label.move_to(index) + + elif direction == "left" and gravity == "top": + for index in label_top_left.size(): + var _label = label_top_left[index] + _label.move_to(index) + + elif direction == "right" and gravity == "bottom": + for index in label_bottom_right.size(): + var _label = label_bottom_right[index] + _label.move_to(index) + + elif direction == "right" and gravity == "top": + for index in label_top_right.size(): + var _label = label_top_right[index] + _label.move_to(index) + + elif direction == "center" and gravity == "bottom": + for index in label_bottom_center.size(): + var _label = label_bottom_center[index] + _label.move_to(index) + + elif direction == "center" and gravity == "top": + for index in label_top_center.size(): + var _label = label_top_center[index] + _label.move_to(index) + + +func remove_label_from_array(label): + if label.direction == "left": + if label.gravity == "top": + label_top_left.erase(label) + else: + label_bottom_left.erase(label) + elif label.direction == "center": + if label.gravity == "top": + label_top_center.erase(label) + else: + label_bottom_center.erase(label) + else: + if label.gravity == "top": + label_top_right.erase(label) + else: + label_bottom_right.erase(label) + +## Event resize +func _on_resize(): + var toast_labels = label_top_left + label_top_right + label_bottom_left + label_bottom_right + label_top_center + label_bottom_center + for _label in toast_labels: + _label.update_x_position() + +func clean_config(config): + if not config.has("text"): + config.text = "🥑 toast party! 🥑" + + if not config.has("direction"): + config.direction = "right" + + if not config.has("gravity"): + config.gravity = "top" + + if not config.has("bgcolor"): + config.bgcolor = Color(0, 0, 0, 0.7) + + if not config.has("color"): + config.color = Color(1, 1, 1, 1) + + return config + +func show(config = {}): + var _config_cleaned = clean_config(config) + _add_new_label(_config_cleaned) diff --git a/addons/toastparty/toast-autoload.gd.uid b/addons/toastparty/toast-autoload.gd.uid new file mode 100644 index 00000000..229a8104 --- /dev/null +++ b/addons/toastparty/toast-autoload.gd.uid @@ -0,0 +1 @@ +uid://bpmj02udcqdo3 diff --git a/addons/toastparty/toast_label/ToastLabel.gd b/addons/toastparty/toast_label/ToastLabel.gd new file mode 100644 index 00000000..b2d52684 --- /dev/null +++ b/addons/toastparty/toast_label/ToastLabel.gd @@ -0,0 +1,198 @@ +extends Label + +signal remove_label(ToastLabel) + +var resolution = Vector2.ZERO + +# Text margin with box parent +const margins = {"left": 12, "top": 7, "right": 12, "bottom": 7} +# margin between buttons +const margin_between = 23 + +# offset position box with screen position +const offset_position = Vector2(10, 10) + +var button_size +var _tween_in: Tween + +# local variables +var gravity = "bottom" # top, bottom +var direction = "center" # left, right, center +var timer_to_destroy = 2 # seconds by default + + +func _ready(): + _set_resolution() + button_size = self.size + + # start position + _tween_destroy_label_timer() + +func _clean_config(config: Dictionary) -> Dictionary: + var _config = config + if not _config.has("text"): + _config["text"] = "Toast Label" + if not _config.has("text_size"): + _config["text_size"] = 18 + if not _config.has("bgcolor"): + _config["bgcolor"] = Color(0, 0, 0, .7) + if not _config.has("direction"): + _config["direction"] = "center" + if not _config.has("gravity"): + _config["gravity"] = "bottom" + if not _config.has("color"): + _config["color"] = Color(1, 1, 1, 1) + if not _config.has("use_font"): + _config["use_font"] = true + return _config + + +func init(config: Dictionary) -> void: + # TODO: add config validation + var config_cleaned = _clean_config(config) + + update_text(config_cleaned.text) + _set_bg_color(config_cleaned.bgcolor) + _set_color(config_cleaned.color) + _set_font(config_cleaned.use_font) + _set_text_size(config_cleaned.text_size) + + direction = config_cleaned.direction + gravity = config_cleaned.gravity + + position.y = get_y_pos(-100, gravity) + + _set_margins() + _set_shadow_direction() + + +func update_text(_text: String) -> void: + self.text = _text + button_size = self.size + update_x_position() + + +func move_to(index: int) -> void: + update_x_position() + + var offset_y = (margin_between + button_size.y) * index + var _y = get_y_pos(offset_y, gravity) + + # bottom + if index == 0: + _tween_in = get_tree().create_tween() + _tween_in.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS) # pause mode + _tween_in.stop() + var delayed = 0.03 + ( + _tween_in + . tween_property(self, "position", Vector2(position.x, _y), .3) + . set_trans(Tween.TRANS_QUINT) + . set_ease(Tween.EASE_IN) + . set_delay(delayed) + ) + _tween_in.play() + else: + _tween_in = get_tree().create_tween() + _tween_in.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS) # pause mode + _tween_in.stop() + ( + _tween_in + . tween_property(self, "position", Vector2(position.x, _y), .3) + . set_trans(Tween.TRANS_ELASTIC) + . set_ease(Tween.EASE_IN_OUT) + ) + _tween_in.play() + + +func _tween_destroy_label_complete() -> void: + # Send event complete + emit_signal("remove_label", self) + queue_free() + + +func _tween_destroy_label_timer(): + # tween alpha to 0 + var tween_alpha = get_tree().create_tween() + tween_alpha.set_pause_mode(Tween.TWEEN_PAUSE_PROCESS) # pause mode + tween_alpha.tween_property(self, "modulate:a", 0, 0.8).set_delay(timer_to_destroy) + tween_alpha.tween_callback(_tween_destroy_label_complete) + + +func get_y_pos(offset = 0, _gravity = "top") -> float: + # left position.x = margins.left + offset_position.x + var _y_pos = 0 + if _gravity == "top": + _y_pos = margins.top + offset_position.y + offset + else: + _y_pos = resolution.y - margins.top - button_size.y - offset_position.y - offset + return _y_pos + + +func update_x_position() -> void: + _set_resolution() + + if direction == "left": + position.x = margins.left + offset_position.x + elif direction == "center": + position.x = (resolution.x / 2) - (size.x / 2) + else: + position.x = resolution.x - margins.left - size.x - offset_position.x + +func _set_color(color: Color) -> void: + # set color + var theme_override = self.get("label_settings") + theme_override.set("font_color", color) + + +func _set_font(use_font: bool) -> void: + # set font + if use_font == false: + var theme_override = self.get("label_settings") + theme_override.set("font", null) + + +func _set_margins() -> void: + # set margins + var theme_override = self.get("theme_override_styles/normal") + theme_override.set("expand_margin_left", margins.left) + theme_override.set("expand_margin_top", margins.top) + theme_override.set("expand_margin_right", margins.right) + theme_override.set("expand_margin_bottom", margins.bottom) + + +func _set_shadow_direction() -> void: + # set shadow direction + var shadow_offset_abs = 2 + var theme_override = self.get("theme_override_styles/normal") + if gravity == "top" and direction == "left": + theme_override.set("shadow_offset", Vector2(-shadow_offset_abs, shadow_offset_abs)) + elif gravity == "top" and direction == "right": + theme_override.set("shadow_offset", Vector2(shadow_offset_abs, shadow_offset_abs)) + + elif gravity == "bottom" and direction == "left": + theme_override.set("shadow_offset", Vector2(-shadow_offset_abs, shadow_offset_abs)) + elif gravity == "bottom" and direction == "right": + theme_override.set("shadow_offset", Vector2(shadow_offset_abs, shadow_offset_abs)) + + elif gravity == "top" and direction == "center": + theme_override.set("shadow_offset", Vector2(0, shadow_offset_abs)) + elif gravity == "bottom" and direction == "center": + theme_override.set("shadow_offset", Vector2(0, shadow_offset_abs)) + + +func _set_text_size(text_size: int) -> void: + # set text size + var theme_override = self.get("label_settings") + theme_override.set("font_size", text_size) + + +func _set_bg_color(color: Color) -> void: + # set bg color + var theme_override = self.get("theme_override_styles/normal") + theme_override.set("bg_color", color) + + +func _set_resolution(): + resolution.x = get_viewport().get_visible_rect().size.x + resolution.y = get_viewport().get_visible_rect().size.y diff --git a/addons/toastparty/toast_label/ToastLabel.gd.uid b/addons/toastparty/toast_label/ToastLabel.gd.uid new file mode 100644 index 00000000..b02625ee --- /dev/null +++ b/addons/toastparty/toast_label/ToastLabel.gd.uid @@ -0,0 +1 @@ +uid://dqv8kbhts0beq diff --git a/addons/toastparty/toast_label/toast_label.tscn b/addons/toastparty/toast_label/toast_label.tscn new file mode 100644 index 00000000..5b5e7545 --- /dev/null +++ b/addons/toastparty/toast_label/toast_label.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=5 format=3 uid="uid://dugii0837nslt"] + +[ext_resource type="StyleBox" uid="uid://blt5gbhfsd0p5" path="res://addons/toastparty/toast_label/toast_label_style.tres" id="1_0l35p"] +[ext_resource type="FontFile" uid="uid://ctfeo8pmd6nse" path="res://addons/toastparty/fonts/Light.ttf" id="2_xpkfi"] +[ext_resource type="Script" path="res://addons/toastparty/toast_label/ToastLabel.gd" id="3_vwlmp"] + +[sub_resource type="LabelSettings" id="LabelSettings_o6str"] +resource_local_to_scene = true +line_spacing = 0.0 +font = ExtResource("2_xpkfi") +font_size = 18 +font_color = Color(0.94902, 0.94902, 0.94902, 1) +shadow_size = 0 + +[node name="Label" type="Label"] +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -55.0 +offset_top = -13.0 +offset_right = 56.0 +offset_bottom = 13.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_font_sizes/font_size = 18 +theme_override_styles/normal = ExtResource("1_0l35p") +text = "Toast Party" +label_settings = SubResource("LabelSettings_o6str") +horizontal_alignment = 1 +vertical_alignment = 1 +script = ExtResource("3_vwlmp") diff --git a/addons/toastparty/toast_label/toast_label_style.tres b/addons/toastparty/toast_label/toast_label_style.tres new file mode 100644 index 00000000..708fc043 --- /dev/null +++ b/addons/toastparty/toast_label/toast_label_style.tres @@ -0,0 +1,19 @@ +[gd_resource type="StyleBoxFlat" format=3 uid="uid://blt5gbhfsd0p5"] + +[resource] +resource_local_to_scene = true +bg_color = Color(0, 0, 0, 0.835294) +border_color = Color(0, 0, 0, 1) +corner_radius_top_left = 2 +corner_radius_top_right = 2 +corner_radius_bottom_right = 2 +corner_radius_bottom_left = 2 +corner_detail = 10 +expand_margin_left = 15.0 +expand_margin_top = 10.0 +expand_margin_right = 15.0 +expand_margin_bottom = 10.0 +shadow_color = Color(0, 0, 0, 0.607843) +shadow_size = 2 +shadow_offset = Vector2(-2, 2) +anti_aliasing = false diff --git a/addons/toastparty/toastparty.gd b/addons/toastparty/toastparty.gd new file mode 100644 index 00000000..85806143 --- /dev/null +++ b/addons/toastparty/toastparty.gd @@ -0,0 +1,9 @@ +@tool +extends EditorPlugin + +const AUTOLOAD_NAME = "ToastParty" +func _enter_tree(): + add_autoload_singleton(AUTOLOAD_NAME, "res://addons/toastparty/toast-autoload.gd") + +func _exit_tree(): + remove_autoload_singleton(AUTOLOAD_NAME) diff --git a/addons/toastparty/toastparty.gd.uid b/addons/toastparty/toastparty.gd.uid new file mode 100644 index 00000000..9a6fc419 --- /dev/null +++ b/addons/toastparty/toastparty.gd.uid @@ -0,0 +1 @@ +uid://cy2q5qgbyjvjq diff --git a/project.godot b/project.godot index 6adb4d9f..fa9451eb 100644 --- a/project.godot +++ b/project.godot @@ -20,10 +20,11 @@ config/icon="res://addons/godot-playfab/icon.png" SceneManager="*res://Scripts/SceneManager.gd" PlayFabManager="*res://addons/godot-playfab/PlayFabManager.gd" +ToastParty="*res://addons/toastparty/toast-autoload.gd" [editor_plugins] -enabled=PackedStringArray("res://addons/godot-playfab/plugin.cfg", "res://addons/gdUnit4/plugin.cfg") +enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg", "res://addons/godot-playfab/plugin.cfg", "res://addons/toastparty/plugin.cfg") [filesystem] From f15e8ff2d8f39972e7ad542eac441d6eb48ae5a5 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:34:13 +0200 Subject: [PATCH 77/84] cleanup --- Scenes/Widgets/ItemCard.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd index 5cc5db25..bb128bf5 100644 --- a/Scenes/Widgets/ItemCard.gd +++ b/Scenes/Widgets/ItemCard.gd @@ -13,7 +13,7 @@ func reset(catalog_item: CatalogItem) -> void: for currency_amount in price.Amounts: var currency := PlayFabManager.catalog.resolve_item(currency_amount.ItemId) - text += "| %s (%s)" % [currency_amount.Amount, currency.Title.get("NEUTRAL")] + text += "| %s (%s)" % [currency_amount.Amount, currency.Title.get(PlayFab.LANG_NEUTRAL)] var buy_button = Button.new() buy_button.text = text From 350d9341191fe747d8e7e429f9626e2bddd44838 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:43:34 +0200 Subject: [PATCH 78/84] Added Toast for purchase confirmation --- Scenes/Widgets/ItemCard.gd | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Scenes/Widgets/ItemCard.gd b/Scenes/Widgets/ItemCard.gd index bb128bf5..436e6fef 100644 --- a/Scenes/Widgets/ItemCard.gd +++ b/Scenes/Widgets/ItemCard.gd @@ -31,7 +31,16 @@ func reset_inventory(catalog_item: CatalogItem, inventory_item: InventoryItem) - func _on_purchase_button_pressed(amount: int, catalog_item: CatalogItem) -> void: var request_data := PurchaseInventoryItemsRequest.from_catalog_item(amount, catalog_item) - PlayFabManager.inventory.purchase_inventory_items(request_data, func(response: PurchaseInventoryItemsResponse): - pass - # TODO: Update inventory, currency + PlayFabManager.inventory.purchase_inventory_items(request_data, func(_response: PurchaseInventoryItemsResponse): + PlayFabManager.inventory.turboload_inventory(func(): + ToastParty.show({ + "text": "🪙 Purchased %sx %s." % [amount, catalog_item.Title.get(PlayFab.LANG_NEUTRAL)], # Text (emojis can be used) + "bgcolor": Color(0, 0, 0, 0.7), # Background Color + "color": Color(1, 1, 1, 1), # Text Color + "gravity": "top", # top or bottom + "direction": "right", # left or center or right + "text_size": 18, # [optional] Text (font) size // experimental (warning!) + "use_font": false # [optional] Use custom ToastParty font // experimental (warning!) + }) + ) ) From 2cd9efcc0e2a2809a68f846943e3895adf8be2d1 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:44:28 +0200 Subject: [PATCH 79/84] Increased default toast timeout from 2 to 5 seconds --- addons/toastparty/toast_label/ToastLabel.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/toastparty/toast_label/ToastLabel.gd b/addons/toastparty/toast_label/ToastLabel.gd index b2d52684..b1678f8d 100644 --- a/addons/toastparty/toast_label/ToastLabel.gd +++ b/addons/toastparty/toast_label/ToastLabel.gd @@ -18,7 +18,7 @@ var _tween_in: Tween # local variables var gravity = "bottom" # top, bottom var direction = "center" # left, right, center -var timer_to_destroy = 2 # seconds by default +var timer_to_destroy = 5 # seconds by default func _ready(): From 359e0db55db8f9f43362c89a9a558f1d095d8e02 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:45:02 +0200 Subject: [PATCH 80/84] Fixed: non UUID reference in ToastLabel scene --- addons/toastparty/toast_label/toast_label.tscn | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/toastparty/toast_label/toast_label.tscn b/addons/toastparty/toast_label/toast_label.tscn index 5b5e7545..36b1be1d 100644 --- a/addons/toastparty/toast_label/toast_label.tscn +++ b/addons/toastparty/toast_label/toast_label.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=5 format=3 uid="uid://dugii0837nslt"] [ext_resource type="StyleBox" uid="uid://blt5gbhfsd0p5" path="res://addons/toastparty/toast_label/toast_label_style.tres" id="1_0l35p"] -[ext_resource type="FontFile" uid="uid://ctfeo8pmd6nse" path="res://addons/toastparty/fonts/Light.ttf" id="2_xpkfi"] -[ext_resource type="Script" path="res://addons/toastparty/toast_label/ToastLabel.gd" id="3_vwlmp"] +[ext_resource type="FontFile" uid="uid://ccprmt0qqd56p" path="res://addons/toastparty/fonts/Light.ttf" id="2_xpkfi"] +[ext_resource type="Script" uid="uid://dqv8kbhts0beq" path="res://addons/toastparty/toast_label/ToastLabel.gd" id="3_vwlmp"] [sub_resource type="LabelSettings" id="LabelSettings_o6str"] resource_local_to_scene = true From 9b05ff293840c955732d719ccc9353c70aac449a Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 22:45:08 +0200 Subject: [PATCH 81/84] cleanup --- addons/godot-playfab/Models/CatalogPriceOptions.gd | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/godot-playfab/Models/CatalogPriceOptions.gd b/addons/godot-playfab/Models/CatalogPriceOptions.gd index d589d8f4..7954de54 100644 --- a/addons/godot-playfab/Models/CatalogPriceOptions.gd +++ b/addons/godot-playfab/Models/CatalogPriceOptions.gd @@ -14,4 +14,3 @@ func _get_type_for_property(property_name: String) -> String: push_error("Could not find mapping for property: " + property_name) return super._get_type_for_property(property_name) - From 2b64cb77da893f3190b564b258d709cb8c1fde8f Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 23:08:03 +0200 Subject: [PATCH 82/84] Fix root node name of inventory --- Scenes/Economy/Inventory.tscn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenes/Economy/Inventory.tscn b/Scenes/Economy/Inventory.tscn index 067d414e..9c69a674 100644 --- a/Scenes/Economy/Inventory.tscn +++ b/Scenes/Economy/Inventory.tscn @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://bdlfoyj3jgt24" path="res://Scenes/Economy/Inventory.gd" id="1_ersrd"] [ext_resource type="PackedScene" uid="uid://wwvhjjuojn77" path="res://Scenes/Widgets/LoadingIndicator.tscn" id="2_s30pp"] -[node name="Invenory" type="Control"] +[node name="Inventory" type="Control"] layout_mode = 3 anchors_preset = 15 anchor_right = 1.0 From 1b2b9786aba663f0f585863e29093cdb13b5105d Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sat, 11 Oct 2025 23:08:30 +0200 Subject: [PATCH 83/84] fix size flags for Catalog & Inventory --- Scenes/Economy/Catalog.tscn | 2 ++ Scenes/Economy/Inventory.tscn | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Scenes/Economy/Catalog.tscn b/Scenes/Economy/Catalog.tscn index f0a18bfd..c827df86 100644 --- a/Scenes/Economy/Catalog.tscn +++ b/Scenes/Economy/Catalog.tscn @@ -10,6 +10,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +size_flags_horizontal = 6 +size_flags_vertical = 6 script = ExtResource("1_lnqhp") [node name="VBoxContainer" type="VBoxContainer" parent="."] diff --git a/Scenes/Economy/Inventory.tscn b/Scenes/Economy/Inventory.tscn index 9c69a674..2b06df82 100644 --- a/Scenes/Economy/Inventory.tscn +++ b/Scenes/Economy/Inventory.tscn @@ -10,6 +10,8 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +size_flags_horizontal = 6 +size_flags_vertical = 6 script = ExtResource("1_ersrd") [node name="VBoxContainer" type="VBoxContainer" parent="."] From 004af07cc1f714d1136b9a9adebae08611122483 Mon Sep 17 00:00:00 2001 From: Johannes Ebner Date: Sun, 12 Oct 2025 00:52:06 +0200 Subject: [PATCH 84/84] Add Steam ItemDem.json --- config/Steam/ItemDem.json | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 config/Steam/ItemDem.json diff --git a/config/Steam/ItemDem.json b/config/Steam/ItemDem.json new file mode 100644 index 00000000..9699e8d1 --- /dev/null +++ b/config/Steam/ItemDem.json @@ -0,0 +1,50 @@ +{ + "appid": 2908850, + "items": [ + { + "itemdefid": 1, + "type": "item", + "name": "Godots 25", + "description": "25 Godots pack.", + "price": "1;VLV25", + "name_color": "7D6D00", + "background_color": "3C352E", + "icon_url": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "icon_url_large": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "store_tags": "Godots", + "tradable": false, + "marketable": false, + "auto_stack": true + }, + { + "itemdefid": 2, + "type": "item", + "name": "Godots 50", + "description": "50 Godots value pack.", + "price": "1;VLV50", + "name_color": "7D6D00", + "background_color": "3C352E", + "icon_url": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "icon_url_large": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "store_tags": "Godots", + "tradable": false, + "marketable": false, + "auto_stack": true + }, + { + "itemdefid": 3, + "type": "item", + "name": "Godots 75", + "description": "75 Godots super saver pack.", + "price": "1;VLV75", + "name_color": "7D6D00", + "background_color": "3C352E", + "icon_url": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "icon_url_large": "https://content1.prod.catalog.playfab.com/pf-title-1a64f85c3341b8ed-b5f9b/9a487eef-e468-4588-906e-d58c78dc8d05/shortcut_icon.png", + "store_tags": "Godots", + "tradable": false, + "marketable": false, + "auto_stack": true + } + ] +}