From 98834378f2379503c7a7c69858b7dbfa2ff546aa Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 11:51:43 -0800 Subject: [PATCH 01/30] fix: Subscription status is reported properly --- .../src/Shared/Ownership/PlayerAssetOwnershipTracker.lua | 9 +++------ .../src/Shared/Trackers/PlayerProductManagerBase.lua | 5 ++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua index 0aaab85d3a..fe2b00714d 100644 --- a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua +++ b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua @@ -193,7 +193,7 @@ function PlayerAssetOwnershipTracker:PromiseOwnsAsset(idOrKey) end end -local id = self._configPicker:ToAssetId(self._assetType, idOrKey) + local id = self._configPicker:ToAssetId(self._assetType, idOrKey) if id then if self._ownedAssetIdSet:Contains(id) then return Promise.resolved(true) @@ -283,11 +283,8 @@ function PlayerAssetOwnershipTracker:_cacheWellKnownAssets() return end - local gameConfigMaid = brio:ToMaid() - local gameConfigAsset = brio:GetValue() - - local wellKnownHandler = WellKnownAssetOwnershipHandler.new(self._player, gameConfigAsset) - gameConfigMaid:GiveTask(wellKnownHandler) + local gameConfigMaid, gameConfigAsset = brio:ToMaidAndValue() + local wellKnownHandler = gameConfigMaid:Add(WellKnownAssetOwnershipHandler.new(self._player, gameConfigAsset)) gameConfigMaid:GiveTask(Rx.combineLatest({ ownershipCallback = self._ownershipCallback:Observe(); diff --git a/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua b/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua index 649e9ae40e..d291b66d87 100644 --- a/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua +++ b/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua @@ -101,7 +101,10 @@ function PlayerProductManagerBase.new(player, serviceBag) end) subscriptionOwnership:SetQueryOwnershipCallback(function(subscriptionId) - return MarketplaceUtils.promisePlayerOwnsBundle(self._player, subscriptionId) + return MarketplaceUtils.promiseUserSubscriptionStatus(self._player, subscriptionId) + :Then(function(status) + return status.IsSubscribed == true + end) end) membershipOwnership:SetQueryOwnershipCallback(function(membershipType) From 637d0ae5bff3c1ca0ceb67137c9c5e08cbe43da8 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 11:55:09 -0800 Subject: [PATCH 02/30] docs: Update docsfor the PlayerAssetMarketTracker --- .../Trackers/PlayerAssetMarketTracker.lua | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua b/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua index 904f4ed556..5953a435f8 100644 --- a/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua +++ b/src/gameproductservice/src/Shared/Trackers/PlayerAssetMarketTracker.lua @@ -19,6 +19,9 @@ PlayerAssetMarketTracker.ClassName = "PlayerAssetMarketTracker" PlayerAssetMarketTracker.__index = PlayerAssetMarketTracker --[=[ + Constructs a new market tracker. Generally you should not need to use + this directly. + @param assetType GameConfigAssetTypes @param convertIds function @param observeIdsBrio function @@ -73,6 +76,11 @@ function PlayerAssetMarketTracker.new(assetType, convertIds, observeIdsBrio) return self end +--[=[ + Observes the number of prompts open + + @return Observable +]=] function PlayerAssetMarketTracker:ObservePromptOpenCount() return self._promptsOpenCount:Observe() end @@ -118,6 +126,11 @@ function PlayerAssetMarketTracker:ObserveAssetPurchased(idOrKey) end) end +--[=[ + Gets the ownership tracker assigned to this tracker + + @return GetOwnershipTracker +]=] function PlayerAssetMarketTracker:GetOwnershipTracker() return self._ownershipTracker end @@ -208,6 +221,11 @@ function PlayerAssetMarketTracker:SetOwnershipTracker(ownershipTracker) self._ownershipTracker = ownershipTracker end +--[=[ + Gets the current asset type + + @return GameConfigAssetTypes +]=] function PlayerAssetMarketTracker:GetAssetType() return self._assetType end @@ -262,6 +280,11 @@ function PlayerAssetMarketTracker:HandlePurchaseEvent(id, isPurchased) purchasePromise:Resolve(isPurchased) end +--[=[ + Handles the prompt closed + + @param id number +]=] function PlayerAssetMarketTracker:HandlePromptClosedEvent(id) assert(type(id) == "number", "Bad id") From 8afb7cc1d160025f53041c6fa5c987078712fa5f Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:56:51 -0800 Subject: [PATCH 03/30] feat: Add PagesProxy to allow caching of pages --- src/pagesutils/package.json | 1 + src/pagesutils/src/Shared/PagesUtils.lua | 3 +- .../src/Shared/Proxy/PagesDatabase.lua | 62 ++++++++++++++++ .../src/Shared/Proxy/PagesProxy.lua | 72 +++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 src/pagesutils/src/Shared/Proxy/PagesDatabase.lua create mode 100644 src/pagesutils/src/Shared/Proxy/PagesProxy.lua diff --git a/src/pagesutils/package.json b/src/pagesutils/package.json index f927494dd2..fb45fee6b8 100644 --- a/src/pagesutils/package.json +++ b/src/pagesutils/package.json @@ -26,6 +26,7 @@ "Quenty" ], "dependencies": { + "@quenty/ducktype": "file:../ducktype", "@quenty/loader": "file:../loader", "@quenty/promise": "file:../promise" }, diff --git a/src/pagesutils/src/Shared/PagesUtils.lua b/src/pagesutils/src/Shared/PagesUtils.lua index c1480aad94..b6f15aed12 100644 --- a/src/pagesutils/src/Shared/PagesUtils.lua +++ b/src/pagesutils/src/Shared/PagesUtils.lua @@ -6,6 +6,7 @@ local require = require(script.Parent.loader).load(script) local Promise = require("Promise") +local PagesProxy = require("PagesProxy") local PagesUtils = {} @@ -16,7 +17,7 @@ local PagesUtils = {} @return { any } ]=] function PagesUtils.promiseAdvanceToNextPage(pages) - assert(typeof(pages) == "Instance" and pages:IsA("Pages"),"Bad pages") + assert(typeof(pages) == "Instance" and pages:IsA("Pages") or PagesProxy.isPagesProxy(pages),"Bad pages") return Promise.spawn(function(resolve, reject) local ok, err = pcall(function() diff --git a/src/pagesutils/src/Shared/Proxy/PagesDatabase.lua b/src/pagesutils/src/Shared/Proxy/PagesDatabase.lua new file mode 100644 index 0000000000..2bb76021ce --- /dev/null +++ b/src/pagesutils/src/Shared/Proxy/PagesDatabase.lua @@ -0,0 +1,62 @@ +--[=[ + @class PagesDatabase +]=] + +local require = require(script.Parent.loader).load(script) + +local DuckTypeUtils = require("DuckTypeUtils") + +local PagesDatabase = {} +PagesDatabase.ClassName = "PagesDatabase" +PagesDatabase.__index = PagesDatabase + +function PagesDatabase.new(pages) + local self = setmetatable({}, PagesDatabase) + + self._pages = assert(pages, "No pages") + self._lastIncrementedIndex = 1 + self._pageData = {} + + self:_storeState() + + return self +end + +function PagesDatabase.isPagesDatabase(value) + return DuckTypeUtils.isImplementation(PagesDatabase, value) +end + +function PagesDatabase:IncrementToPageIdAsync(pageId) + while self._lastIncrementedIndex < pageId do + self._lastIncrementedIndex += 1 + self._pages:AdvanceToNextPageAsync() + self:_storeState() + end +end + +function PagesDatabase:GetPage(pageId) + assert(type(pageId) == "number", "Bad pageId") + + return self:_getPageState(pageId).currentPage +end + +function PagesDatabase:GetIsFinished(pageId) + assert(type(pageId) == "number", "Bad pageId") + + return self:_getPageState(pageId).isFinished +end + +function PagesDatabase:_getPageState(pageId) + assert(pageId > 0 and pageId <= self._lastIncrementedIndex, "pageId is out of bounds") + + return assert(self._pageData[pageId], "Missing data") +end + +function PagesDatabase:_storeState() + self._pageData[self._lastIncrementedIndex] = { + currentPage = self._pages:GetCurrentPage(); + isFinished = self._pages.IsFinished; + } +end + +return PagesDatabase \ No newline at end of file diff --git a/src/pagesutils/src/Shared/Proxy/PagesProxy.lua b/src/pagesutils/src/Shared/Proxy/PagesProxy.lua new file mode 100644 index 0000000000..fdab71c0d8 --- /dev/null +++ b/src/pagesutils/src/Shared/Proxy/PagesProxy.lua @@ -0,0 +1,72 @@ +--[=[ + Proxy pages and cache the results to allow for reuse + + @class PagesProxy +]=] + +local require = require(script.Parent.loader).load(script) + +local DuckTypeUtils = require("DuckTypeUtils") +local PagesDatabase = require("PagesDatabase") + +local PagesProxy = {} +PagesProxy.ClassName = "PagesProxy" +PagesProxy.__index = PagesProxy + +function PagesProxy.new(database) + local self = setmetatable({}, PagesProxy) + + if PagesDatabase.isPagesDatabase(database) then + self._database = database + elseif typeof(database) == "Instance" and database:IsA("Pages") then + -- Convenient for consumers + self._database = PagesDatabase.new(database) + else + error("Bad database") + end + + self._currentPageIndex = 1 + + return self +end + +function PagesProxy.isPagesProxy(value) + return DuckTypeUtils.isImplementation(PagesProxy, value) +end + +function PagesProxy:AdvanceToNextPageAsync() + if self._database:GetIsFinished(self._currentPageIndex) then + error("Already finished, cannot increment more") + end + + self._currentPageIndex += 1 + + return self._database:IncrementToPageIdAsync(self._currentPageIndex) +end + +function PagesProxy:GetCurrentPage() + return self._database:GetPage(self._currentPageIndex) +end + +function PagesProxy:Clone() + local copy = PagesProxy.new(self._database) + copy._currentPageIndex = self._currentPageIndex + + return copy +end + +function PagesProxy:__index(index) + if index == nil then + error("Attempt to index with a nil value") + elseif PagesProxy[index] then + return PagesProxy[index] + elseif index == "IsFinished" then + return self._database:GetIsFinished(self._currentPageIndex) + elseif type(index) == "string" then + return rawget(self, index) + else + error("Bad index") + end +end + +return PagesProxy \ No newline at end of file From aaa37796c3c72d19ac43591fb7bc86e5b2199af5 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:57:06 -0800 Subject: [PATCH 04/30] refactor: Move aggregation logic into shared module --- src/aggregator/README.md | 23 +++ src/aggregator/default.project.json | 7 + src/aggregator/package.json | 37 +++++ src/aggregator/src/Shared/Aggregator.lua | 134 ++++++++++++++++++ src/aggregator/src/node_modules.project.json | 7 + src/aggregator/test/default.project.json | 12 ++ src/userserviceutils/package.json | 1 + .../src/Shared/UserInfoAggregator.lua | 111 +++------------ 8 files changed, 243 insertions(+), 89 deletions(-) create mode 100644 src/aggregator/README.md create mode 100644 src/aggregator/default.project.json create mode 100644 src/aggregator/package.json create mode 100644 src/aggregator/src/Shared/Aggregator.lua create mode 100644 src/aggregator/src/node_modules.project.json create mode 100644 src/aggregator/test/default.project.json diff --git a/src/aggregator/README.md b/src/aggregator/README.md new file mode 100644 index 0000000000..498d18fd31 --- /dev/null +++ b/src/aggregator/README.md @@ -0,0 +1,23 @@ +## Aggregator + + + +Aggregates async promise requests + + + +## Installation + +``` +npm install @quenty/aggregator --save +``` diff --git a/src/aggregator/default.project.json b/src/aggregator/default.project.json new file mode 100644 index 0000000000..d543baf7af --- /dev/null +++ b/src/aggregator/default.project.json @@ -0,0 +1,7 @@ +{ + "name": "aggregator", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": "src" + } +} diff --git a/src/aggregator/package.json b/src/aggregator/package.json new file mode 100644 index 0000000000..84f1e0c844 --- /dev/null +++ b/src/aggregator/package.json @@ -0,0 +1,37 @@ +{ + "name": "@quenty/aggregator", + "version": "1.0.0", + "description": "Aggregates async promise requests", + "keywords": [ + "Roblox", + "Nevermore", + "Lua", + "aggregator" + ], + "bugs": { + "url": "https://github.com/Quenty/NevermoreEngine/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Quenty/NevermoreEngine.git", + "directory": "src/aggregator/" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/quenty" + }, + "license": "MIT", + "contributors": [ + "Quenty" + ], + "dependencies": { + "@quenty/baseobject": "file:../baseobject", + "@quenty/loader": "file:../loader", + "@quenty/lrucache": "file:../lrucache", + "@quenty/promise": "file:../promise", + "@quenty/rx": "file:../rx" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/src/aggregator/src/Shared/Aggregator.lua b/src/aggregator/src/Shared/Aggregator.lua new file mode 100644 index 0000000000..a4d78461b9 --- /dev/null +++ b/src/aggregator/src/Shared/Aggregator.lua @@ -0,0 +1,134 @@ +--[=[ + Aggregates all requests into one big send request to deduplicate the request + + @class Aggregator +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local Promise = require("Promise") +local Rx = require("Rx") +local LRUCache = require("LRUCache") + +local Aggregator = setmetatable({}, BaseObject) +Aggregator.ClassName = "Aggregator" +Aggregator.__index = Aggregator + +function Aggregator.new(debugName, promiseBulkQuery) + assert(type(debugName) == "string", "Bad debugName") + + local self = setmetatable(BaseObject.new(), Aggregator) + + self._debugName = debugName + self._promiseBulkQuery = assert(promiseBulkQuery, "No promiseBulkQuery") + + -- TODO: LRU cache this? Limit to 1k or something? + self._promisesLruCache = LRUCache.new(2000) + + self._maxPerRequest = 200 + self._unsentCount = 0 + self._unsentPromises = {} + + return self +end + +function Aggregator:Promise(id) + assert(type(id) == "number", "Bad id") + + local found = self._promisesLruCache:get(id) + if found then + return found + end + + local promise = Promise.new() + + self._unsentPromises[id] = promise + self._unsentCount = self._unsentCount + 1 + self._promisesLruCache:set(id, promise) + + self:_queueAggregatedPromises() + + return promise +end + +--[=[ + Observes the user display name for the id + + @param id number + @return Observable +]=] +function Aggregator:Observe(id) + assert(type(id) == "number", "Bad id") + + return Rx.fromPromise(self:Promise(id)) +end + +function Aggregator:_sendAggregatedPromises(promiseMap) + assert(promiseMap, "No promiseMap") + + local idList = {} + local unresolvedMap = {} + for id, promise in pairs(promiseMap) do + table.insert(idList, id) + unresolvedMap[id] = promise + end + + if #idList == 0 then + return + end + + assert(#idList <= self._maxPerRequest, "Too many idList sent") + + self._maid:GivePromise(self._promiseBulkQuery(idList)) + :Then(function(result) + assert(type(result) == "table", "Bad result") + + for _, data in pairs(result) do + assert(type(data.Id) == "number", "Bad result[?].Id") + + if unresolvedMap[data.Id] then + unresolvedMap[data.Id]:Resolve(data) + unresolvedMap[data.Id] = nil + end + end + + -- Reject other ones + for id, promise in pairs(unresolvedMap) do + promise:Reject(string.format("Aggregated %s failed to get result for id %d", self._debugName, id)) + end + end, function(...) + for _, item in pairs(unresolvedMap) do + item:Reject(...) + end + end) +end + +function Aggregator:_resetQueue() + local promiseMap = self._unsentPromises + + self._maid._queue = nil + self._unsentCount = 0 + self._unsentPromises = {} + + return promiseMap +end + +function Aggregator:_queueAggregatedPromises() + if self._unsentCount >= self._maxPerRequest then + self:_sendAggregatedPromises(self:_resetQueue()) + return + end + + if self._maid._queue then + return + end + + self._maid._queue = task.delay(0.1, function() + task.spawn(function() + self:_sendAggregatedPromises(self:_resetQueue()) + end) + end) +end + +return Aggregator \ No newline at end of file diff --git a/src/aggregator/src/node_modules.project.json b/src/aggregator/src/node_modules.project.json new file mode 100644 index 0000000000..46233dac4f --- /dev/null +++ b/src/aggregator/src/node_modules.project.json @@ -0,0 +1,7 @@ +{ + "name": "node_modules", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": { "optional": "../node_modules" } + } +} \ No newline at end of file diff --git a/src/aggregator/test/default.project.json b/src/aggregator/test/default.project.json new file mode 100644 index 0000000000..17d08c4bcd --- /dev/null +++ b/src/aggregator/test/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "AggregatorTest", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "aggregator": { + "$path": ".." + } + } + } +} \ No newline at end of file diff --git a/src/userserviceutils/package.json b/src/userserviceutils/package.json index c1a50e9ddc..ed301153bb 100644 --- a/src/userserviceutils/package.json +++ b/src/userserviceutils/package.json @@ -25,6 +25,7 @@ "Quenty" ], "dependencies": { + "@quenty/aggregator": "file:../aggregator", "@quenty/baseobject": "file:../baseobject", "@quenty/loader": "file:../loader", "@quenty/maid": "file:../maid", diff --git a/src/userserviceutils/src/Shared/UserInfoAggregator.lua b/src/userserviceutils/src/Shared/UserInfoAggregator.lua index 9ea0118f94..6e9002c1f0 100644 --- a/src/userserviceutils/src/Shared/UserInfoAggregator.lua +++ b/src/userserviceutils/src/Shared/UserInfoAggregator.lua @@ -6,12 +6,10 @@ local require = require(script.Parent.loader).load(script) +local Aggregator = require("Aggregator") local BaseObject = require("BaseObject") -local Promise = require("Promise") -local UserServiceUtils = require("UserServiceUtils") local Rx = require("Rx") - -local MAX_USER_IDS_PER_REQUEST = 200 +local UserServiceUtils = require("UserServiceUtils") local UserInfoAggregator = setmetatable({}, BaseObject) UserInfoAggregator.ClassName = "UserInfoAggregator" @@ -20,11 +18,9 @@ UserInfoAggregator.__index = UserInfoAggregator function UserInfoAggregator.new() local self = setmetatable(BaseObject.new(), UserInfoAggregator) - -- TODO: LRU cache this? Limit to 1k or something? - self._promises = {} - - self._unsentCount = 0 - self._unsentPromises = {} + self._aggregator = self._maid:Add(Aggregator.new("UserServiceUtils.promiseUserInfosByUserIds", function(userIdList) + return UserServiceUtils.promiseUserInfosByUserIds(userIdList) + end)) return self end @@ -39,19 +35,7 @@ end function UserInfoAggregator:PromiseUserInfo(userId) assert(type(userId) == "number", "Bad userId") - if self._promises[userId] then - return self._promises[userId] - end - - local promise = Promise.new() - - self._unsentPromises[userId] = promise - self._unsentCount = self._unsentCount + 1 - self._promises[userId] = promise - - self:_queueAggregatedPromises() - - return promise + return self._aggregator:Promise(userId) end --[=[ @@ -63,7 +47,7 @@ end function UserInfoAggregator:PromiseDisplayName(userId) assert(type(userId) == "number", "Bad userId") - return self:PromiseUserInfo(userId) + return self._aggregator:Promise(userId) :Then(function(userInfo) return userInfo.DisplayName end) @@ -78,7 +62,7 @@ end function UserInfoAggregator:PromiseDisplayName(userId) assert(type(userId) == "number", "Bad userId") - return self:PromiseUserInfo(userId) + return self._aggregator:Promise(userId) :Then(function(userInfo) return userInfo.DisplayName end) @@ -93,7 +77,7 @@ end function UserInfoAggregator:PromiseHasVerifiedBadge(userId) assert(type(userId) == "number", "Bad userId") - return self:PromiseUserInfo(userId) + return self._aggregator:Promise(userId) :Then(function(userInfo) return userInfo.HasVerifiedBadge end) @@ -108,7 +92,7 @@ end function UserInfoAggregator:ObserveUserInfo(userId) assert(type(userId) == "number", "Bad userId") - return Rx.fromPromise(self:PromiseUserInfo(userId)) + return self._aggregator:Observe(userId) end --[=[ @@ -120,78 +104,27 @@ end function UserInfoAggregator:ObserveDisplayName(userId) assert(type(userId) == "number", "Bad userId") - return self:ObserveUserInfo():Pipe({ + return self._aggregator:Observe(userId):Pipe({ Rx.map(function(userInfo) return userInfo.DisplayName end) }) end -function UserInfoAggregator:_sendAggregatedPromises(promiseMap) - assert(promiseMap, "No promiseMap") - - local userIds = {} - local unresolvedMap = {} - for userId, promise in pairs(promiseMap) do - table.insert(userIds, userId) - unresolvedMap[userId] = promise - end - - if #userIds == 0 then - return - end - - assert(#userIds <= MAX_USER_IDS_PER_REQUEST, "Too many userIds sent") - - self._maid:GivePromise(UserServiceUtils.promiseUserInfosByUserIds(userIds)) - :Then(function(result) - assert(type(result) == "table", "Bad result") - - for _, data in pairs(result) do - assert(type(data.Id) == "number", "Bad result[?].Id") - - if unresolvedMap[data.Id] then - unresolvedMap[data.Id]:Resolve(data) - unresolvedMap[data.Id] = nil - end - end - - -- Reject other ones - for userId, promise in pairs(unresolvedMap) do - promise:Reject(string.format("Failed to get result for userId %d", userId)) - end - end, function(...) - for _, item in pairs(unresolvedMap) do - item:Reject(...) - end - end) -end - -function UserInfoAggregator:_resetQueue() - local promiseMap = self._unsentPromises - - self._maid._queue = nil - self._unsentCount = 0 - self._unsentPromises = {} - - return promiseMap -end - -function UserInfoAggregator:_queueAggregatedPromises() - if self._unsentCount >= MAX_USER_IDS_PER_REQUEST then - self:_sendAggregatedPromises(self:_resetQueue()) - return - end +--[=[ + Observes the user display name for the userId - if self._maid._queue then - return - end + @param userId number + @return Observable +]=] +function UserInfoAggregator:ObserveHasVerifiedBadge(userId) + assert(type(userId) == "number", "Bad userId") - self._maid._queue = task.delay(0.1, function() - task.spawn(function() - self:_sendAggregatedPromises(self:_resetQueue()) + return self._aggregator:Observe(userId):Pipe({ + Rx.map(function(userInfo) + return userInfo.HasVerifiedBadge end) - end) + }) end return UserInfoAggregator \ No newline at end of file From 9d2390fe02b287b8ba85387676935c461953b929 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:57:15 -0800 Subject: [PATCH 05/30] docs: Fix receipt processing docs --- src/receiptprocessing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/receiptprocessing/README.md b/src/receiptprocessing/README.md index 1983e1942c..5218a71a07 100644 --- a/src/receiptprocessing/README.md +++ b/src/receiptprocessing/README.md @@ -14,7 +14,7 @@ Centralize receipt processing within games since this is a constrained resource. - + ## Installation From 8995ffc94abe3e6e34f6a924f275177e7a22b641 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:57:39 -0800 Subject: [PATCH 06/30] fix: Reject promises when failing to succeed prompt --- .../src/Shared/AvatarEditorUtils.lua | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/avatareditorutils/src/Shared/AvatarEditorUtils.lua b/src/avatareditorutils/src/Shared/AvatarEditorUtils.lua index af9b5e3f64..2671718231 100644 --- a/src/avatareditorutils/src/Shared/AvatarEditorUtils.lua +++ b/src/avatareditorutils/src/Shared/AvatarEditorUtils.lua @@ -456,7 +456,11 @@ function AvatarEditorUtils.promptAllowInventoryReadAccess() end) maid:GiveTask(AvatarEditorService.PromptAllowInventoryReadAccessCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() @@ -493,8 +497,12 @@ function AvatarEditorUtils.promptCreateOutfit(outfit: HumanoidDescription, rigTy maid:DoCleaning() end) - maid:GiveTask(AvatarEditorService.PromptCreateOutfitCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + maid:GiveTask(AvatarEditorService.PromptCreateOutfitCompleted:Connect(function(avatarPromptResult, failureType) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult, failureType) + end end)) local ok, err = pcall(function() @@ -526,7 +534,11 @@ function AvatarEditorUtils.promptDeleteOutfit(outfitId: number) end) maid:GiveTask(AvatarEditorService.PromptDeleteOutfitCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() @@ -558,7 +570,11 @@ function AvatarEditorUtils.promptRenameOutfit(outfitId: number) end) maid:GiveTask(AvatarEditorService.PromptRenameOutfitCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() @@ -590,7 +606,11 @@ function AvatarEditorUtils.promptSaveAvatar(humanoidDescription: HumanoidDescrip end) maid:GiveTask(AvatarEditorService.PromptSaveAvatarCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() @@ -626,7 +646,11 @@ function AvatarEditorUtils.promptSetFavorite(itemId: number, itemType: AvatarIte end) maid:GiveTask(AvatarEditorService.PromptSetFavoriteCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() @@ -662,7 +686,11 @@ function AvatarEditorUtils.promptUpdateOutfit(outfitId: number, updatedOutfit: H end) maid:GiveTask(AvatarEditorService.PromptUpdateOutfitCompleted:Connect(function(avatarPromptResult) - promise:Resolve(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + promise:Resolve(avatarPromptResult) + else + promise:Reject(avatarPromptResult) + end end)) local ok, err = pcall(function() From 0232f25f0bdcd57801e053abd3766a0dc32eeb9a Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:58:11 -0800 Subject: [PATCH 07/30] feat: Use EnumUtils here --- .../src/Shared/AccessoryTypeUtils.lua | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/accessorytypeutils/src/Shared/AccessoryTypeUtils.lua b/src/accessorytypeutils/src/Shared/AccessoryTypeUtils.lua index bddcbf67f0..aa86c2b8db 100644 --- a/src/accessorytypeutils/src/Shared/AccessoryTypeUtils.lua +++ b/src/accessorytypeutils/src/Shared/AccessoryTypeUtils.lua @@ -4,6 +4,8 @@ local require = require(script.Parent.loader).load(script) +local EnumUtils = require("EnumUtils") + local AvatarEditorService = game:GetService("AvatarEditorService") local AccessoryTypeUtils = {} @@ -40,34 +42,22 @@ end @param assetTypeId number @return AssetType | nl ]=] -function AccessoryTypeUtils.convertAssetTypeIdToAssetType(assetTypeId): AssetType +function AccessoryTypeUtils.convertAssetTypeIdToAssetType(assetTypeId: number): AssetType? assert(type(assetTypeId) == "number", "Bad assetTypeId") - for _, enumItem in pairs(Enum.AssetType:GetEnumItems()) do - if enumItem.Value == assetTypeId then - return enumItem - end - end - - return nil + return EnumUtils.toEnum(Enum.AssetType, assetTypeId) end --[=[ Converts an enum value (retrieved from MarketplaceService) into a proper enum if possible - @param assetTypeId number + @param avatarAssetTypeId number @return AvatarAssetType | nil ]=] -function AccessoryTypeUtils.convertAssetTypeIdToAvatarAssetType(assetTypeId): AvatarAssetType - assert(type(assetTypeId) == "number", "Bad assetTypeId") - - for _, enumItem in pairs(Enum.AvatarAssetType:GetEnumItems()) do - if enumItem.Value == assetTypeId then - return enumItem - end - end +function AccessoryTypeUtils.convertAssetTypeIdToAvatarAssetType(avatarAssetTypeId: number): AvatarAssetType? + assert(type(avatarAssetTypeId) == "number", "Bad avatarAssetTypeId") - return nil + return EnumUtils.toEnum(Enum.AvatarAssetType, avatarAssetTypeId) end return AccessoryTypeUtils \ No newline at end of file From 939f8b10398b9955129195b71e95f12bb29bd219 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 14:59:06 -0800 Subject: [PATCH 08/30] feat: Break up logic into inventory and catalog to allow centralized querying --- src/avatareditorutils/package.json | 6 +- .../src/Client/AvatarEditorInventory.lua | 63 ++++++++++ .../AvatarEditorInventoryServiceClient.lua | 110 ++++++++++++++++++ .../Cache/CatalogSearchServiceCache.lua | 34 +++++- 4 files changed, 207 insertions(+), 6 deletions(-) create mode 100644 src/avatareditorutils/src/Client/AvatarEditorInventory.lua create mode 100644 src/avatareditorutils/src/Client/AvatarEditorInventoryServiceClient.lua diff --git a/src/avatareditorutils/package.json b/src/avatareditorutils/package.json index 063f824ab8..94c1612c2f 100644 --- a/src/avatareditorutils/package.json +++ b/src/avatareditorutils/package.json @@ -25,14 +25,18 @@ "Quenty" ], "dependencies": { + "@quenty/aggregator": "file:../aggregator", "@quenty/brio": "file:../brio", "@quenty/enumutils": "file:../enumutils", "@quenty/loader": "file:../loader", "@quenty/maid": "file:../maid", "@quenty/memoize": "file:../memoize", + "@quenty/observablecollection": "file:../observablecollection", "@quenty/promise": "file:../promise", "@quenty/rx": "file:../rx", - "@quenty/symbol": "file:../symbol" + "@quenty/servicebag": "file:../servicebag", + "@quenty/symbol": "file:../symbol", + "@quenty/valueobject": "file:../valueobject" }, "publishConfig": { "access": "public" diff --git a/src/avatareditorutils/src/Client/AvatarEditorInventory.lua b/src/avatareditorutils/src/Client/AvatarEditorInventory.lua new file mode 100644 index 0000000000..aa995f92d1 --- /dev/null +++ b/src/avatareditorutils/src/Client/AvatarEditorInventory.lua @@ -0,0 +1,63 @@ +--[=[ + @class AvatarEditorInventory +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local ObservableMap = require("ObservableMap") +local Rx = require("Rx") +local PagesUtils = require("PagesUtils") +local Promise = require("Promise") + +local AvatarEditorInventory = setmetatable({}, BaseObject) +AvatarEditorInventory.ClassName = "AvatarEditorInventory" +AvatarEditorInventory.__index = AvatarEditorInventory + +function AvatarEditorInventory.new() + local self = setmetatable(BaseObject.new(), AvatarEditorInventory) + + self._assetIdToAsset = self._maid:Add(ObservableMap.new()) + + return self +end + +function AvatarEditorInventory:PromiseProcessPages(inventoryPages) + return Promise.spawn(function(resolve, reject) + local pageData = inventoryPages:GetCurrentPage() + while pageData do + for _, data in pairs(pageData) do + self._assetIdToAsset:Set(data.AssetId, data) + end + + pageData = nil + + if not inventoryPages.IsFinished then + local ok, err = PagesUtils.promiseAdvanceToNextPage(inventoryPages):Yield() + if not ok then + return reject(string.format("Failed to advance to next page due to %s", tostring(err))) + end + + pageData = err + end + end + + resolve() + end) +end + +function AvatarEditorInventory:IsAssetIdInInventory(assetId) + return self._assetIdToAsset:Get(assetId) ~= nil +end + +function AvatarEditorInventory:ObserveAssetIdInInventory(assetId) + assert(type(assetId) == "number", "Bad assetId") + + return self._assetIdToAsset:ObserveAtKey(assetId):Pipe({ + Rx.map(function(data) + return data ~= nil + end) + }) +end + +return AvatarEditorInventory \ No newline at end of file diff --git a/src/avatareditorutils/src/Client/AvatarEditorInventoryServiceClient.lua b/src/avatareditorutils/src/Client/AvatarEditorInventoryServiceClient.lua new file mode 100644 index 0000000000..168279ff38 --- /dev/null +++ b/src/avatareditorutils/src/Client/AvatarEditorInventoryServiceClient.lua @@ -0,0 +1,110 @@ +--[=[ + @class AvatarEditorInventoryServiceClient +]=] + +local require = require(script.Parent.loader).load(script) + +local AvatarEditorService = game:GetService("AvatarEditorService") + +local AvatarEditorInventory = require("AvatarEditorInventory") +local AvatarEditorUtils = require("AvatarEditorUtils") +local EnumUtils = require("EnumUtils") +local Maid = require("Maid") +local MemorizeUtils = require("MemorizeUtils") +local Promise = require("Promise") +local ValueObject = require("ValueObject") +local PagesProxy = require("PagesProxy") + +local AvatarEditorInventoryServiceClient = {} +AvatarEditorInventoryServiceClient.ServiceName = "AvatarEditorInventoryServiceClient" + +function AvatarEditorInventoryServiceClient:Init(serviceBag) + assert(not self._serviceBag, "Already initialized") + self._serviceBag = assert(serviceBag, "No serviceBag") + self._maid = Maid.new() + + self._isAccessAllowed = self._maid:Add(ValueObject.new(false, "boolean")) + self._assetTypeToInventoryPromises = {} + + self._maid:GiveTask(AvatarEditorService.PromptAllowInventoryReadAccessCompleted:Connect(function(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + self._isAccessAllowed.Value = true + end + end)) + + self._promiseInventoryPages = MemorizeUtils.memoize(function(avatarAssetTypes) + return AvatarEditorUtils.promiseInventoryPages(avatarAssetTypes) + :Then(function(catalogPages) + -- Allow for replay + return PagesProxy.new(catalogPages) + end) + end) +end + +function AvatarEditorInventoryServiceClient:PromiseInventoryPages(avatarAssetTypes) + return self:PromiseEnsureAccess() + :Then(function() + return self._promiseInventoryPages(avatarAssetTypes) + end) + :Then(function(pagesProxy) + return pagesProxy:Clone() + end) +end + +function AvatarEditorInventoryServiceClient:PromiseInventoryForAvatarAssetType(avatarAssetType) + assert(EnumUtils.isOfType(Enum.AvatarAssetType, avatarAssetType), "Bad avatarAssetType") + + if self._assetTypeToInventoryPromises[avatarAssetType] then + return self._assetTypeToInventoryPromises[avatarAssetType] + end + + local inventory = self._maid:Add(AvatarEditorInventory.new()) + + self._assetTypeToInventoryPromises[avatarAssetType] = AvatarEditorUtils.promiseInventoryPages({ + avatarAssetType + }) + :Then(function(inventoryPages) + return inventory:PromiseProcessPages(inventoryPages) + end) + :Then(function() + return inventory + end) + + return self._assetTypeToInventoryPromises[avatarAssetType] +end + +function AvatarEditorInventoryServiceClient:IsInventoryAccessAllowed() + return self._isAccessAllowed.Value +end + +function AvatarEditorInventoryServiceClient:ObserveIsInventoryAccessAllowed() + return self._isAccessAllowed:Observe() +end + +function AvatarEditorInventoryServiceClient:PromiseEnsureAccess() + if self._isAccessAllowed.Value then + return Promise.resolved() + end + + if self._currentAccessPromise and self._currentAccessPromise:IsPending() then + return self._currentAccessPromise + end + + local promise = self._maid:GivePromise(AvatarEditorUtils.promptAllowInventoryReadAccess()) + + promise:Then(function(avatarPromptResult) + if avatarPromptResult == Enum.AvatarPromptResult.Success then + self._isAccessAllowed.Value = true + end + end) + + self._currentAccessPromise = promise + + return self._currentAccessPromise +end + +function AvatarEditorInventoryServiceClient:Destroy() + self._maid:DoCleaning() +end + +return AvatarEditorInventoryServiceClient \ No newline at end of file diff --git a/src/avatareditorutils/src/Shared/Cache/CatalogSearchServiceCache.lua b/src/avatareditorutils/src/Shared/Cache/CatalogSearchServiceCache.lua index 77dcb33618..434298981d 100644 --- a/src/avatareditorutils/src/Shared/Cache/CatalogSearchServiceCache.lua +++ b/src/avatareditorutils/src/Shared/Cache/CatalogSearchServiceCache.lua @@ -6,6 +6,9 @@ local require = require(script.Parent.loader).load(script) local MemorizeUtils = require("MemorizeUtils") local AvatarEditorUtils = require("AvatarEditorUtils") +local Aggregator = require("Aggregator") +local Maid = require("Maid") +local PagesProxy = require("PagesProxy") local CatalogSearchServiceCache = {} CatalogSearchServiceCache.ServiceName = "CatalogSearchServiceCache" @@ -13,14 +16,22 @@ CatalogSearchServiceCache.ServiceName = "CatalogSearchServiceCache" function CatalogSearchServiceCache:Init(serviceBag) assert(not self._serviceBag, "Already initialized") self._serviceBag = assert(serviceBag, "No serviceBag") + self._maid = Maid.new() + -- TODO: If you scroll down long enough this leaks memory self._promiseSearchCatalog = MemorizeUtils.memoize(function(params) return AvatarEditorUtils.promiseSearchCatalog(params) + :Then(function(catalogPages) + return PagesProxy.new(catalogPages) + end) end) - self._promiseInventoryPages = MemorizeUtils.memoize(function(avatarAssetTypes) - return AvatarEditorUtils.promiseInventoryPages(avatarAssetTypes) - end) + self._assetAggregator = self._maid:Add(Aggregator.new("AvatarEditorUtils.promiseBatchItemDetails", function(itemIds) + return AvatarEditorUtils.promiseBatchItemDetails(itemIds, Enum.AvatarItemType.Asset) + end)) + self._bundleAggregator = self._maid:Add(Aggregator.new("AvatarEditorUtils.promiseBatchItemDetails", function(itemIds) + return AvatarEditorUtils.promiseBatchItemDetails(itemIds, Enum.AvatarItemType.Bundle) + end)) end function CatalogSearchServiceCache:PromiseAvatarRules() @@ -32,12 +43,25 @@ function CatalogSearchServiceCache:PromiseAvatarRules() return self._avatarRulesPromise end +function CatalogSearchServiceCache:PromiseItemDetails(assetId, avatarItemType) + if avatarItemType == Enum.AvatarItemType.Asset then + return self._assetAggregator:Promise(assetId) + elseif avatarItemType == Enum.AvatarItemType.Bundle then + return self._bundleAggregator:Promise(assetId) + else + error("Unknown avatarItemType") + end +end + function CatalogSearchServiceCache:PromiseSearchCatalog(params) return self._promiseSearchCatalog(params) + :Then(function(pagesProxy) + return pagesProxy:Clone() + end) end -function CatalogSearchServiceCache:PromiseInventoryPages(avatarAssetTypes) - return self._promiseInventoryPages(avatarAssetTypes) +function CatalogSearchServiceCache:Destroy() + self._maid:DoCleaning() end return CatalogSearchServiceCache \ No newline at end of file From 4dffc3ce700ff6f750cf5483d4a6fb95ebc1c80a Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 19:13:31 -0800 Subject: [PATCH 09/30] feat: Add funnels helper package --- src/funnels/README.md | 23 +++++++ src/funnels/default.project.json | 7 ++ src/funnels/package.json | 38 +++++++++++ .../src/Server/Steps/FunnelStepLogger.lua | 66 +++++++++++++++++++ .../src/Shared/Steps/FunnelStepTracker.lua | 49 ++++++++++++++ src/funnels/src/node_modules.project.json | 7 ++ src/funnels/test/default.project.json | 12 ++++ 7 files changed, 202 insertions(+) create mode 100644 src/funnels/README.md create mode 100644 src/funnels/default.project.json create mode 100644 src/funnels/package.json create mode 100644 src/funnels/src/Server/Steps/FunnelStepLogger.lua create mode 100644 src/funnels/src/Shared/Steps/FunnelStepTracker.lua create mode 100644 src/funnels/src/node_modules.project.json create mode 100644 src/funnels/test/default.project.json diff --git a/src/funnels/README.md b/src/funnels/README.md new file mode 100644 index 0000000000..26c7657ffc --- /dev/null +++ b/src/funnels/README.md @@ -0,0 +1,23 @@ +## Funnels + + + +Funnel utility class + + + +## Installation + +``` +npm install @quenty/funnels --save +``` diff --git a/src/funnels/default.project.json b/src/funnels/default.project.json new file mode 100644 index 0000000000..4e6dd695b6 --- /dev/null +++ b/src/funnels/default.project.json @@ -0,0 +1,7 @@ +{ + "name": "funnels", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": "src" + } +} diff --git a/src/funnels/package.json b/src/funnels/package.json new file mode 100644 index 0000000000..b37a7d8fcf --- /dev/null +++ b/src/funnels/package.json @@ -0,0 +1,38 @@ +{ + "name": "@quenty/funnels", + "version": "1.0.0", + "description": "Funnel utility class", + "keywords": [ + "Roblox", + "Nevermore", + "Lua", + "funnels" + ], + "bugs": { + "url": "https://github.com/Quenty/NevermoreEngine/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Quenty/NevermoreEngine.git", + "directory": "src/funnels/" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/quenty" + }, + "license": "MIT", + "contributors": [ + "Quenty" + ], + "dependencies": { + "@quenty/adorneedata": "file:../adorneedata", + "@quenty/baseobject": "file:../baseobject", + "@quenty/binder": "file:../binder", + "@quenty/loader": "file:../loader", + "@quenty/propertyvalue": "file:../propertyvalue", + "@quenty/servicebag": "file:../servicebag" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/src/funnels/src/Server/Steps/FunnelStepLogger.lua b/src/funnels/src/Server/Steps/FunnelStepLogger.lua new file mode 100644 index 0000000000..faf708bb93 --- /dev/null +++ b/src/funnels/src/Server/Steps/FunnelStepLogger.lua @@ -0,0 +1,66 @@ +--[=[ + Server-side funnel logger + + @class FunnelStepLogger +]=] + +local require = require(script.Parent.loader).load(script) + +local AnalyticsService = game:GetService("AnalyticsService") +local HttpService = game:GetService("HttpService") + +local BaseObject = require("BaseObject") +local FunnelStepTracker = require("FunnelStepTracker") + +local FunnelStepLogger = setmetatable({}, BaseObject) +FunnelStepLogger.ClassName = "FunnelStepLogger" +FunnelStepLogger.__index = FunnelStepLogger + +function FunnelStepLogger.new(player, funnelName) + local self = setmetatable(BaseObject.new(), FunnelStepLogger) + + self._player = assert(player, "No player") + self._stepTracker = self._maid:Add(FunnelStepTracker.new()) + self._funnelName = assert(funnelName, "Bad funnelName") + self._funnelSessionId = HttpService:GenerateGUID(false) + self._printDebugEnabled = false + + local steps = self._stepTracker:GetLoggedSteps() + if next(steps) then + -- Give us time to print if we need + self._maid:GiveTask(task.defer(function() + for stepNumber, stepName in pairs(steps) do + self:_sendStep(stepNumber, stepName) + end + end)) + end + + self._maid:GiveTask(self._stepTracker.StepLogged:Connect(function(stepNumber, stepName) + self:_sendStep(stepNumber, stepName) + end)) + + return self +end + +function FunnelStepLogger:SetPrintDebugEnabled(debugEnabled) + assert(type(debugEnabled) == "boolean", "Bad debugEnabled") + + self._printDebugEnabled = debugEnabled +end + +function FunnelStepLogger:LogStep(stepNumber, stepName) + assert(type(stepNumber) == "number", "Bad stepNumber") + assert(type(stepName) == "string", "Bad stepName") + + self._stepTracker:LogStep(stepNumber, stepName) +end + +function FunnelStepLogger:_sendStep(stepNumber, stepName) + AnalyticsService:LogFunnelStepEvent(self._player, self._funnelName, self._funnelSessionId, stepNumber, stepName) + + if self._printDebugEnabled then + print(string.format("%s - %d - %s", self._funnelName, stepNumber, stepName)) + end +end + +return FunnelStepLogger \ No newline at end of file diff --git a/src/funnels/src/Shared/Steps/FunnelStepTracker.lua b/src/funnels/src/Shared/Steps/FunnelStepTracker.lua new file mode 100644 index 0000000000..abdc7bc564 --- /dev/null +++ b/src/funnels/src/Shared/Steps/FunnelStepTracker.lua @@ -0,0 +1,49 @@ +--[=[ + @class FunnelStepTracker +]=] + +local require = require(script.Parent.loader).load(script) + +local Signal = require("Signal") +local BaseObject = require("BaseObject") + +local FunnelStepTracker = setmetatable({}, BaseObject) +FunnelStepTracker.ClassName = "FunnelStepTracker" +FunnelStepTracker.__index = FunnelStepTracker + +function FunnelStepTracker.new() + local self = setmetatable(BaseObject.new(), FunnelStepTracker) + + self._stepsLogged = {} + + self.StepLogged = self._maid:Add(Signal.new()) + + return self +end + +function FunnelStepTracker:LogStep(stepNumber, stepName) + assert(type(stepNumber) == "number", "Bad stepNumber") + assert(type(stepName) == "string", "Bad stepName") + + if self._stepsLogged[stepNumber] then + if self._stepsLogged[stepNumber] ~= stepName then + error(string.format("[FunnelStepTracker.LogStep] - Trying to log step with 2 separate names, %q and %q", self._stepsLogged[stepNumber], stepNumber)) + end + + return + end + + self._stepsLogged[stepNumber] = stepName + + self.StepLogged:Fire(stepNumber, stepName) +end + +function FunnelStepTracker:GetLoggedSteps() + return table.clone(self._stepsLogged) +end + +function FunnelStepTracker:ClearLoggedSteps() + table.clear(self._stepsLogged) +end + +return FunnelStepTracker \ No newline at end of file diff --git a/src/funnels/src/node_modules.project.json b/src/funnels/src/node_modules.project.json new file mode 100644 index 0000000000..46233dac4f --- /dev/null +++ b/src/funnels/src/node_modules.project.json @@ -0,0 +1,7 @@ +{ + "name": "node_modules", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": { "optional": "../node_modules" } + } +} \ No newline at end of file diff --git a/src/funnels/test/default.project.json b/src/funnels/test/default.project.json new file mode 100644 index 0000000000..9bd3fb3668 --- /dev/null +++ b/src/funnels/test/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "FunnelsTest", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "funnels": { + "$path": ".." + } + } + } +} \ No newline at end of file From 8577179e6a881ba23406938681787ef92535b9de Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 19:13:42 -0800 Subject: [PATCH 10/30] docs: Update docs --- readme.md | 10 ++++++---- src/gameconfig/README.md | 2 +- src/remoting/README.md | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index 53dcf608a6..8176d87027 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ Many of these packages represent not just useful code, but useful patterns, or w ### All packages -There are 265 packages in Nevermore. +There are 267 packages in Nevermore. | Package | Description | Install | docs | source | changelog | npm | | ------- | ----------- | ------- | ---- | ------ | --------- | --- | @@ -66,6 +66,7 @@ There are 265 packages in Nevermore. | [AdorneeData](https://quenty.github.io/NevermoreEngine/api/AdorneeData) | Bridges attributes and serialization | `npm i @quenty/adorneedata` | [docs](https://quenty.github.io/NevermoreEngine/api/AdorneeData) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneedata) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneedata/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/adorneedata) | | [AdorneeUtils](https://quenty.github.io/NevermoreEngine/api/AdorneeUtils) | AdorneeUtils - Generic adornee functions to make attaching to objects in Roblox easier. | `npm i @quenty/adorneeutils` | [docs](https://quenty.github.io/NevermoreEngine/api/AdorneeUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneeutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneeutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/adorneeutils) | | [AdorneeValue](https://quenty.github.io/NevermoreEngine/api/AdorneeValue) | Adorneevalue - Helper class to transform a an adornee into relative positions/information. | `npm i @quenty/adorneevalue` | [docs](https://quenty.github.io/NevermoreEngine/api/AdorneeValue) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneevalue) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/adorneevalue/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/adorneevalue) | +| [Aggregator](https://quenty.github.io/NevermoreEngine/api/AggregatorUtils) | Aggregates async promise requests | `npm i @quenty/aggregator` | [docs](https://quenty.github.io/NevermoreEngine/api/AggregatorUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/aggregator) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/aggregator/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/aggregator) | | [AnimationGroup](https://quenty.github.io/NevermoreEngine/api/AnimationGroup) | A group of weighted tracks that can be played back with weighted probability. The closest example to this is the idle animation that looks around at a 1:10 ratio when you're standing still in default Roblox animation script. | `npm i @quenty/animationgroup` | [docs](https://quenty.github.io/NevermoreEngine/api/AnimationGroup) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/animationgroup) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/animationgroup/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/animationgroup) | | [AnimationProvider](https://quenty.github.io/NevermoreEngine/api/AnimationProvider) | Provides animations for anything tagged with "AnimationContainer" and from a folder named "Animations" in ReplicatedStorage. | `npm i @quenty/animationprovider` | [docs](https://quenty.github.io/NevermoreEngine/api/AnimationProvider) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/animationprovider) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/animationprovider/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/animationprovider) | | [Animations](https://quenty.github.io/NevermoreEngine/api/AnimationsUtils) | Utility methods for playing back animations on Roblox | `npm i @quenty/animations` | [docs](https://quenty.github.io/NevermoreEngine/api/AnimationsUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/animations) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/animations/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/animations) | @@ -137,8 +138,9 @@ There are 265 packages in Nevermore. | [Flipbook](https://quenty.github.io/NevermoreEngine/api/Flipbook) | Handles playing back animated spritesheets | `npm i @quenty/flipbook` | [docs](https://quenty.github.io/NevermoreEngine/api/Flipbook) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/flipbook) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/flipbook/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/flipbook) | | [FriendUtils](https://quenty.github.io/NevermoreEngine/api/FriendUtils) | Utlity functions to help find friends of a user. Also contains utility to make testing in studio easier. | `npm i @quenty/friendutils` | [docs](https://quenty.github.io/NevermoreEngine/api/FriendUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/friendutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/friendutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/friendutils) | | [FunctionUtils](https://quenty.github.io/NevermoreEngine/api/FunctionUtils) | Utility functions involving functions | `npm i @quenty/functionutils` | [docs](https://quenty.github.io/NevermoreEngine/api/FunctionUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/functionutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/functionutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/functionutils) | +| [Funnels](https://quenty.github.io/NevermoreEngine/api/RecurringFunnel) | Funnel utility class | `npm i @quenty/funnels` | [docs](https://quenty.github.io/NevermoreEngine/api/RecurringFunnel) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/funnels) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/funnels/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/funnels) | | [Fzy](https://quenty.github.io/NevermoreEngine/api/FzyUtils) | Lua implementation of fzy string search algorithm | `npm i @quenty/fzy` | [docs](https://quenty.github.io/NevermoreEngine/api/FzyUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/fzy) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/fzy/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/fzy) | -| [GameConfig](https://quenty.github.io/NevermoreEngine/api/GameConfig) | Configuration service to specify Roblox badges, products, and other specific assets. | `npm i @quenty/gameconfig` | [docs](https://quenty.github.io/NevermoreEngine/api/GameConfig) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameconfig) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameconfig/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/gameconfig) | +| [GameConfig](https://quenty.github.io/NevermoreEngine/api/GameConfigService) | Configuration service to specify Roblox badges, products, and other specific assets. | `npm i @quenty/gameconfig` | [docs](https://quenty.github.io/NevermoreEngine/api/GameConfigService) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameconfig) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameconfig/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/gameconfig) | | [GameProductService](https://quenty.github.io/NevermoreEngine/api/GameProductService) | Generalized monetization system for handling products and purchases correctly. | `npm i @quenty/gameproductservice` | [docs](https://quenty.github.io/NevermoreEngine/api/GameProductService) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameproductservice) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameproductservice/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/gameproductservice) | | [GameScalingUtils](https://quenty.github.io/NevermoreEngine/api/GameScalingUtils) | Scale ratios for the UI on different devices | `npm i @quenty/gamescalingutils` | [docs](https://quenty.github.io/NevermoreEngine/api/GameScalingUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/gamescalingutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/gamescalingutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/gamescalingutils) | | [GameVersionUtils](https://quenty.github.io/NevermoreEngine/api/GameVersionUtils) | Utility functions to automatically detect the version a game is running at | `npm i @quenty/gameversionutils` | [docs](https://quenty.github.io/NevermoreEngine/api/GameVersionUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameversionutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/gameversionutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/gameversionutils) | @@ -244,12 +246,12 @@ There are 265 packages in Nevermore. | [Raycaster](https://quenty.github.io/NevermoreEngine/api/Raycaster) | Repeats raycasting attempts while ignoring items via a filter function | `npm i @quenty/raycaster` | [docs](https://quenty.github.io/NevermoreEngine/api/Raycaster) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/raycaster) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/raycaster/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/raycaster) | | [RbxAsset](https://quenty.github.io/NevermoreEngine/api/RbxAssetUtils) | Utility methods to help with Roblox asset id | `npm i @quenty/rbxasset` | [docs](https://quenty.github.io/NevermoreEngine/api/RbxAssetUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/rbxasset) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/rbxasset/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/rbxasset) | | [RbxThumb](https://quenty.github.io/NevermoreEngine/api/RbxThumbUtils) | Wraps the rbxthumb URL api surface. | `npm i @quenty/rbxthumb` | [docs](https://quenty.github.io/NevermoreEngine/api/RbxThumbUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/rbxthumb) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/rbxthumb/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/rbxthumb) | -| [ReceiptProcessing](https://quenty.github.io/NevermoreEngine/api/ReceiptProcessingUtils) | Centralize receipt processing within games since this is a constrained resource. | `npm i @quenty/receiptprocessing` | [docs](https://quenty.github.io/NevermoreEngine/api/ReceiptProcessingUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/receiptprocessing) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/receiptprocessing/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/receiptprocessing) | +| [ReceiptProcessing](https://quenty.github.io/NevermoreEngine/api/ReceiptProcessingService) | Centralize receipt processing within games since this is a constrained resource. | `npm i @quenty/receiptprocessing` | [docs](https://quenty.github.io/NevermoreEngine/api/ReceiptProcessingService) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/receiptprocessing) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/receiptprocessing/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/receiptprocessing) | | [RectUtils](https://quenty.github.io/NevermoreEngine/api/RectUtils) | Utility methods for Rect data object | `npm i @quenty/rectutils` | [docs](https://quenty.github.io/NevermoreEngine/api/RectUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/rectutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/rectutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/rectutils) | | [Region3int16Utils](https://quenty.github.io/NevermoreEngine/api/Region3int16Utils) | Module for working with Region3int16 | `npm i @quenty/region3int16utils` | [docs](https://quenty.github.io/NevermoreEngine/api/Region3int16Utils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/region3int16utils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/region3int16utils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/region3int16utils) | | [Region3Utils](https://quenty.github.io/NevermoreEngine/api/Region3Utils) | Utility methods for Region3 | `npm i @quenty/region3utils` | [docs](https://quenty.github.io/NevermoreEngine/api/Region3Utils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/region3utils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/region3utils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/region3utils) | | [RemoteFunctionUtils](https://quenty.github.io/NevermoreEngine/api/RemoteFunctionUtils) | Utility functions to wrap invoking a remote function with a promise | `npm i @quenty/remotefunctionutils` | [docs](https://quenty.github.io/NevermoreEngine/api/RemoteFunctionUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/remotefunctionutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/remotefunctionutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/remotefunctionutils) | -| [Remoting](https://quenty.github.io/NevermoreEngine/api/GetRemoteEvent) | Global remoting retrieval system for Roblox (RemoteFunctions/RemoteEvents) | `npm i @quenty/remoting` | [docs](https://quenty.github.io/NevermoreEngine/api/GetRemoteEvent) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/remoting) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/remoting/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/remoting) | +| [Remoting](https://quenty.github.io/NevermoreEngine/api/Remoting) | Global remoting retrieval system for Roblox (RemoteFunctions/RemoteEvents) | `npm i @quenty/remoting` | [docs](https://quenty.github.io/NevermoreEngine/api/Remoting) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/remoting) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/remoting/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/remoting) | | [ResetService](https://quenty.github.io/NevermoreEngine/api/ResetService) | Handles reset requests since Roblox's reset system doesn't handle ragdolls correctly | `npm i @quenty/resetservice` | [docs](https://quenty.github.io/NevermoreEngine/api/ResetService) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/resetservice) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/resetservice/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/resetservice) | | [RichText](https://quenty.github.io/NevermoreEngine/api/RichTextUtils) | Holds rich text utility methods | `npm i @quenty/richtext` | [docs](https://quenty.github.io/NevermoreEngine/api/RichTextUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/richtext) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/richtext/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/richtext) | | [RigBuilderUtils](https://quenty.github.io/NevermoreEngine/api/RigBuilderUtils) | Utility functions for debugging, builds a Roblox character rig | `npm i @quenty/rigbuilderutils` | [docs](https://quenty.github.io/NevermoreEngine/api/RigBuilderUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/rigbuilderutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/rigbuilderutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/rigbuilderutils) | diff --git a/src/gameconfig/README.md b/src/gameconfig/README.md index 4bd8846c6c..86262a4f20 100644 --- a/src/gameconfig/README.md +++ b/src/gameconfig/README.md @@ -13,7 +13,7 @@ Generalized game configuration system - + ## Installation ``` diff --git a/src/remoting/README.md b/src/remoting/README.md index 2d743ae395..f80ab0e681 100644 --- a/src/remoting/README.md +++ b/src/remoting/README.md @@ -13,7 +13,7 @@ Provides global remoting functionality for Roblox (GetRemoteEvent/GetRemoteFunction). This is extremely light-weight, but makes programming with global remotes a lot easier. - + ## Installation ``` From d159eeade8701bcbcd87e97126df3c3dfb155b8c Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 19:14:14 -0800 Subject: [PATCH 11/30] feat: PlayerProductManagerClient queries inventory when inventory is available --- src/accessorytypeutils/package.json | 1 + src/avatareditorutils/package.json | 1 + src/gameproductservice/package.json | 1 + .../src/Client/GameProductServiceClient.lua | 3 +- .../Manager/PlayerProductManagerClient.lua | 49 ++++++++++++++++++- .../Trackers/PlayerProductManagerBase.lua | 2 + 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/accessorytypeutils/package.json b/src/accessorytypeutils/package.json index 986ad79905..19fe4d51e8 100644 --- a/src/accessorytypeutils/package.json +++ b/src/accessorytypeutils/package.json @@ -25,6 +25,7 @@ "Quenty" ], "dependencies": { + "@quenty/enumutils": "file:../enumutils", "@quenty/loader": "file:../loader" }, "publishConfig": { diff --git a/src/avatareditorutils/package.json b/src/avatareditorutils/package.json index 94c1612c2f..51716d05bc 100644 --- a/src/avatareditorutils/package.json +++ b/src/avatareditorutils/package.json @@ -32,6 +32,7 @@ "@quenty/maid": "file:../maid", "@quenty/memoize": "file:../memoize", "@quenty/observablecollection": "file:../observablecollection", + "@quenty/pagesutils": "file:../pagesutils", "@quenty/promise": "file:../promise", "@quenty/rx": "file:../rx", "@quenty/servicebag": "file:../servicebag", diff --git a/src/gameproductservice/package.json b/src/gameproductservice/package.json index 9ee51c07d8..841a2b9ac7 100644 --- a/src/gameproductservice/package.json +++ b/src/gameproductservice/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@quenty/attributeutils": "file:../attributeutils", + "@quenty/avatareditorutils": "file:../avatareditorutils", "@quenty/baseobject": "file:../baseobject", "@quenty/binder": "file:../binder", "@quenty/brio": "file:../brio", diff --git a/src/gameproductservice/src/Client/GameProductServiceClient.lua b/src/gameproductservice/src/Client/GameProductServiceClient.lua index 0a11bb23bc..9b02621770 100644 --- a/src/gameproductservice/src/Client/GameProductServiceClient.lua +++ b/src/gameproductservice/src/Client/GameProductServiceClient.lua @@ -34,8 +34,9 @@ function GameProductServiceClient:Init(serviceBag) self._maid = Maid.new() -- External - self._serviceBag:GetService(require("GameConfigServiceClient")) + self._serviceBag:GetService(require("AvatarEditorInventoryServiceClient")) self._serviceBag:GetService(require("CmdrServiceClient")) + self._serviceBag:GetService(require("GameConfigServiceClient")) -- Internal self._gameProductDataService = self._serviceBag:GetService(require("GameProductDataService")) diff --git a/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua b/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua index 013cdc60a3..fdf3d0f2c4 100644 --- a/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua +++ b/src/gameproductservice/src/Client/Manager/PlayerProductManagerClient.lua @@ -8,11 +8,16 @@ local require = require(script.Parent.loader).load(script) local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") +local AvatarEditorInventoryServiceClient = require("AvatarEditorInventoryServiceClient") local Binder = require("Binder") +local CatalogSearchServiceCache = require("CatalogSearchServiceCache") +local EnumUtils = require("EnumUtils") local GameConfigAssetTypes = require("GameConfigAssetTypes") +local MarketplaceUtils = require("MarketplaceUtils") local PlayerProductManagerBase = require("PlayerProductManagerBase") local PlayerProductManagerInterface = require("PlayerProductManagerInterface") local Remoting = require("Remoting") +local Promise = require("Promise") local PlayerProductManagerClient = setmetatable({}, PlayerProductManagerBase) PlayerProductManagerClient.ClassName = "PlayerProductManagerClient" @@ -21,6 +26,9 @@ PlayerProductManagerClient.__index = PlayerProductManagerClient function PlayerProductManagerClient.new(obj, serviceBag) local self = setmetatable(PlayerProductManagerBase.new(obj, serviceBag), PlayerProductManagerClient) + self._avatarEditorInventoryServiceClient = self._serviceBag:GetService(AvatarEditorInventoryServiceClient) + self._catalogSearchServiceCache = self._serviceBag:GetService(CatalogSearchServiceCache) + if self._obj == Players.LocalPlayer then self._remoting = self._maid:Add(Remoting.new(self._obj, "PlayerProductManager", Remoting.Realms.CLIENT)) @@ -40,7 +48,6 @@ function PlayerProductManagerClient.new(obj, serviceBag) return self end - --[=[ Gets the current player @return Player @@ -51,6 +58,11 @@ end function PlayerProductManagerClient:_setupAssetTracker() local tracker = self:GetAssetTrackerOrError(GameConfigAssetTypes.ASSET) + local assetOwnership = assert(tracker:GetOwnershipTracker(), "Missing ownershipTracker on client") + + assetOwnership:SetQueryOwnershipCallback(function(assetId) + return self:_promiseBulkOwnsAssetQuery(assetId) + end) self._maid:GiveTask(MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased) if player == self._obj then @@ -59,7 +71,6 @@ function PlayerProductManagerClient:_setupAssetTracker() self._remoting.AssetPromptPurchaseFinished:FireServer(assetId, isPurchased) end end)) - end function PlayerProductManagerClient:_setupMembershipTracker() @@ -180,6 +191,12 @@ end function PlayerProductManagerClient:_setupBundleTracker() local tracker = self:GetAssetTrackerOrError(GameConfigAssetTypes.BUNDLE) + local bundleOwnership = assert(tracker:GetOwnershipTracker(), "Missing ownershipTracker on client") + + bundleOwnership:SetQueryOwnershipCallback(function(assetId) + return self:_promiseBulkOwnsBundleQuery(assetId, Enum.AvatarItemType.Bundle) + end) + self._maid:GiveTask(MarketplaceService.PromptBundlePurchaseFinished:Connect(function(player, bundleId, isPurchased) if player == self._obj then tracker:HandlePromptClosedEvent(bundleId) @@ -190,4 +207,32 @@ function PlayerProductManagerClient:_setupBundleTracker() end)) end +function PlayerProductManagerClient:_promiseBulkOwnsAssetQuery(assetId) + if self._avatarEditorInventoryServiceClient:IsInventoryAccessAllowed() then + -- When scrolling through a ton of entries in the avatar editor we want to query + -- this is typically faster. We really hope we aren't the Roblox account. + return self._catalogSearchServiceCache:PromiseItemDetails(assetId, Enum.AvatarItemType.Asset) + :Then(function(itemDetails) + -- https://devforum.roblox.com/t/avatareditorservicegetitemdetails-returns-ownership-where-as-avatareditorservicegetbatchitemdetails-does-not/3257431 + + local assetType = EnumUtils.toEnum(Enum.AvatarAssetType, itemDetails.AssetType) + if not assetType then + -- TODO: Fallback to standard query? + return Promise.rejected("Failed to get assetType") + end + + return self._avatarEditorInventoryServiceClient:PromiseInventoryForAvatarAssetType(assetType) + :Then(function(inventory) + return inventory:IsAssetIdInInventory(assetId) + end) + end) + end + + return MarketplaceUtils.promisePlayerOwnsAsset(self._player, assetId) +end + +function PlayerProductManagerClient:_promiseBulkOwnsBundleQuery(bundleId) + return MarketplaceUtils.promisePlayerOwnsBundle(self._player, bundleId) +end + return Binder.new("PlayerProductManager", PlayerProductManagerClient) \ No newline at end of file diff --git a/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua b/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua index d291b66d87..6e55125380 100644 --- a/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua +++ b/src/gameproductservice/src/Shared/Trackers/PlayerProductManagerBase.lua @@ -93,10 +93,12 @@ function PlayerProductManagerBase.new(player, serviceBag) -- Configure assets too assetOwnership:SetQueryOwnershipCallback(function(assetId) + -- NOTE: client overrides these to bulk operations return MarketplaceUtils.promisePlayerOwnsAsset(self._player, assetId) end) bundleOwnership:SetQueryOwnershipCallback(function(bundleId) + -- NOTE: client overrides these to bulk operations return MarketplaceUtils.promisePlayerOwnsBundle(self._player, bundleId) end) From f1c068dd206b6a624e97cab86bb18c39e800a79a Mon Sep 17 00:00:00 2001 From: James Onnen Date: Wed, 13 Nov 2024 19:15:03 -0800 Subject: [PATCH 12/30] fix: ObservableSortedList fires removed indexes (note: Doesn't handle negatives right now) --- .../SortedList/ObservableSortedList.lua | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/observablecollection/src/Shared/SortedList/ObservableSortedList.lua b/src/observablecollection/src/Shared/SortedList/ObservableSortedList.lua index 43361f8438..a700d5dcd8 100644 --- a/src/observablecollection/src/Shared/SortedList/ObservableSortedList.lua +++ b/src/observablecollection/src/Shared/SortedList/ObservableSortedList.lua @@ -298,17 +298,14 @@ end function ObservableSortedList:ObserveAtIndex(indexToObserve) assert(type(indexToObserve) == "number", "Bad indexToObserve") - return self._indexObservers:Observe(indexToObserve) - :Pipe({ - Rx.start(function() - local node = self:_findNodeAtIndex(indexToObserve) - if node then - return node.data, node - else - return nil - end - end); - }) + return self._indexObservers:Observe(indexToObserve, function(sub) + local node = self:_findNodeAtIndex(indexToObserve) + if node then + sub:Fire(node.data, node) + else + sub:Fire(nil, nil) + end + end) end --[=[ @@ -321,16 +318,12 @@ end function ObservableSortedList:ObserveIndexByKey(node) assert(SortedNode.isSortedNode(node), "Bad node") - return self._nodeIndexObservables:Observe(node):Pipe({ - Rx.startFrom(function() - local currentIndex = self:_findNodeIndex(node) - if currentIndex then - return { currentIndex } - else - return {} - end - end); - }) + return self._nodeIndexObservables:Observe(node, function(sub) + local currentIndex = self:_findNodeIndex(node) + if currentIndex then + sub:Fire(currentIndex) + end + end) end --[=[ @@ -494,12 +487,11 @@ function ObservableSortedList:_fireEvents() local nodesRemoved = self._nodesRemoved self._nodesRemoved = {} + local lastCount = self._countValue.Value + local newCount = if self._root then self._root.descendantCount else 0 + -- Fire count changed first - if self._root then - self._countValue.Value = self._root.descendantCount - else - self._countValue.Value = 0 - end + self._countValue.Value = newCount if not self.Destroy then return end @@ -532,6 +524,12 @@ function ObservableSortedList:_fireEvents() self._indexObservers:Fire(index, node.data, node) self._indexObservers:Fire(negative, node.data, node) end + + for index=newCount+1, lastCount do + self._indexObservers:Fire(index, nil, nil) + end + + -- TODO: Fire negatives beyond range end if not self.Destroy then return end From aecd3a59fd9b747b5214d6686bcae0fe99a078ee Mon Sep 17 00:00:00 2001 From: James Onnen Date: Mon, 18 Nov 2024 13:10:03 -0800 Subject: [PATCH 13/30] docs: Add articles to binder README.md --- src/binder/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/binder/README.md b/src/binder/README.md index c73ccbaa2d..4c072e87b8 100644 --- a/src/binder/README.md +++ b/src/binder/README.md @@ -18,4 +18,10 @@ Binders bind a class to Roblox Instance ## Installation ``` npm install @quenty/binder --save -``` \ No newline at end of file +``` + +## Articles and more + +* [5 powerful programming patterns](https://www.youtube.com/watch?v=MOjiKS6F59s&t=1896s) +* [OzzyPig's Binder Pattern Post](https://ozzypig.com/2021/11/15/binder-pattern) + From 5147d5083c7c0b945dbf8f41e74805905d5e0271 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Mon, 18 Nov 2024 14:22:46 -0800 Subject: [PATCH 14/30] feat: Remove ownership tracking mess with well-known assets and attributes and just use raw state --- .../Server/Manager/PlayerProductManager.lua | 2 - .../src/Shared/GameProductDataService.lua | 23 +- .../Ownership/PlayerAssetOwnershipTracker.lua | 240 +++++------------- .../Ownership/PlayerAssetOwnershipUtils.lua | 108 -------- .../WellKnownAssetOwnershipHandler.lua | 82 ------ 5 files changed, 77 insertions(+), 378 deletions(-) delete mode 100644 src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipUtils.lua delete mode 100644 src/gameproductservice/src/Shared/Ownership/WellKnownAssetOwnershipHandler.lua diff --git a/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua b/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua index ab599faa69..ba7fe776d0 100644 --- a/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua +++ b/src/gameproductservice/src/Server/Manager/PlayerProductManager.lua @@ -103,7 +103,6 @@ end function PlayerProductManager:_setupPassTracker() local tracker = self:GetAssetTrackerOrError(GameConfigAssetTypes.PASS) - tracker:GetOwnershipTracker():SetWriteAttributesEnabled(true) self._maid:GiveTask(self._remoting.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, isPurchased) assert(player == self._obj, "Bad player") @@ -132,7 +131,6 @@ function PlayerProductManager:_setupSubscriptionTracker() self._remoting.UserSubscriptionStatusChanged:DeclareEvent() local tracker = self:GetAssetTrackerOrError(GameConfigAssetTypes.SUBSCRIPTION) - tracker:GetOwnershipTracker():SetWriteAttributesEnabled(true) self._maid:GiveTask(self._remoting.PromptSubscriptionPurchaseFinished:Connect(function(player, subscriptionId) assert(player == self._obj, "Bad player") diff --git a/src/gameproductservice/src/Shared/GameProductDataService.lua b/src/gameproductservice/src/Shared/GameProductDataService.lua index 85fa795c17..389f26567d 100644 --- a/src/gameproductservice/src/Shared/GameProductDataService.lua +++ b/src/gameproductservice/src/Shared/GameProductDataService.lua @@ -188,11 +188,15 @@ function GameProductDataService:ObservePlayerOwnership(player, assetType, idOrKe -- TODO: Maybe make this more light weight and cache return self:_observePlayerProductManagerBrio(player):Pipe({ - RxBrioUtils.switchMapBrio(function(playerProductManager) - local ownershipTracker = playerProductManager:GetOwnershipTrackerOrError(assetType) - return ownershipTracker:ObserveOwnsAsset(idOrKey) + RxBrioUtils.flattenToValueAndNil; + Rx.switchMap(function(playerProductManager) + if playerProductManager then + local ownershipTracker = playerProductManager:GetOwnershipTrackerOrError(assetType) + return ownershipTracker:ObserveOwnsAsset(idOrKey) + else + return Rx.EMPTY + end end); - RxStateStackUtils.topOfStack(false); }) end @@ -210,11 +214,16 @@ function GameProductDataService:ObservePlayerAssetPurchased(player, assetType, i assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey") return self:_observePlayerProductManagerBrio(player):Pipe({ + RxBrioUtils.flattenToValueAndNil; RxBrioUtils.switchMapBrio(function(playerProductManager) - local ownershipTracker = playerProductManager:GetOwnershipTrackerOrError(assetType) - return ownershipTracker:ObserveAssetPurchased(idOrKey) + if playerProductManager then + local ownershipTracker = playerProductManager:GetOwnershipTrackerOrError(assetType) + return ownershipTracker:ObserveAssetPurchased(idOrKey) + else + return Rx.EMPTY + end end); - Rx.map(function(_brio) + Rx.map(function() return true end) }) diff --git a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua index fe2b00714d..0d8458d643 100644 --- a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua +++ b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua @@ -10,17 +10,12 @@ local require = require(script.Parent.loader).load(script) local BaseObject = require("BaseObject") local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils") -local Maid = require("Maid") -local ObservableMapSet = require("ObservableMapSet") -local PlayerAssetOwnershipUtils = require("PlayerAssetOwnershipUtils") +local ObservableSet = require("ObservableSet") local Promise = require("Promise") local Rx = require("Rx") -local RxAttributeUtils = require("RxAttributeUtils") -local RxBrioUtils = require("RxBrioUtils") -local RxStateStackUtils = require("RxStateStackUtils") local ValueObject = require("ValueObject") -local WellKnownAssetOwnershipHandler = require("WellKnownAssetOwnershipHandler") -local ObservableSet = require("ObservableSet") +local Observable = require("Observable") +local Maid = require("Maid") local PlayerAssetOwnershipTracker = setmetatable({}, BaseObject) PlayerAssetOwnershipTracker.ClassName = "PlayerAssetOwnershipTracker" @@ -37,11 +32,7 @@ function PlayerAssetOwnershipTracker.new(player, configPicker, assetType, market self._marketTracker = assert(marketTracker, "No marketTracker") self._ownershipCallback = self._maid:Add(ValueObject.new(nil)) - self._attributesEnabled = self._maid:Add(ValueObject.new(false, "boolean")) - self._ownedAssetIdSet = self._maid:Add(ObservableSet.new()) - self._assetIdToWellKnownOwnershipTracker = self._maid:Add(ObservableMapSet.new()) - self._assetKeyToWellKnownOwnershipTracker = self._maid:Add(ObservableMapSet.new()) self._assetOwnershipPromiseCache = {} @@ -49,14 +40,6 @@ function PlayerAssetOwnershipTracker.new(player, configPicker, assetType, market self:SetOwnership(idOrKey, true) end)) - self._maid:GiveTask(self._attributesEnabled:Observe():Subscribe(function(isEnabled) - if isEnabled then - self._maid._wellKnown = self:_cacheWellKnownAssets() - else - self._maid._wellKnown = nil - end - end)) - return self end @@ -75,70 +58,41 @@ function PlayerAssetOwnershipTracker:SetQueryOwnershipCallback(promiseOwnsAsset) self._ownershipCallback.Value = promiseOwnsAsset end -function PlayerAssetOwnershipTracker:_promiseQueryIdOrKeyOwnershipCached(idOrKey) +function PlayerAssetOwnershipTracker:_promiseQueryAssetId(assetId) + assert(type(assetId) == "number", "Bad assetId") + local promiseOwnershipCallback = self._ownershipCallback.Value if not promiseOwnershipCallback then - return nil + return Promise.rejected(string.format("[PlayerAssetOwnershipTracker] - Cannot query ownership for assetType %q - No ownership callback set", tostring(self._assetType))) end - local id = self._configPicker:ToAssetId(self._assetType, idOrKey) - if not id then - warn(string.format("[PlayerAssetOwnershipTracker._promiseQueryIdOrKeyOwnershipCached] - Nothing with key %q", tostring(idOrKey))) - return Promise.resolved(false) - end - - if self._assetOwnershipPromiseCache[id] ~= nil then - if Promise.isPromise(self._assetOwnershipPromiseCache[id]) then - return self._assetOwnershipPromiseCache[id] + if self._assetOwnershipPromiseCache[assetId] ~= nil then + if Promise.isPromise(self._assetOwnershipPromiseCache[assetId]) then + return self._assetOwnershipPromiseCache[assetId] else - return Promise.resolved(self._assetOwnershipPromiseCache[id]) + return Promise.resolved(self._assetOwnershipPromiseCache[assetId]) end end - local promise = promiseOwnershipCallback(id) + local promise = promiseOwnershipCallback(assetId) assert(Promise.isPromise(promise), "Expected promise from callack") promise = self._maid:GivePromise(promise) promise:Then(function(ownsItem) - self._assetOwnershipPromiseCache[id] = ownsItem + self._assetOwnershipPromiseCache[assetId] = ownsItem -- Cache this stuff if ownsItem then - self:SetOwnership(idOrKey, true) + self:SetOwnership(assetId, true) end end) - self._assetOwnershipPromiseCache[id] = promise + self._assetOwnershipPromiseCache[assetId] = promise return promise end -function PlayerAssetOwnershipTracker:_observeQueryOwnershipIdOrKeyCachedBrio(idOrKey) - return self._ownershipCallback:Observe():Pipe({ - RxBrioUtils.switchToBrio(); - RxBrioUtils.switchMapBrio(function() - local promise = self:_promiseQueryIdOrKeyOwnershipCached(idOrKey) - if promise then - -- Do cached version - return Rx.fromPromise(promise) - else - return Rx.of(false) - end - end); - }); -end - ---[=[ - Sets whether attributes should be written for this asset type. - @param attributesEnabled boolean -]=] -function PlayerAssetOwnershipTracker:SetWriteAttributesEnabled(attributesEnabled) - assert(type(attributesEnabled) == "boolean", "Bad attributesEnabled") - - self._attributesEnabled.Value = attributesEnabled -end - --[=[ Sets the players ownership of a the asset @@ -149,29 +103,17 @@ function PlayerAssetOwnershipTracker:SetOwnership(idOrKey, ownsAsset) assert(type(idOrKey) == "number" or type(idOrKey) == "string", "idOrKey") assert(type(ownsAsset) == "boolean", "Bad ownsAsset") - local id = self._configPicker:ToAssetId(self._assetType, idOrKey) - if not id then + local assetId = self._configPicker:ToAssetId(self._assetType, idOrKey) + if not assetId then warn(string.format("[PlayerAssetOwnershipTracker.SetOwnership] - Nothing with key %q", tostring(idOrKey))) return end - if self._ownedAssetIdSet:Contains(id) then + if self._ownedAssetIdSet:Contains(assetId) then return end - self._ownedAssetIdSet:Add(id) - - if self._attributesEnabled.Value then - local attributeNames = PlayerAssetOwnershipUtils.getAttributeNames(self._configPicker, self._assetType, idOrKey) - for _, attributeName in pairs(attributeNames) do - self._player:SetAttribute(attributeName, ownsAsset) - end - end - - -- Update trackers - for _, wellOwnedAsset in pairs(self:_getWellKnownAssets(idOrKey)) do - wellOwnedAsset:SetIsOwned(ownsAsset) - end + self._ownedAssetIdSet:Add(assetId) end --[=[ @@ -183,19 +125,9 @@ end function PlayerAssetOwnershipTracker:PromiseOwnsAsset(idOrKey) assert(type(idOrKey) == "number" or type(idOrKey) == "string", "idOrKey") - -- Check attributes - if self._attributesEnabled.Value then - local attributeNames = PlayerAssetOwnershipUtils.getAttributeNames(self._configPicker, self._assetType, idOrKey) - for _, attributeName in pairs(attributeNames) do - if self._player:GetAttribute(attributeName) then - return Promise.resolved(true) - end - end - end - - local id = self._configPicker:ToAssetId(self._assetType, idOrKey) - if id then - if self._ownedAssetIdSet:Contains(id) then + local assetId = self._configPicker:ToAssetId(self._assetType, idOrKey) + if assetId then + if self._ownedAssetIdSet:Contains(assetId) then return Promise.resolved(true) end else @@ -203,7 +135,7 @@ function PlayerAssetOwnershipTracker:PromiseOwnsAsset(idOrKey) end -- Check actual callback querying Roblox - local promise = self:_promiseQueryIdOrKeyOwnershipCached(idOrKey) + local promise = self:_promiseQueryAssetId(assetId) if promise then return promise else @@ -220,100 +152,50 @@ end function PlayerAssetOwnershipTracker:ObserveOwnsAsset(idOrKey) assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey") - return Rx.merge({ - -- Observe attributes - PlayerAssetOwnershipUtils.observeAttributeNamesBrio(self._configPicker, self._assetType, idOrKey) - :Pipe({ - RxBrioUtils.flatMapBrio(function(attributeName) - return RxAttributeUtils.observeAttribute(self._player, attributeName) - end); - }); - - -- Observe well known assets - self:_observeWellKnownAsset(idOrKey):Pipe({ - RxBrioUtils.flatMapBrio(function(knownAsset) - return knownAsset:ObserveIsOwned() - end); - }); - - -- Observe our internal cache - self._configPicker:ObserveToAssetIdBrio(self._assetType, idOrKey):Pipe({ - RxBrioUtils.flatMapBrio(function(id) - return self._ownedAssetIdSet:ObserveContains(id) - end); - }); - - -- Observe promise (in case we aren't a well known asset) - self:_observeQueryOwnershipIdOrKeyCachedBrio(idOrKey); - }) - :Pipe({ - RxBrioUtils.where(function(value) - return value and true or false - end); - RxStateStackUtils.topOfStack(false); - Rx.throttleDefer(); - }) -end + -- TODO: Get rid of several concepts here, including well known assets, attributes, and more -function PlayerAssetOwnershipTracker:_getWellKnownAssets(idOrKey) - if type(idOrKey) == "number" then - return self._assetIdToWellKnownOwnershipTracker:GetListForKey(idOrKey) - elseif type(idOrKey) == "string" then - return self._assetKeyToWellKnownOwnershipTracker:GetListForKey(idOrKey) - else - error("[PlayerAssetOwnershipTracker._getWellKnownAssets] - Bad idOrKey") - end -end + if type(idOrKey) == "string" then + return Observable.new(function(sub) + local topMaid = Maid.new() -function PlayerAssetOwnershipTracker:_observeWellKnownAsset(idOrKey) - if type(idOrKey) == "number" then - return self._assetIdToWellKnownOwnershipTracker:ObserveItemsForKeyBrio(idOrKey) - elseif type(idOrKey) == "string" then - return self._assetKeyToWellKnownOwnershipTracker:ObserveItemsForKeyBrio(idOrKey) - else - error("[PlayerAssetOwnershipTracker._observeWellKnownAsset] - Bad idOrKey") - end -end + topMaid:GiveTask(self._configPicker:ObserveToAssetIdBrio(self._assetType, idOrKey):Subscribe(function(brio) + if brio:IsDead() then + return + end -function PlayerAssetOwnershipTracker:_cacheWellKnownAssets() - local maid = Maid.new() + -- Only fire once we find the asset + local maid, assetId = brio:ToMaidAndValue() + maid:GivePromise(self:_promiseQueryAssetId(assetId)) + :Then(function() + maid:GiveTask(self._ownedAssetIdSet:ObserveContains(assetId):Subscribe(function(value) + sub:Fire(value) + end)) + end) + end)) - maid:GiveTask(self._configPicker:ObserveActiveAssetOfTypeBrio(self._assetType):Subscribe(function(brio) - if brio:IsDead() then - return - end + return topMaid + end) - local gameConfigMaid, gameConfigAsset = brio:ToMaidAndValue() - local wellKnownHandler = gameConfigMaid:Add(WellKnownAssetOwnershipHandler.new(self._player, gameConfigAsset)) - - gameConfigMaid:GiveTask(Rx.combineLatest({ - ownershipCallback = self._ownershipCallback:Observe(); - assetId = gameConfigAsset:ObserveAssetId(); - }):Pipe({ - RxBrioUtils.switchToBrio(); - RxBrioUtils.switchMapBrio(function(state) - if state.assetId then - local promise = self:_promiseQueryIdOrKeyOwnershipCached(state.assetId) - if promise then - -- Do cached version - return Rx.fromPromise(promise) - else - return Rx.of(false) - end - else - return Rx.of(false) - end - end); - RxStateStackUtils.topOfStack(false); - }):Subscribe(function(owned) - wellKnownHandler:SetIsOwned(owned) - end)) - - gameConfigMaid:GiveTask(self._assetIdToWellKnownOwnershipTracker:Push(gameConfigAsset:ObserveAssetId(), wellKnownHandler)) - gameConfigMaid:GiveTask(self._assetKeyToWellKnownOwnershipTracker:Push(gameConfigAsset:ObserveAssetKey(), wellKnownHandler)) - end)) - return maid + elseif type(idOrKey) == "number" then + return Observable.new(function(sub) + local maid = Maid.new() + + maid:GivePromise(self:_promiseQueryAssetId(idOrKey)) + :Then(function() + -- Only fire once we find ownership status + + maid:GiveTask(self._ownedAssetIdSet:ObserveContains(idOrKey):Subscribe(function(value) + sub:Fire(value) + end)) + end) + + return maid + end) + else + error("Bad idOrKey") + end + end diff --git a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipUtils.lua b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipUtils.lua deleted file mode 100644 index a95dd18212..0000000000 --- a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipUtils.lua +++ /dev/null @@ -1,108 +0,0 @@ ---[=[ - @class PlayerAssetOwnershipUtils -]=] - -local require = require(script.Parent.loader).load(script) - -local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils") -local String = require("String") -local Rx = require("Rx") -local RxBrioUtils = require("RxBrioUtils") - -local PlayerAssetOwnershipUtils = {} - -function PlayerAssetOwnershipUtils.toKeyOwnedAttribute(assetType, assetKey) - assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType") - assert(type(assetKey) == "string", "bad assetKey") - - return string.format("Owns_%s_%s", String.toCamelCase(assetType), assetKey) -end - -function PlayerAssetOwnershipUtils.toIdOwnedAttribute(assetType, id) - assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType") - - return string.format("Owns_%s_Id_%d", String.toCamelCase(assetType), id) -end - -function PlayerAssetOwnershipUtils.getAttributeNames(configPicker, assetType, idOrKey) - assert(configPicker, "Bad configPicker") - assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType") - assert(type(idOrKey) == "number" or type(idOrKey) == "string", "idOrKey") - - local assetKeys = {} - local assetIds = {} - - if type(idOrKey) == "number" then - assetIds[idOrKey] = true - - for _, gameConfig in pairs(configPicker:GetActiveConfigs()) do - for _, gameConfigAsset in pairs(gameConfig:GetAssetsOfTypeAndId(assetType, idOrKey)) do - assetKeys[gameConfigAsset:GetAssetKey()] = true - end - end - elseif type(idOrKey) == "string" then - assetKeys[idOrKey] = true - - for _, gameConfig in pairs(configPicker:GetActiveConfigs()) do - for _, gameConfigAsset in pairs(gameConfig:GetAssetsOfTypeAndKey(assetType, idOrKey)) do - assetIds[gameConfigAsset:GetAssetId()] = true - end - end - else - error("Bad idOrKey") - end - - local attributeNames = {} - for assetId, _ in pairs(assetIds) do - table.insert(attributeNames, PlayerAssetOwnershipUtils.toIdOwnedAttribute(assetType, assetId)) - end - - for assetKey, _ in pairs(assetKeys) do - table.insert(attributeNames, PlayerAssetOwnershipUtils.toKeyOwnedAttribute(assetType, assetKey)) - end - - return attributeNames -end - -function PlayerAssetOwnershipUtils.observeAttributeNamesBrio(configPicker, assetType, idOrKey) - assert(configPicker, "Bad configPicker") - assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType") - assert(type(idOrKey) == "number" or type(idOrKey) == "string", "idOrKey") - - return configPicker:ObserveActiveConfigsBrio():Pipe({ - RxBrioUtils.flatMapBrio(function(activeConfig) - if type(idOrKey) == "number" then - return activeConfig:ObserveAssetByIdBrio(idOrKey) - elseif type(idOrKey) == "string" then - return activeConfig:ObserveAssetByKeyBrio(idOrKey) - else - return Rx.of(nil) - end - end); - RxBrioUtils.flatMapBrio(function(gameConfigAsset) - return PlayerAssetOwnershipUtils.observeAttributeNamesForGameConfigAssetBrio(assetType, gameConfigAsset) - end); - }) -end - -function PlayerAssetOwnershipUtils.observeAttributeNamesForGameConfigAssetBrio(assetType, gameConfigAsset) - assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType") - assert(gameConfigAsset, "No gameConfigAsset") - - return Rx.merge({ - gameConfigAsset:ObserveAssetId():Pipe({ - Rx.map(function(assetId) - return PlayerAssetOwnershipUtils.toIdOwnedAttribute(assetType, assetId) - end); - RxBrioUtils.switchToBrio(); - }); - gameConfigAsset:ObserveAssetKey():Pipe({ - Rx.map(function(assetKey) - return PlayerAssetOwnershipUtils.toKeyOwnedAttribute(assetType, assetKey) - end); - RxBrioUtils.switchToBrio(); - }); - }) -end - -return PlayerAssetOwnershipUtils \ No newline at end of file diff --git a/src/gameproductservice/src/Shared/Ownership/WellKnownAssetOwnershipHandler.lua b/src/gameproductservice/src/Shared/Ownership/WellKnownAssetOwnershipHandler.lua deleted file mode 100644 index c96da2649e..0000000000 --- a/src/gameproductservice/src/Shared/Ownership/WellKnownAssetOwnershipHandler.lua +++ /dev/null @@ -1,82 +0,0 @@ ---[=[ - @class WellKnownAssetOwnershipHandler -]=] - -local require = require(script.Parent.loader).load(script) - -local BaseObject = require("BaseObject") -local PlayerAssetOwnershipUtils = require("PlayerAssetOwnershipUtils") -local ValueObject = require("ValueObject") - -local WellKnownAssetOwnershipHandler = setmetatable({}, BaseObject) -WellKnownAssetOwnershipHandler.ClassName = "WellKnownAssetOwnershipHandler" -WellKnownAssetOwnershipHandler.__index = WellKnownAssetOwnershipHandler - -function WellKnownAssetOwnershipHandler.new(adornee, gameConfigAsset) - local self = setmetatable(BaseObject.new(adornee), WellKnownAssetOwnershipHandler) - - self._gameConfigAsset = assert(gameConfigAsset, "No gameConfigAsset") - - self._isOwned = ValueObject.new(false, "boolean") - self._maid:GiveTask(self._isOwned) - - self._maid:GiveTask(self:_observeAttributeNamesBrio():Subscribe(function(brio) - if brio:IsDead() then - return - end - - local maid = brio:ToMaid() - local attributeName = brio:GetValue() - - maid:GiveTask(self._isOwned.Changed:Connect(function() - self._obj:SetAttribute(attributeName, self._isOwned.Value) - end)) - - -- preload the data - if self._obj:GetAttribute(attributeName) == nil then - self._obj:SetAttribute(attributeName, self._isOwned.Value) - end - - -- Sync it up if something explicit happens - maid:GiveTask(self._obj:GetAttributeChangedSignal(attributeName):Connect(function() - self:SetIsOwned(self._obj:GetAttribute(attributeName) and true or false) - end)) - end)) - - return self -end - ---[=[ - Sets if the asset is owned - - @param isOwned boolean -]=] -function WellKnownAssetOwnershipHandler:SetIsOwned(isOwned) - assert(type(isOwned) == "boolean", "Bad isOwned") - - self._isOwned.Value = isOwned -end - ---[=[ - Gets if the asset is owned - - @return boolean -]=] -function WellKnownAssetOwnershipHandler:GetIsOwned() - return self._isOwned.Value -end - ---[=[ - Observes if the asset is owned - - @return Observable -]=] -function WellKnownAssetOwnershipHandler:ObserveIsOwned() - return self._isOwned:Observe() -end - -function WellKnownAssetOwnershipHandler:_observeAttributeNamesBrio() - return PlayerAssetOwnershipUtils.observeAttributeNamesForGameConfigAssetBrio(self._gameConfigAsset:GetAssetType(), self._gameConfigAsset) -end - -return WellKnownAssetOwnershipHandler \ No newline at end of file From dae065fa838e854f8046f056a73e1914863f8640 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Sun, 24 Nov 2024 13:04:02 -0800 Subject: [PATCH 15/30] refactor: Optimize switchMap and flatMap calls to use less maids, and to cancel and merge properly --- src/brio/src/Shared/RxBrioUtils.lua | 5 +- src/rx/src/Shared/Rx.lua | 388 ++++++++++++++-------------- src/rx/src/Shared/Subscription.lua | 1 - 3 files changed, 194 insertions(+), 200 deletions(-) diff --git a/src/brio/src/Shared/RxBrioUtils.lua b/src/brio/src/Shared/RxBrioUtils.lua index 009fa4600c..ed16281aca 100644 --- a/src/brio/src/Shared/RxBrioUtils.lua +++ b/src/brio/src/Shared/RxBrioUtils.lua @@ -407,15 +407,14 @@ end @deprecated 3.6.0 -- This method does not wrap the resulting value in a Brio, which can sometimes lead to leaks. @param project (value: TBrio) -> TProject - @param resultSelector ((initial TBrio, value: TProject) -> TResult)? @return (source: Observable> -> Observable) ]=] -function RxBrioUtils.flatMap(project, resultSelector) +function RxBrioUtils.flatMap(project) assert(type(project) == "function", "Bad project") warn("[RxBrioUtils.flatMap] - Deprecated since 3.6.0. Use RxBrioUtils.flatMapBrio") - return Rx.flatMap(RxBrioUtils.mapBrio(project), resultSelector) + return Rx.flatMap(RxBrioUtils.mapBrio(project)) end --[=[ diff --git a/src/rx/src/Shared/Rx.lua b/src/rx/src/Shared/Rx.lua index 0f5804bf6c..c29ce3743a 100644 --- a/src/rx/src/Shared/Rx.lua +++ b/src/rx/src/Shared/Rx.lua @@ -22,6 +22,9 @@ local CancelToken = require("CancelToken") local MaidTaskUtils = require("MaidTaskUtils") local UNSET_VALUE = Symbol.named("unsetValue") +local function identity(...) + return ... +end --[=[ An empty observable that completes immediately @@ -901,72 +904,7 @@ end @return (source: Observable>) -> Observable ]=] function Rx.mergeAll() - return function(source) - assert(Observable.isObservable(source), "Bad observable") - - return Observable.new(function(sub) - local topMaid = Maid.new() - - local pendingCount = 0 - local topComplete = false - - topMaid:GiveTask(source:Subscribe( - function(observable) - assert(Observable.isObservable(observable), "Not an observable") - - pendingCount = pendingCount + 1 - - local innerSub = nil - innerSub = observable:Subscribe( - function(...) - -- Merge each inner observable - sub:Fire(...) - end, - function(...) - -- Emit failure automatically - sub:Fail(...) - topMaid:DoCleaning() - - if innerSub then - topMaid[innerSub] = nil - end - end, - function() - pendingCount = pendingCount - 1 - if pendingCount == 0 and topComplete then - sub:Complete() - topMaid:DoCleaning() - end - - if innerSub then - topMaid[innerSub] = nil - end - end) - - -- Make sure we only do this if we aren't already cleaned up here - if innerSub:IsPending() then - if sub:IsPending() then - topMaid[innerSub] = innerSub - else - innerSub:Destroy() - end - end - end, - function(...) - sub:Fail(...) -- Also reflect failures up to the top! - topMaid:DoCleaning() - end, - function() - topComplete = true - if pendingCount == 0 then - sub:Complete() - topMaid:DoCleaning() - end - end)) - - return topMaid - end) - end + return Rx.flatMap(identity) end --[=[ @@ -981,77 +919,7 @@ end @return (source: Observable>) -> Observable ]=] function Rx.switchAll() - return function(source) - assert(Observable.isObservable(source), "Bad observable") - - return Observable.new(function(sub) - local outerMaid = Maid.new() - local topComplete = false - local insideComplete = false - local currentInside = nil - - outerMaid:GiveTask(function() - -- Ensure inner subscription is disconnected first. This prevents - -- the inner sub from firing while the outer is subscribed, - -- throwing a warning. - outerMaid._innerSub = nil - outerMaid._outerSuber = nil - end) - - outerMaid._outerSuber = source:Subscribe( - function(observable) - assert(Observable.isObservable(observable), "Bad observable returned from subscription in switchAll()") - - insideComplete = false - currentInside = observable - outerMaid._innerSub = nil - - local subscription = observable:Subscribe( - function(...) - if currentInside == observable then - sub:Fire(...) - else - warn(string.format("[Rx.switchAll] - Observable is still firing despite disconnect (%q)", tostring(observable._source))) - end - end, -- Merge each inner observable - function(...) - if currentInside == observable then - currentInside = nil - sub:Fail(...) - end - end, -- Emit failure automatically - function() - if currentInside == observable then - insideComplete = true - if insideComplete and topComplete then - sub:Complete() - outerMaid:DoCleaning() -- Paranoid ensure cleanup. - end - end - end) - - if currentInside == observable then - outerMaid._innerSub = subscription - else - -- We cleaned up while connecting - subscription:Destroy() - end - end, - function(...) - sub:Fail(...) -- Also reflect failures up to the top! - outerMaid:DoCleaning() - end, - function() - topComplete = true - if insideComplete and topComplete then - sub:Complete() - outerMaid:DoCleaning() -- Paranoid ensure cleanup - end - end) - - return outerMaid - end) - end + return Rx.switchMap(identity) end --[=[ @@ -1060,77 +928,107 @@ end This takes a stream of observables @param project (value: T) -> Observable - @param resultSelector ((initialValue: T, outputValue: U) -> U)? @return (source: Observable) -> Observable ]=] -function Rx.flatMap(project, resultSelector) +function Rx.flatMap(project) assert(type(project) == "function", "Bad project") return function(source) assert(Observable.isObservable(source), "Bad observable") return Observable.new(function(sub) - local maid = Maid.new() - + local isComplete = false local pendingCount = 0 - local topComplete = false + local subscriptions = {} - maid:GiveTask(source:Subscribe( - function(...) - local outerValue = ... + local function checkComplete() + if isComplete and pendingCount == 0 then + sub:Complete() + end + end - local observable = project(...) - assert(Observable.isObservable(observable), "Bad observable from project") + local function onNextObservable(...) + local observable = project(...) + assert(Observable.isObservable(observable), "Bad observable returned from subscription project call") - pendingCount = pendingCount + 1 + if not sub:IsPending() then + -- Projecting last subscription cancelled us + return + end - local innerMaid = Maid.new() + local innerCompleteOrFail = false + local subscription - local subscription = innerMaid:Add(observable:Subscribe( - function(...) - -- Merge each inner observable - if resultSelector then - sub:Fire(resultSelector(outerValue, ...)) - else - sub:Fire(...) - end - end, - function(...) - sub:Fail(...) - end, -- Emit failure automatically - function() - innerMaid:DoCleaning() - pendingCount = pendingCount - 1 - if pendingCount == 0 and topComplete then - sub:Complete() - maid:DoCleaning() - end - end)) - - if subscription:IsPending() then - local key = maid:GiveTask(innerMaid) - - -- Cleanup - innerMaid:GiveTask(function() - maid[key] = nil - end) - else - subscription:Destroy() + local function onNext(...) + if innerCompleteOrFail or pendingCount == 0 then + return end - end, + + sub:Fire(...) + end + + local function onFail(...) + if innerCompleteOrFail or pendingCount == 0 then + return + end + + if subscription then + -- Trust subscription to clean itself up in this scenario + subscriptions[subscription] = nil + subscription = nil + end + + pendingCount = 0 + sub:Fail(...) + end + + local function onComplete() + if innerCompleteOrFail or pendingCount == 0 then + return + end + + innerCompleteOrFail = true + pendingCount -= 1 + + if subscription then + -- Trust subscription to clean itself up in this scenario + subscriptions[subscription] = nil + subscription = nil + end + + checkComplete() + end + + pendingCount += 1 + subscription = observable:Subscribe(onNext, onFail, onComplete) + + if innerCompleteOrFail or not sub:IsPending() then + -- Subscribing cancelled ourselves in some way + subscription:Destroy() + return + end + + subscriptions[subscription] = true + end + + local outerSubscription = source:Subscribe(onNextObservable, function(...) - sub:Fail(...) -- Also reflect failures up to the top! - maid:DoCleaning() + sub:Fail(...) end, function() - topComplete = true - if pendingCount == 0 then - sub:Complete() - maid:DoCleaning() - end - end)) + isComplete = true + checkComplete() + end) - return maid + return function() + pendingCount = 0 + + for subscription, _ in pairs(subscriptions) do + subscription:Destroy() + end + + outerSubscription:Destroy() + end end) end end @@ -1192,10 +1090,110 @@ end @return Observable ]=] function Rx.switchMap(project) - return Rx.pipe({ - Rx.map(project); - Rx.switchAll(); - }) + assert(type(project) == "function", "Bad project") + + return function(source) + assert(Observable.isObservable(source), "Bad observable") + + return Observable.new(function(sub) + local isComplete = false + local insideComplete = false + local insideSubscription = nil + local outerIndex = 0 + + local function checkComplete() + if isComplete and insideComplete then + outerIndex = nil + sub:Complete() + end + end + + local function onNextObservable(...) + insideComplete = false + + local observable = project(...) + assert(Observable.isObservable(observable), "Bad observable returned from subscription project call") + + -- Handle cancellation when external callers do weird state stuff + if not outerIndex then + return + end + + local index = outerIndex + 1 + outerIndex = index + + -- Cancel previous subscription + if insideSubscription then + insideSubscription:Destroy() + insideSubscription = nil + end + + if not sub:IsPending() or index ~= outerIndex then + -- Cancelling last subscription cancelled us + return + end + + local function onNext(...) + if index ~= outerIndex then + return + end + + sub:Fire(...) + end + + local function onFail(...) + if index ~= outerIndex then + return + end + + insideSubscription = nil -- trust subscription to clean itself up + outerIndex = nil + sub:Fail(...) + end + + local function onComplete() + if index ~= outerIndex then + return + end + + insideSubscription = nil -- trust subscription to clean itself up + insideComplete = true + checkComplete() + end + + local subscription = observable:Subscribe(onNext, onFail, onComplete) + + if not sub:IsPending() or index ~= outerIndex then + -- Subscribing cancelled ourselves + subscription:Destroy() + return + end + + insideSubscription = subscription + end + + local outerSubscription = source:Subscribe(onNextObservable, + function(...) + outerIndex = nil + sub:Fail(...) + end, + function() + isComplete = true + checkComplete() + end) + + return function() + outerIndex = nil + + if insideSubscription then + insideSubscription:Destroy() + insideSubscription = nil + end + + outerSubscription:Destroy() + end + end) + end end function Rx.takeUntil(notifier) @@ -2167,9 +2165,7 @@ function Rx.mergeScan(accumulator, seed) return Rx.pipe({ Rx.scan(accumulator, seed), - Rx.flatMap(function(x) - return x - end) + Rx.mergeAll(); }) end diff --git a/src/rx/src/Shared/Subscription.lua b/src/rx/src/Shared/Subscription.lua index 854a2da1df..f32f00f374 100644 --- a/src/rx/src/Shared/Subscription.lua +++ b/src/rx/src/Shared/Subscription.lua @@ -224,7 +224,6 @@ function Subscription:Destroy() end self:_doCleanup() - end --[=[ From b5d379b3553fb94b954870562e1bd2a57e9a1c10 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Sun, 24 Nov 2024 13:07:23 -0800 Subject: [PATCH 16/30] style: Remove unused imports --- src/gameproductservice/src/Shared/GameProductDataService.lua | 1 - .../src/Shared/Ownership/PlayerAssetOwnershipTracker.lua | 1 - 2 files changed, 2 deletions(-) diff --git a/src/gameproductservice/src/Shared/GameProductDataService.lua b/src/gameproductservice/src/Shared/GameProductDataService.lua index 389f26567d..9a638fe1ad 100644 --- a/src/gameproductservice/src/Shared/GameProductDataService.lua +++ b/src/gameproductservice/src/Shared/GameProductDataService.lua @@ -11,7 +11,6 @@ local PlayerProductManagerInterface = require("PlayerProductManagerInterface") local Promise = require("Promise") local Rx = require("Rx") local RxBrioUtils = require("RxBrioUtils") -local RxStateStackUtils = require("RxStateStackUtils") local Signal = require("Signal") local TieRealmService = require("TieRealmService") diff --git a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua index 0d8458d643..9038e92b6a 100644 --- a/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua +++ b/src/gameproductservice/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua @@ -12,7 +12,6 @@ local BaseObject = require("BaseObject") local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils") local ObservableSet = require("ObservableSet") local Promise = require("Promise") -local Rx = require("Rx") local ValueObject = require("ValueObject") local Observable = require("Observable") local Maid = require("Maid") From 61c9b9782dc62f37b3bfc81febea3dbec406f157 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:28:58 -0800 Subject: [PATCH 17/30] feat: Add PlayerUtils.formatDisplayName(name) and other useful methods --- src/playerutils/src/Shared/PlayerUtils.lua | 31 +++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/playerutils/src/Shared/PlayerUtils.lua b/src/playerutils/src/Shared/PlayerUtils.lua index 9b2ed617c9..295fd29b26 100644 --- a/src/playerutils/src/Shared/PlayerUtils.lua +++ b/src/playerutils/src/Shared/PlayerUtils.lua @@ -42,7 +42,7 @@ end @param displayName string @return string -- Formatted name ]=] -function PlayerUtils.formatDisplayName(name, displayName) +function PlayerUtils.formatDisplayName(name: string, displayName: string): string if string.lower(name) == string.lower(displayName) then return displayName else @@ -50,6 +50,35 @@ function PlayerUtils.formatDisplayName(name, displayName) end end +--[=[ + Formats the display name from the user info + @param userInfo UserInfo + @return string +]=] +function PlayerUtils.formatDisplayNameFromUserInfo(userInfo): string + assert(type(userInfo) == "table", "Bad userInfo") + assert(type(userInfo.Username) == "string", "Bad userInfo.Username") + assert(type(userInfo.DisplayName) == "string", "Bad userInfo.DisplayName") + + local result = PlayerUtils.formatDisplayName(userInfo.Username, userInfo.DisplayName) + + if userInfo.HasVerifiedBadge then + return PlayerUtils.addVerifiedBadgeToName(result) + end + + return result +end + +--[=[ + Adds verified badges to the name + + @param name string + @return string +]=] +function PlayerUtils.addVerifiedBadgeToName(name: string): string + return string.format("%s %s", name, utf8.char(0xE000)) +end + local NAME_COLORS = { BrickColor.new("Bright red").Color; BrickColor.new("Bright blue").Color; From e6b0838eefee355da68cf0a0ef19388ab60ead38 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:29:09 -0800 Subject: [PATCH 18/30] feat: Add BoundingBox API calls --- .../src/Shared/AdorneeBoundingBox.lua | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/adorneeboundingbox/src/Shared/AdorneeBoundingBox.lua b/src/adorneeboundingbox/src/Shared/AdorneeBoundingBox.lua index 9b82c0b9ac..ebd2631341 100644 --- a/src/adorneeboundingbox/src/Shared/AdorneeBoundingBox.lua +++ b/src/adorneeboundingbox/src/Shared/AdorneeBoundingBox.lua @@ -51,6 +51,31 @@ function AdorneeBoundingBox:SetAdornee(adornee) end end +function AdorneeBoundingBox:ObserveBoundingBox() + return Rx.combineLatest({ + CFrame = self:ObserveCFrame(); + Size = self:ObserveSize(); + }):Pipe({ + Rx.where(function(state) + return state.CFrame and state.Size + end); + }) +end + +function AdorneeBoundingBox:GetBoundingBox() + local cframe = self._bbCFrame.Value + local size = self._bbSize.Value + + if cframe and size then + return { + CFrame = cframe; + Size = size; + } + else + return nil + end +end + --[=[ Observes the cframe of the adornee @return Observable From c07db0937e56055ead6cd4332e2fa19f4ab4259e Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:30:01 -0800 Subject: [PATCH 19/30] feat: Add UGCSanitize to FirstPersonTransparency --- readme.md | 5 +- .../README.md | 2 +- .../package.json | 3 +- .../src/Client/DisableHatParticles.lua | 69 ------------ src/ugcsanitize/README.md | 23 ++++ src/ugcsanitize/default.project.json | 7 ++ src/ugcsanitize/package.json | 37 +++++++ .../src/Shared/DisableHatParticles.lua | 101 ++++++++++++++++++ .../src/Shared/UGCSanitizeUtils.lua | 9 ++ src/ugcsanitize/src/node_modules.project.json | 7 ++ src/ugcsanitize/test/default.project.json | 12 +++ 11 files changed, 202 insertions(+), 73 deletions(-) delete mode 100644 src/firstpersoncharactertransparency/src/Client/DisableHatParticles.lua create mode 100644 src/ugcsanitize/README.md create mode 100644 src/ugcsanitize/default.project.json create mode 100644 src/ugcsanitize/package.json create mode 100644 src/ugcsanitize/src/Shared/DisableHatParticles.lua create mode 100644 src/ugcsanitize/src/Shared/UGCSanitizeUtils.lua create mode 100644 src/ugcsanitize/src/node_modules.project.json create mode 100644 src/ugcsanitize/test/default.project.json diff --git a/readme.md b/readme.md index 8176d87027..1ac0fefbc4 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ Many of these packages represent not just useful code, but useful patterns, or w ### All packages -There are 267 packages in Nevermore. +There are 268 packages in Nevermore. | Package | Description | Install | docs | source | changelog | npm | | ------- | ----------- | ------- | ---- | ------ | --------- | --- | @@ -134,7 +134,7 @@ There are 267 packages in Nevermore. | [EquippedTracker](https://quenty.github.io/NevermoreEngine/api/EquippedTracker) | Tracks the equipped player of a tool | `npm i @quenty/equippedtracker` | [docs](https://quenty.github.io/NevermoreEngine/api/EquippedTracker) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/equippedtracker) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/equippedtracker/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/equippedtracker) | | [ExperienceCalculator](https://quenty.github.io/NevermoreEngine/api/ExperienceUtils) | Calculate experience on an exponential curve and perform relevant calculations Uses formulas from stackoverflow.com/questions/6954874/php-game-formula-to-calculate-a-level-based-on-exp | `npm i @quenty/experiencecalculator` | [docs](https://quenty.github.io/NevermoreEngine/api/ExperienceUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/experiencecalculator) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/experiencecalculator/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/experiencecalculator) | | [FakeSkybox](https://quenty.github.io/NevermoreEngine/api/FakeSkybox) | Allow transitions between skyboxes | `npm i @quenty/fakeskybox` | [docs](https://quenty.github.io/NevermoreEngine/api/FakeSkybox) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/fakeskybox) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/fakeskybox/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/fakeskybox) | -| [FirstPersonCharacterTransparency](https://quenty.github.io/NevermoreEngine/api/DisableHatParticles) | Allows transparency to manually be controlled for a character in first-person mode | `npm i @quenty/firstpersoncharactertransparency` | [docs](https://quenty.github.io/NevermoreEngine/api/DisableHatParticles) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/firstpersoncharactertransparency) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/firstpersoncharactertransparency/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/firstpersoncharactertransparency) | +| [FirstPersonCharacterTransparency](https://quenty.github.io/NevermoreEngine/api/ShowBody) | Allows transparency to manually be controlled for a character in first-person mode | `npm i @quenty/firstpersoncharactertransparency` | [docs](https://quenty.github.io/NevermoreEngine/api/ShowBody) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/firstpersoncharactertransparency) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/firstpersoncharactertransparency/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/firstpersoncharactertransparency) | | [Flipbook](https://quenty.github.io/NevermoreEngine/api/Flipbook) | Handles playing back animated spritesheets | `npm i @quenty/flipbook` | [docs](https://quenty.github.io/NevermoreEngine/api/Flipbook) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/flipbook) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/flipbook/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/flipbook) | | [FriendUtils](https://quenty.github.io/NevermoreEngine/api/FriendUtils) | Utlity functions to help find friends of a user. Also contains utility to make testing in studio easier. | `npm i @quenty/friendutils` | [docs](https://quenty.github.io/NevermoreEngine/api/FriendUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/friendutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/friendutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/friendutils) | | [FunctionUtils](https://quenty.github.io/NevermoreEngine/api/FunctionUtils) | Utility functions involving functions | `npm i @quenty/functionutils` | [docs](https://quenty.github.io/NevermoreEngine/api/FunctionUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/functionutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/functionutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/functionutils) | @@ -314,6 +314,7 @@ There are 267 packages in Nevermore. | [TransitionModel](https://quenty.github.io/NevermoreEngine/api/TransitionModelUtils) | Helps with Gui visiblity showing and hiding | `npm i @quenty/transitionmodel` | [docs](https://quenty.github.io/NevermoreEngine/api/TransitionModelUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/transitionmodel) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/transitionmodel/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/transitionmodel) | | [TransparencyService](https://quenty.github.io/NevermoreEngine/api/TransparencyService) | Service that orchistrates transparency setting from multiple colliding sources and handle the transparency appropriately. This means that 2 systems can work with transparency without knowing about each other. | `npm i @quenty/transparencyservice` | [docs](https://quenty.github.io/NevermoreEngine/api/TransparencyService) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/transparencyservice) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/transparencyservice/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/transparencyservice) | | [Tuple](https://quenty.github.io/NevermoreEngine/api/TupleUtils) | Tuple utility package | `npm i @quenty/tuple` | [docs](https://quenty.github.io/NevermoreEngine/api/TupleUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/tuple) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/tuple/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/tuple) | +| [UGCSanitize](https://quenty.github.io/NevermoreEngine/api/UGCSanitizeUtils) | Sanitizes UGC hats and other items | `npm i @quenty/ugcsanitize` | [docs](https://quenty.github.io/NevermoreEngine/api/UGCSanitizeUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/ugcsanitize) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/ugcsanitize/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/ugcsanitize) | | [UIObjectUtils](https://quenty.github.io/NevermoreEngine/api/PlayerGuiUtils) | UI object utils library for Roblox | `npm i @quenty/uiobjectutils` | [docs](https://quenty.github.io/NevermoreEngine/api/PlayerGuiUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/uiobjectutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/uiobjectutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/uiobjectutils) | | [UltrawideContainerUtils](https://quenty.github.io/NevermoreEngine/api/UltrawideContainerUtils) | Creates a 1920x1080 scaling container to handle ultrawide monitors and screens in a reasonable way. This helps keep UI centered and available for ultrawide screens. | `npm i @quenty/ultrawidecontainerutils` | [docs](https://quenty.github.io/NevermoreEngine/api/UltrawideContainerUtils) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/ultrawidecontainerutils) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/ultrawidecontainerutils/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/ultrawidecontainerutils) | | [UndoStack](https://quenty.github.io/NevermoreEngine/api/UndoStack) | Generalized undo stack for Roblox | `npm i @quenty/undostack` | [docs](https://quenty.github.io/NevermoreEngine/api/UndoStack) | [source](https://github.com/Quenty/NevermoreEngine/tree/main/src/undostack) | [changelog](https://github.com/Quenty/NevermoreEngine/tree/main/src/undostack/CHANGELOG.md) | [npm](https://www.npmjs.com/package/@quenty/undostack) | diff --git a/src/firstpersoncharactertransparency/README.md b/src/firstpersoncharactertransparency/README.md index 202c0f385a..4d48aaa39f 100644 --- a/src/firstpersoncharactertransparency/README.md +++ b/src/firstpersoncharactertransparency/README.md @@ -13,7 +13,7 @@ Allows transparency to manually be controlled for a character in first-person mode - + ## Installation ``` diff --git a/src/firstpersoncharactertransparency/package.json b/src/firstpersoncharactertransparency/package.json index 28c9a06758..6e327c2db6 100644 --- a/src/firstpersoncharactertransparency/package.json +++ b/src/firstpersoncharactertransparency/package.json @@ -35,7 +35,8 @@ "@quenty/r15utils": "file:../r15utils", "@quenty/rx": "file:../rx", "@quenty/statestack": "file:../statestack", - "@quenty/transparencyservice": "file:../transparencyservice" + "@quenty/transparencyservice": "file:../transparencyservice", + "@quenty/ugcsanitize": "file:../ugcsanitize" }, "publishConfig": { "access": "public" diff --git a/src/firstpersoncharactertransparency/src/Client/DisableHatParticles.lua b/src/firstpersoncharactertransparency/src/Client/DisableHatParticles.lua deleted file mode 100644 index fd9b7b846c..0000000000 --- a/src/firstpersoncharactertransparency/src/Client/DisableHatParticles.lua +++ /dev/null @@ -1,69 +0,0 @@ ---[=[ - @class DisableHatParticles -]=] - -local require = require(script.Parent.loader).load(script) - -local BaseObject = require("BaseObject") -local Maid = require("Maid") - -local DisableHatParticles = setmetatable({}, BaseObject) -DisableHatParticles.ClassName = "DisableHatParticles" -DisableHatParticles.__index = DisableHatParticles - -function DisableHatParticles.new(character) - local self = setmetatable(BaseObject.new(character), DisableHatParticles) - - -- Connect - self._maid:GiveTask(self._obj.ChildRemoved:Connect(function(child) - self:_handleChildRemoving(child) - end)) - self._maid:GiveTask(self._obj.ChildAdded:Connect(function(child) - self:_handleChild(child) - end)) - - for _, child in pairs(self._obj:GetChildren()) do - self:_handleChild(child) - end - - return self -end - -function DisableHatParticles:_handleChild(child) - if not child:IsA("Accessory") then - return - end - - local maid = Maid.new() - - local function handleDescendant(descendant) - if descendant:IsA("Fire") - or descendant:IsA("Sparkles") - or descendant:IsA("Smoke") - or descendant:IsA("ParticleEmitter") then - if descendant.Enabled then - maid[descendant] = function() - descendant.Enabled = true - end - - descendant.Enabled = false - end - end - end - maid:GiveTask(child.DescendantAdded:Connect(handleDescendant)) - maid:GiveTask(child.DescendantRemoving:Connect(function(descendant) - maid[descendant] = nil - end)) - - for _, descendant in pairs(child:GetDescendants()) do - handleDescendant(descendant) - end - - self._maid[child] = maid -end - -function DisableHatParticles:_handleChildRemoving(child) - self._maid[child] = nil -end - -return DisableHatParticles \ No newline at end of file diff --git a/src/ugcsanitize/README.md b/src/ugcsanitize/README.md new file mode 100644 index 0000000000..7172419554 --- /dev/null +++ b/src/ugcsanitize/README.md @@ -0,0 +1,23 @@ +## UGCSanitize + + + +Sanitizes UGC hats and other items + + + +## Installation + +``` +npm install @quenty/ugcsanitize --save +``` diff --git a/src/ugcsanitize/default.project.json b/src/ugcsanitize/default.project.json new file mode 100644 index 0000000000..134ad9e09e --- /dev/null +++ b/src/ugcsanitize/default.project.json @@ -0,0 +1,7 @@ +{ + "name": "ugcsanitize", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": "src" + } +} diff --git a/src/ugcsanitize/package.json b/src/ugcsanitize/package.json new file mode 100644 index 0000000000..c2f56f752c --- /dev/null +++ b/src/ugcsanitize/package.json @@ -0,0 +1,37 @@ +{ + "name": "@quenty/ugcsanitize", + "version": "1.0.0", + "description": "Sanitizes UGC hats and other items", + "keywords": [ + "Roblox", + "Nevermore", + "Lua", + "ugcsanitize" + ], + "bugs": { + "url": "https://github.com/Quenty/NevermoreEngine/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Quenty/NevermoreEngine.git", + "directory": "src/ugcsanitize/" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/quenty" + }, + "license": "MIT", + "contributors": [ + "Quenty" + ], + "dependencies": { + "@quenty/baseobject": "file:../baseobject", + "@quenty/instanceutils": "file:../instanceutils", + "@quenty/loader": "file:../loader", + "@quenty/maid": "file:../maid", + "@quenty/string": "file:../string" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/src/ugcsanitize/src/Shared/DisableHatParticles.lua b/src/ugcsanitize/src/Shared/DisableHatParticles.lua new file mode 100644 index 0000000000..cd68551e5a --- /dev/null +++ b/src/ugcsanitize/src/Shared/DisableHatParticles.lua @@ -0,0 +1,101 @@ +--[=[ + @class DisableHatParticles +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local RxInstanceUtils = require("RxInstanceUtils") +local String = require("String") + +local DisableHatParticles = setmetatable({}, BaseObject) +DisableHatParticles.ClassName = "DisableHatParticles" +DisableHatParticles.__index = DisableHatParticles + +function DisableHatParticles.new(character) + local self = setmetatable(BaseObject.new(character), DisableHatParticles) + + self._maid:GiveTask(RxInstanceUtils.observeChildrenOfClassBrio(self._obj, "Accessory"):Subscribe(function(brio) + if brio:IsDead() then + return + end + + local maid, accessory = brio:ToMaidAndValue() + self:_handleAccessory(maid, accessory) + end)) + + return self +end + +function DisableHatParticles:_handleAccessory(maid, accessory) + maid:GiveTask(accessory.DescendantAdded:Connect(function(descendant) + self:_handleAccessoryDescendant(maid, descendant) + end)) + maid:GiveTask(accessory.DescendantRemoving:Connect(function(descendant) + maid[descendant] = nil + end)) + + for _, descendant in pairs(accessory:GetDescendants()) do + self:_handleAccessoryDescendant(maid, descendant) + end +end + +function DisableHatParticles:_handleAccessoryDescendant(maid, descendant) + if descendant:IsA("Fire") + or descendant:IsA("Sparkles") + or descendant:IsA("Smoke") + or descendant:IsA("ParticleEmitter") then + if descendant.Enabled then + maid[descendant] = function() + descendant.Enabled = true + end + + descendant.Enabled = false + end + end + + -- TODO: This code is unsafe? Use a sound group? + if self:_isASoundScript(descendant) then + maid[descendant] = function() + descendant.Enabled = true + end + + descendant.Enabled = false + end + + if self:_isSound(descendant) then + local originalVolume = descendant.Volume + maid[descendant] = function() + descendant.Volume = originalVolume + end + + descendant.Volume = 0 + end +end + +function DisableHatParticles:_isASoundScript(descendant) + if not descendant:IsA("LocalScript") then + return false + end + + if String.endsWith(descendant.Name, "Sounds") then + return true + end + + if String.endsWith(descendant.Name, "Sound") then + return true + end + + if String.startsWith(descendant.Name, "Sound") then + return true + end + + return false +end + +function DisableHatParticles:_isSound(descendant) + -- Sound group check is paranoid but likely to be valid as to identify hat-sounds + return descendant:IsA("Sound") and descendant.SoundGroup == nil +end + +return DisableHatParticles \ No newline at end of file diff --git a/src/ugcsanitize/src/Shared/UGCSanitizeUtils.lua b/src/ugcsanitize/src/Shared/UGCSanitizeUtils.lua new file mode 100644 index 0000000000..f81b868f67 --- /dev/null +++ b/src/ugcsanitize/src/Shared/UGCSanitizeUtils.lua @@ -0,0 +1,9 @@ +--[=[ + @class UGCSanitizeUtils +]=] + +local require = require(script.Parent.loader).load(script) + +local UGCSanitizeUtils = {} + +return UGCSanitizeUtils \ No newline at end of file diff --git a/src/ugcsanitize/src/node_modules.project.json b/src/ugcsanitize/src/node_modules.project.json new file mode 100644 index 0000000000..46233dac4f --- /dev/null +++ b/src/ugcsanitize/src/node_modules.project.json @@ -0,0 +1,7 @@ +{ + "name": "node_modules", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$path": { "optional": "../node_modules" } + } +} \ No newline at end of file diff --git a/src/ugcsanitize/test/default.project.json b/src/ugcsanitize/test/default.project.json new file mode 100644 index 0000000000..0b06815193 --- /dev/null +++ b/src/ugcsanitize/test/default.project.json @@ -0,0 +1,12 @@ +{ + "name": "UGCSanitizeTest", + "globIgnorePaths": [ "**/.package-lock.json" ], + "tree": { + "$className": "DataModel", + "ServerScriptService": { + "ugcsanitize": { + "$path": ".." + } + } + } +} \ No newline at end of file From 92ea9dea686cba1a9757f32648a930840efad2e7 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:30:14 -0800 Subject: [PATCH 20/30] fix: Fix missing services --- src/gameconfig/src/Client/GameConfigServiceClient.lua | 1 + src/gameconfig/src/Server/GameConfigService.lua | 1 + src/gameproductservice/src/Client/GameProductServiceClient.lua | 1 + src/gameproductservice/src/Server/GameProductService.lua | 1 + 4 files changed, 4 insertions(+) diff --git a/src/gameconfig/src/Client/GameConfigServiceClient.lua b/src/gameconfig/src/Client/GameConfigServiceClient.lua index ba7add14bd..ee72b5897b 100644 --- a/src/gameconfig/src/Client/GameConfigServiceClient.lua +++ b/src/gameconfig/src/Client/GameConfigServiceClient.lua @@ -17,6 +17,7 @@ function GameConfigServiceClient:Init(serviceBag) -- External self._serviceBag:GetService(require("CmdrServiceClient")) + self._serviceBag:GetService(require("MarketplaceServiceCache")) -- Internal self._serviceBag:GetService(require("GameConfigCommandServiceClient")) diff --git a/src/gameconfig/src/Server/GameConfigService.lua b/src/gameconfig/src/Server/GameConfigService.lua index 2940fc3a32..9cd8d19ddd 100644 --- a/src/gameconfig/src/Server/GameConfigService.lua +++ b/src/gameconfig/src/Server/GameConfigService.lua @@ -29,6 +29,7 @@ function GameConfigService:Init(serviceBag) -- External self._serviceBag:GetService(require("CmdrService")) + self._serviceBag:GetService(require("MarketplaceServiceCache")) -- Internal self._serviceBag:GetService(require("GameConfigCommandService")) diff --git a/src/gameproductservice/src/Client/GameProductServiceClient.lua b/src/gameproductservice/src/Client/GameProductServiceClient.lua index 9b02621770..78eac23016 100644 --- a/src/gameproductservice/src/Client/GameProductServiceClient.lua +++ b/src/gameproductservice/src/Client/GameProductServiceClient.lua @@ -36,6 +36,7 @@ function GameProductServiceClient:Init(serviceBag) -- External self._serviceBag:GetService(require("AvatarEditorInventoryServiceClient")) self._serviceBag:GetService(require("CmdrServiceClient")) + self._serviceBag:GetService(require("CatalogSearchServiceCache")) self._serviceBag:GetService(require("GameConfigServiceClient")) -- Internal diff --git a/src/gameproductservice/src/Server/GameProductService.lua b/src/gameproductservice/src/Server/GameProductService.lua index b48fe78db8..6e5c911aa3 100644 --- a/src/gameproductservice/src/Server/GameProductService.lua +++ b/src/gameproductservice/src/Server/GameProductService.lua @@ -31,6 +31,7 @@ function GameProductService:Init(serviceBag) -- External self._serviceBag:GetService(require("GameConfigService")) + self._serviceBag:GetService(require("CatalogSearchServiceCache")) self._serviceBag:GetService(require("ReceiptProcessingService")) self._serviceBag:GetService(require("CmdrService")) From a605933b1375e0f0fc4e55f9d1c8dbf9e5893c4b Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:30:24 -0800 Subject: [PATCH 21/30] fix: Fix username query --- .../src/Shared/UserInfoAggregator.lua | 22 ++++++++++++++++--- .../src/Shared/UserInfoService.lua | 14 +++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/userserviceutils/src/Shared/UserInfoAggregator.lua b/src/userserviceutils/src/Shared/UserInfoAggregator.lua index 6e9002c1f0..50625ebddc 100644 --- a/src/userserviceutils/src/Shared/UserInfoAggregator.lua +++ b/src/userserviceutils/src/Shared/UserInfoAggregator.lua @@ -54,17 +54,17 @@ function UserInfoAggregator:PromiseDisplayName(userId) end --[=[ - Promises the user display name for the userId + Promises the Username for the userId @param userId number @return Promise ]=] -function UserInfoAggregator:PromiseDisplayName(userId) +function UserInfoAggregator:PromiseUsername(userId) assert(type(userId) == "number", "Bad userId") return self._aggregator:Promise(userId) :Then(function(userInfo) - return userInfo.DisplayName + return userInfo.Username end) end @@ -111,6 +111,22 @@ function UserInfoAggregator:ObserveDisplayName(userId) }) end +--[=[ + Observes the Username for the userId + + @param userId number + @return Observable +]=] +function UserInfoAggregator:ObserveUsername(userId) + assert(type(userId) == "number", "Bad userId") + + return self._aggregator:Observe(userId):Pipe({ + Rx.map(function(userInfo) + return userInfo.Username + end) + }) +end + --[=[ Observes the user display name for the userId diff --git a/src/userserviceutils/src/Shared/UserInfoService.lua b/src/userserviceutils/src/Shared/UserInfoService.lua index ddfd28c954..3e8c9de5c3 100644 --- a/src/userserviceutils/src/Shared/UserInfoService.lua +++ b/src/userserviceutils/src/Shared/UserInfoService.lua @@ -42,7 +42,7 @@ end function UserInfoService:ObserveUserInfo(userId) assert(type(userId) == "number", "Bad userId") - return self._aggregator:ObserveDisplayName(userId) + return self._aggregator:ObserveUserInfo(userId) end --[=[ @@ -57,6 +57,18 @@ function UserInfoService:PromiseDisplayName(userId) return self._aggregator:PromiseDisplayName(userId) end +--[=[ + Promises the Username for the userId + + @param userId number + @return Promise +]=] +function UserInfoService:PromiseUsername(userId) + assert(type(userId) == "number", "Bad userId") + + return self._aggregator:PromiseUsername(userId) +end + --[=[ Observes the user display name for the userId From afb43373b3376a5e5c608eff0ea0a889240081a8 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:30:37 -0800 Subject: [PATCH 22/30] fix: Symbols should be newproxy() --- src/symbol/src/Shared/Symbol.lua | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/symbol/src/Shared/Symbol.lua b/src/symbol/src/Shared/Symbol.lua index 8ccc5f13fe..d6387eb162 100644 --- a/src/symbol/src/Shared/Symbol.lua +++ b/src/symbol/src/Shared/Symbol.lua @@ -7,8 +7,6 @@ ]=] local Symbol = {} -Symbol.ClassName = "Symbol" -Symbol.__index = Symbol --[=[ Creates a Symbol with the given name. @@ -22,17 +20,24 @@ Symbol.__index = Symbol function Symbol.named(name) assert(type(name) == "string", "Symbols must be created using a string name!") - return table.freeze(setmetatable({ - _symbolName = name; - }, Symbol)) -end + local self = newproxy(true) -function Symbol.isSymbol(value) - return type(value) == "table" and getmetatable(value) == Symbol + local wrappedName = string.format("Symbol(%s)", name) + + getmetatable(self).__tostring = function() + return wrappedName + end + + return self end -function Symbol:__tostring() - return string.format("Symbol(%s)", self._symbolName) +--[=[ + Returns true if a symbol + + @return boolean +]=] +function Symbol.isSymbol(value) + return typeof(value) == "userdata" end -return table.freeze(Symbol) \ No newline at end of file +return Symbol \ No newline at end of file From bc4fb74a9901882843ea4a4a63531b47bf7e6c35 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:30:51 -0800 Subject: [PATCH 23/30] feat: Add RateAggregator --- src/aggregator/src/Shared/RateAggregator.lua | 108 +++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/aggregator/src/Shared/RateAggregator.lua diff --git a/src/aggregator/src/Shared/RateAggregator.lua b/src/aggregator/src/Shared/RateAggregator.lua new file mode 100644 index 0000000000..9c4979aea8 --- /dev/null +++ b/src/aggregator/src/Shared/RateAggregator.lua @@ -0,0 +1,108 @@ +--[=[ + @class RateAggregator +]=] + +local require = require(script.Parent.loader).load(script) + +local BaseObject = require("BaseObject") +local Queue = require("Queue") +local Promise = require("Promise") +local TupleLookup = require("TupleLookup") +local LRUCache = require("LRUCache") + +local RateAggregator = setmetatable({}, BaseObject) +RateAggregator.ClassName = "RateAggregator" +RateAggregator.__index = RateAggregator + +function RateAggregator.new(promiseQuery) + local self = setmetatable(BaseObject.new(), RateAggregator) + + self._promiseQuery = promiseQuery + + -- Configuration + self._maxRequestsPerSecond = 50 + self._minWaitTime = 1/60 + + -- State tracking + self._bankedWaitTime = 0 + self._lastQueryTime = 0 + self._queueRunning = false + + self._queue = Queue.new() + self._tupleLookup = TupleLookup.new() + self._promisesLruCache = LRUCache.new(2000) + + return self +end + +--[=[ + Observes the aggregated data + + @param id number + @return Observable +]=] +function RateAggregator:Promise(...) + local promise = self._maid:GivePromise(Promise.new()) + + local tuple = self._tupleLookup:ToTuple(...) + local found = self._promisesLruCache:get(tuple) + if found then + return found + end + + self._queue:PushRight({ + tuple = tuple; + promise = promise; + }) + + self._promisesLruCache:set(tuple, promise) + + self:_startQueue() + + return promise +end + +function RateAggregator:_startQueue() + if self._queueRunning then + return + end + + self._queueRunning = true + + self._maid._processing = task.spawn(function() + local timeSinceLastQuery = os.clock() - self._lastQueryTime + if timeSinceLastQuery < 1/self._maxRequestsPerSecond then + -- eww + task.wait(1/self._maxRequestsPerSecond) + end + + while not self._queue:IsEmpty() do + local data = self._queue:PopLeft() + self._lastQueryTime = os.clock() + + task.spawn(function() + data.promise:Resolve(self._promiseQuery(data.tuple:Unpack())) + end) + + local thisStepWaitTime = 1/self._maxRequestsPerSecond + local requiredWaitTime = thisStepWaitTime - self._bankedWaitTime + + if requiredWaitTime < self._minWaitTime then + self._bankedWaitTime -= thisStepWaitTime + else + local realWaitTime = math.max(self._minWaitTime, requiredWaitTime) + local timeWaited = task.wait(realWaitTime) + local extraWaitTime = timeWaited - requiredWaitTime + if extraWaitTime > 0 then + self._bankedWaitTime += extraWaitTime + end + end + end + + self._bankedWaitTime = 0 + self._queueRunning = false + self._maid._processing = nil + end) +end + +return RateAggregator \ No newline at end of file From 2eafeccab277e4462235568a31873d743767ad07 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:31:06 -0800 Subject: [PATCH 24/30] fix: Fix missing dependencies --- src/buttonhighlightmodel/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/buttonhighlightmodel/package.json b/src/buttonhighlightmodel/package.json index 34753c7ef8..45f2e14774 100644 --- a/src/buttonhighlightmodel/package.json +++ b/src/buttonhighlightmodel/package.json @@ -29,6 +29,7 @@ "@quenty/acceltween": "file:../acceltween", "@quenty/baseobject": "file:../baseobject", "@quenty/blend": "file:../blend", + "@quenty/instanceutils": "file:../instanceutils", "@quenty/loader": "file:../loader", "@quenty/maid": "file:../maid", "@quenty/rectutils": "file:../rectutils", From f7f07f7509d8bd0c68fec5a3778b6cf38e223578 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:31:15 -0800 Subject: [PATCH 25/30] docs: Add docs to aggregator class --- src/aggregator/package.json | 4 +++- src/aggregator/src/Shared/Aggregator.lua | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/aggregator/package.json b/src/aggregator/package.json index 84f1e0c844..fbfaf1d94d 100644 --- a/src/aggregator/package.json +++ b/src/aggregator/package.json @@ -29,7 +29,9 @@ "@quenty/loader": "file:../loader", "@quenty/lrucache": "file:../lrucache", "@quenty/promise": "file:../promise", - "@quenty/rx": "file:../rx" + "@quenty/queue": "file:../queue", + "@quenty/rx": "file:../rx", + "@quenty/tuple": "file:../tuple" }, "publishConfig": { "access": "public" diff --git a/src/aggregator/src/Shared/Aggregator.lua b/src/aggregator/src/Shared/Aggregator.lua index a4d78461b9..a96542069d 100644 --- a/src/aggregator/src/Shared/Aggregator.lua +++ b/src/aggregator/src/Shared/Aggregator.lua @@ -15,6 +15,14 @@ local Aggregator = setmetatable({}, BaseObject) Aggregator.ClassName = "Aggregator" Aggregator.__index = Aggregator +--[=[ + Creates a new aggregator that aggregates promised results together + + @param debugName string + @param promiseBulkQuery ({ number }) -> Promise + + @return Aggregator +]=] function Aggregator.new(debugName, promiseBulkQuery) assert(type(debugName) == "string", "Bad debugName") @@ -33,6 +41,10 @@ function Aggregator.new(debugName, promiseBulkQuery) return self end +--[=[ + @param id number + @return Promise +]=] function Aggregator:Promise(id) assert(type(id) == "number", "Bad id") @@ -53,10 +65,10 @@ function Aggregator:Promise(id) end --[=[ - Observes the user display name for the id + Observes the aggregated data @param id number - @return Observable + @return Observable ]=] function Aggregator:Observe(id) assert(type(id) == "number", "Bad id") From b4b90471fbb678c6fb84664dbfc32cb9bfc56c2c Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:31:38 -0800 Subject: [PATCH 26/30] fix: Use modern Roblox APIs --- .../src/modules/Server/UIConverterUtils.lua | 2 +- src/adorneeutils/src/Shared/AdorneeUtils.lua | 2 +- .../src/Shared/BoundingBoxUtils.lua | 16 ++++++++-------- src/convexhull/src/Shared/ConvexHull2DUtils.lua | 5 +++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/ui-converter-plugin/src/modules/Server/UIConverterUtils.lua b/plugins/ui-converter-plugin/src/modules/Server/UIConverterUtils.lua index 974eb7a15b..d97396e3d8 100644 --- a/plugins/ui-converter-plugin/src/modules/Server/UIConverterUtils.lua +++ b/plugins/ui-converter-plugin/src/modules/Server/UIConverterUtils.lua @@ -119,7 +119,7 @@ function UIConverterUtils.toLuaPropertyString(value, debugHint) roundY, roundZ) else - return string.format("CFrame.new(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", applyToTuple(roundNumber, value:components())) + return string.format("CFrame.new(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", applyToTuple(roundNumber, value:GetComponents())) end end elseif valueType == "Rect" then diff --git a/src/adorneeutils/src/Shared/AdorneeUtils.lua b/src/adorneeutils/src/Shared/AdorneeUtils.lua index 7d318632e9..6c40a3ab1e 100644 --- a/src/adorneeutils/src/Shared/AdorneeUtils.lua +++ b/src/adorneeutils/src/Shared/AdorneeUtils.lua @@ -19,7 +19,7 @@ function AdorneeUtils.getCenter(adornee) if adornee:IsA("BasePart") then return adornee.Position elseif adornee:IsA("Model") then - return adornee:GetBoundingBox().p + return adornee:GetBoundingBox().Position elseif adornee:IsA("Attachment") then return adornee.WorldPosition elseif adornee:IsA("Humanoid") then diff --git a/src/boundingboxutils/src/Shared/BoundingBoxUtils.lua b/src/boundingboxutils/src/Shared/BoundingBoxUtils.lua index 3e8d5c3adb..77dad3f6e2 100644 --- a/src/boundingboxutils/src/Shared/BoundingBoxUtils.lua +++ b/src/boundingboxutils/src/Shared/BoundingBoxUtils.lua @@ -29,7 +29,7 @@ end @return Vector3 -- Center of bounding box ]=] function BoundingBoxUtils.clampPointToBoundingBox(cframe, size, point) - local transform = cframe:pointToObjectSpace(point) -- transform into local space + local transform = cframe:PointToObjectSpace(point) -- transform into local space local halfSize = size * 0.5 return cframe * Vector3.new( -- Clamp & transform into world space math.clamp(transform.x, -halfSize.x, halfSize.x), @@ -47,7 +47,7 @@ end @return Vector3 ]=] function BoundingBoxUtils.pushPointToLieOnBoundingBox(cframe, size, point) - local transform = cframe:pointToObjectSpace(point) -- transform into local space + local transform = cframe:PointToObjectSpace(point) -- transform into local space local halfSize = size * 0.5 local x = transform.x < 0 and -halfSize.x or halfSize.x local y = transform.y < 0 and -halfSize.y or halfSize.y @@ -109,18 +109,18 @@ end @return Vector3 -- size @return Vector3 -- position ]=] -function BoundingBoxUtils.getBoundingBox(data, relativeTo) +function BoundingBoxUtils.getBoundingBox(data, relativeTo: CFrame?) relativeTo = relativeTo or CFrame.new() local minx, miny, minz = math.huge, math.huge, math.huge local maxx, maxy, maxz = -math.huge, -math.huge, -math.huge for _, obj in pairs(data) do - local cf = relativeTo:toObjectSpace(obj.CFrame) + local cframe = relativeTo:toObjectSpace(obj.CFrame) local size = obj.Size local sx, sy, sz = size.X, size.Y, size.Z - local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = cf:components() + local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = cframe:GetComponents() -- https://zeuxcg.org/2010/10/17/aabb-from-obb-with-component-wise-abs/ local wsx = 0.5 * (math.abs(R00) * sx + math.abs(R01) * sy + math.abs(R02) * sz) @@ -162,7 +162,7 @@ end @return boolean ]=] function BoundingBoxUtils.inBoundingBox(cframe: CFrame, size: Vector3, testPosition: Vector3): boolean - local relative = cframe:pointToObjectSpace(testPosition) + local relative = cframe:PointToObjectSpace(testPosition) local hsx, hsy, hsz = size.X/2, size.Y/2, size.Z/2 local rx, ry, rz = relative.x, relative.y, relative.z @@ -183,7 +183,7 @@ end @return boolean ]=] function BoundingBoxUtils.inCylinderBoundingBox(cframe, size, testPosition) - local relative = cframe:pointToObjectSpace(testPosition) + local relative = cframe:PointToObjectSpace(testPosition) local half_height = size.x/2 local radius = math.min(size.y, size.z)/2 @@ -202,7 +202,7 @@ end @return boolean ]=] function BoundingBoxUtils.inBallBoundingBox(cframe, size, testPosition) - local relative = cframe:pointToObjectSpace(testPosition) + local relative = cframe:PointToObjectSpace(testPosition) local radius = math.min(size.x, size.y, size.z)/2 local rx, ry, rz = relative.x, relative.y, relative.z diff --git a/src/convexhull/src/Shared/ConvexHull2DUtils.lua b/src/convexhull/src/Shared/ConvexHull2DUtils.lua index 9271fe1073..3487d043d6 100644 --- a/src/convexhull/src/Shared/ConvexHull2DUtils.lua +++ b/src/convexhull/src/Shared/ConvexHull2DUtils.lua @@ -47,8 +47,8 @@ end Computes line intersection between vectors ]=] function ConvexHull2DUtils.lineIntersect(a: Vector2, b: Vector2, c: Vector2, d: Vector2): Vector2 | nil - local r = (b - a) - local s = (d - c) + local r = b - a + local s = d - c local dot = r.x * s.y - r.y * s.x local u = ((c.x - a.x) * r.y - (c.y - a.y) * r.x) / dot local t = ((c.x - a.x) * s.y - (c.y - a.y) * s.x) / dot @@ -88,6 +88,7 @@ function ConvexHull2DUtils.raycast(from: Vector2, to: Vector2, hull: { Vector2 } if not closest then return nil, nil, nil end + return closest.point, closest.startPoint, closest.finishPoint end From 52a0cf99d32995644d6bb00a94a59dcd6f62939c Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:31:58 -0800 Subject: [PATCH 27/30] fix: Use enum:FromValue and enum:FromName API methods --- src/enumutils/src/Shared/EnumUtils.lua | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/enumutils/src/Shared/EnumUtils.lua b/src/enumutils/src/Shared/EnumUtils.lua index 3c8a75628c..47a451a465 100644 --- a/src/enumutils/src/Shared/EnumUtils.lua +++ b/src/enumutils/src/Shared/EnumUtils.lua @@ -66,19 +66,9 @@ function EnumUtils.toEnum(enumType, value) return nil end elseif type(value) == "number" then - -- There has to be a better way, right? - for _, item in pairs(enumType:GetEnumItems()) do - if item.Value == value then - return item - end - end - - return nil + return enumType:FromValue(value) elseif type(value) == "string" then - local result = nil - pcall(function() - result = enumType[value] - end) + local result = enumType:FromName(value) if result then return result end @@ -118,7 +108,7 @@ function EnumUtils.decodeFromString(value) if enumType and enumName then local enumValue local ok, err = pcall(function() - enumValue = Enum[enumType][enumName] + enumValue = Enum[enumType]:FromName(enumName) end) if not ok then warn(err, string.format("[EnumUtils.decodeFromString] - Failed to decode %q into an enum value due to %q", value, tostring(err))) From 8544c2b9b5993de54d299ec8cbf4443b721af029 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:32:16 -0800 Subject: [PATCH 28/30] feat: Return a cleanup method, even when amount is 0 --- .../src/Shared/ObservableCountingMap.lua | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/observablecollection/src/Shared/ObservableCountingMap.lua b/src/observablecollection/src/Shared/ObservableCountingMap.lua index 2950c6d309..32ed5c55f6 100644 --- a/src/observablecollection/src/Shared/ObservableCountingMap.lua +++ b/src/observablecollection/src/Shared/ObservableCountingMap.lua @@ -271,18 +271,10 @@ end ]=] function ObservableCountingMap:Set(key, amount) local current = self:Get(key) - if current == amount then - return - end - if current < amount then self:Add(-(amount - current)) - return - elseif current == amount then - return - else + elseif current > amount then self:Add(current - amount) - return end end @@ -298,7 +290,9 @@ function ObservableCountingMap:Add(key, amount) amount = amount or 1 if amount == 0 then - return + return function() + + end end local oldValue = self._map[key] From 03fd05aaefbeccc6cc26359239876e1a39569bc1 Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:40:52 -0800 Subject: [PATCH 29/30] feat: Add :SetValue() behavior to cleanup past mounted sub and return a function to unset the value --- src/valueobject/src/Shared/ValueObject.lua | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/valueobject/src/Shared/ValueObject.lua b/src/valueobject/src/Shared/ValueObject.lua index 7ad22d7fc1..8efc9e639b 100644 --- a/src/valueobject/src/Shared/ValueObject.lua +++ b/src/valueobject/src/Shared/ValueObject.lua @@ -29,6 +29,7 @@ ValueObject.ClassName = "ValueObject" function ValueObject.new(baseValue, checkType) local self = setmetatable({ _value = baseValue; + _default = baseValue; _checkType = checkType; }, ValueObject) @@ -111,7 +112,7 @@ function ValueObject:Mount(value) self:_cleanupLastMountedSub() local sub = observable:Subscribe(function(...) - self:SetValue(...) + ValueObject._applyValue(self, ...) end) rawset(self, "_lastMountedSub", sub) @@ -124,7 +125,7 @@ function ValueObject:Mount(value) else self:_cleanupLastMountedSub() - self:SetValue(value) + ValueObject._applyValue(self, value) return EMPTY_FUNCTION end @@ -234,8 +235,21 @@ end @param value T @param ... any -- Additional args. Can be used to pass event changing state args with value + @return () -> () -- Cleanup ]=] function ValueObject:SetValue(value, ...) + self:_cleanupLastMountedSub() + + ValueObject._applyValue(self, value, ...) + + return function() + if rawget(self, "_value") == value then + ValueObject._applyValue(self, rawget(self, "_default")) + end + end +end + +function ValueObject:_applyValue(value, ...) local previous = rawget(self, "_value") local checkType = rawget(self, "_checkType") @@ -298,7 +312,7 @@ end function ValueObject:__newindex(index, value) if index == "Value" then -- Avoid deoptimization - ValueObject.SetValue(self, value) + ValueObject._applyValue(self, value) elseif index == "LastEventContext" or ValueObject[index] then error(string.format("%q cannot be set in ValueObject", tostring(index))) else From 1d2faf48e778ada6f57aecb8000c7c6c76ac33cd Mon Sep 17 00:00:00 2001 From: James Onnen Date: Tue, 3 Dec 2024 13:48:43 -0800 Subject: [PATCH 30/30] docs: Fix missing docs --- src/aggregator/src/Shared/RateAggregator.lua | 2 +- src/symbol/src/Shared/Symbol.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/aggregator/src/Shared/RateAggregator.lua b/src/aggregator/src/Shared/RateAggregator.lua index 9c4979aea8..486aba6d7d 100644 --- a/src/aggregator/src/Shared/RateAggregator.lua +++ b/src/aggregator/src/Shared/RateAggregator.lua @@ -38,7 +38,7 @@ end --[=[ Observes the aggregated data - @param id number + @param ... any @return Observable ]=] function RateAggregator:Promise(...) diff --git a/src/symbol/src/Shared/Symbol.lua b/src/symbol/src/Shared/Symbol.lua index d6387eb162..d26705ce51 100644 --- a/src/symbol/src/Shared/Symbol.lua +++ b/src/symbol/src/Shared/Symbol.lua @@ -34,6 +34,7 @@ end --[=[ Returns true if a symbol + @param value boolean @return boolean ]=] function Symbol.isSymbol(value)