@quenty/gameproductservice 5.15.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +4 -2
- package/src/Client/GameProductServiceClient.lua +136 -114
- package/src/Client/Manager/PlayerProductManagerClient.lua +49 -54
- package/src/Server/GameProductService.lua +93 -6
- package/src/Server/Manager/PlayerProductManager.lua +50 -100
- package/src/Shared/Helpers/GameProductServiceHelper.lua +165 -0
- package/src/Shared/Manager/PlayerProductManagerConstants.lua +1 -2
- package/src/Shared/Ownership/PlayerAssetOwnershipTracker.lua +296 -0
- package/src/Shared/Ownership/PlayerAssetOwnershipUtils.lua +108 -0
- package/src/Shared/Ownership/WellKnownAssetOwnershipHandler.lua +66 -0
- package/src/Shared/Trackers/PlayerAssetMarketTracker.lua +173 -0
- package/src/Shared/Trackers/PlayerMarketeer.lua +147 -0
- package/test/scripts/Client/ClientMain.client.lua +8 -1
- package/test/scripts/Server/ServerMain.server.lua +36 -16
- package/src/Client/GameProductServiceUtilsClient.lua +0 -85
- package/src/Shared/GameProductServiceBase.lua +0 -149
- package/src/Shared/Manager/PlayerProductManagerBase.lua +0 -180
- package/src/Shared/Manager/PlayerProductManagerUtils.lua +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [6.0.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@5.15.0...@quenty/gameproductservice@6.0.0) (2023-02-27)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @quenty/gameproductservice
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# [5.15.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@5.14.0...@quenty/gameproductservice@5.15.0) (2023-02-27)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @quenty/gameproductservice
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/gameproductservice",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Generalized monetization system for handling products and purchases correctly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@quenty/loader": "^6.1.0",
|
|
35
35
|
"@quenty/maid": "^2.4.0",
|
|
36
36
|
"@quenty/marketplaceutils": "^6.2.0",
|
|
37
|
+
"@quenty/observablecollection": "^5.8.0",
|
|
37
38
|
"@quenty/playerbinder": "^8.8.0",
|
|
38
39
|
"@quenty/promise": "^6.2.0",
|
|
39
40
|
"@quenty/remoting": "^6.2.0",
|
|
@@ -42,7 +43,8 @@
|
|
|
42
43
|
"@quenty/servicebag": "^6.5.0",
|
|
43
44
|
"@quenty/signal": "^2.3.0",
|
|
44
45
|
"@quenty/statestack": "^8.6.0",
|
|
46
|
+
"@quenty/string": "^3.1.0",
|
|
45
47
|
"@quenty/table": "^3.2.0"
|
|
46
48
|
},
|
|
47
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "ef2767286911d37d3584dd21efee66b88061cb6e"
|
|
48
50
|
}
|
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
--[=[
|
|
2
|
+
This service provides an interface to purchase produces, assets, and other
|
|
3
|
+
marketplace items. This listens to events, handles requests between server and
|
|
4
|
+
client, and takes in both assetKeys from GameConfigService, as well as
|
|
5
|
+
assetIds.
|
|
6
|
+
|
|
7
|
+
See [GameProductService] for the server equivalent. The API surface should be
|
|
8
|
+
effectively the same between the two.
|
|
9
|
+
|
|
10
|
+
@client
|
|
2
11
|
@class GameProductServiceClient
|
|
3
12
|
]=]
|
|
4
13
|
|
|
5
14
|
local require = require(script.Parent.loader).load(script)
|
|
6
15
|
|
|
7
16
|
local Players = game:GetService("Players")
|
|
8
|
-
local MarketplaceService = game:GetService("MarketplaceService")
|
|
9
17
|
|
|
10
|
-
local Maid = require("Maid")
|
|
11
|
-
local GameProductServiceBase = require("GameProductServiceBase")
|
|
12
|
-
local Signal = require("Signal")
|
|
13
18
|
local GameConfigAssetTypes = require("GameConfigAssetTypes")
|
|
19
|
+
local Maid = require("Maid")
|
|
14
20
|
local Promise = require("Promise")
|
|
21
|
+
local RxBinderUtils = require("RxBinderUtils")
|
|
22
|
+
local Signal = require("Signal")
|
|
23
|
+
local GameProductServiceHelper = require("GameProductServiceHelper")
|
|
24
|
+
local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils")
|
|
15
25
|
|
|
16
|
-
local GameProductServiceClient =
|
|
26
|
+
local GameProductServiceClient = {}
|
|
27
|
+
GameProductServiceClient.ServiceName = "GameProductServiceClient"
|
|
17
28
|
|
|
29
|
+
--[=[
|
|
30
|
+
Initializes the service. Should be done via [ServiceBag]
|
|
31
|
+
@param serviceBag ServiceBag
|
|
32
|
+
]=]
|
|
18
33
|
function GameProductServiceClient:Init(serviceBag)
|
|
19
34
|
assert(not self._serviceBag, "Already initialized")
|
|
20
35
|
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
@@ -26,151 +41,158 @@ function GameProductServiceClient:Init(serviceBag)
|
|
|
26
41
|
-- Internal
|
|
27
42
|
self._binders = self._serviceBag:GetService(require("GameProductBindersClient"))
|
|
28
43
|
|
|
29
|
-
self.
|
|
30
|
-
self._maid:GiveTask(self.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self._purchasedGamePassesThisSession[gamepassId] = true
|
|
45
|
-
-- self._fireworksService:Create(3)
|
|
46
|
-
self.GamepassPurchased:Fire(gamepassId)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end))
|
|
50
|
-
|
|
51
|
-
self._purchasedDevProductsThisSession = {}
|
|
52
|
-
self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished
|
|
53
|
-
:Connect(function(userId, productId, wasPurchased)
|
|
54
|
-
if userId == Players.LocalPlayer.UserId then
|
|
55
|
-
self._promptClosedEvent:Fire()
|
|
56
|
-
if wasPurchased then
|
|
57
|
-
-- self._fireworksService:Create(3)
|
|
58
|
-
self._purchasedDevProductsThisSession[productId] = true
|
|
59
|
-
self.DevProductPurchased:Fire(productId)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end))
|
|
44
|
+
self._helper = GameProductServiceHelper.new(self._binders.PlayerProductManager)
|
|
45
|
+
self._maid:GiveTask(self._helper)
|
|
46
|
+
|
|
47
|
+
-- Additional API for ergonomics
|
|
48
|
+
self.GamePassPurchased = Signal.new() -- :Fire(gamePassId)
|
|
49
|
+
self._maid:GiveTask(self.GamePassPurchased)
|
|
50
|
+
|
|
51
|
+
self.ProductPurchased = Signal.new() -- :Fire(productId)
|
|
52
|
+
self._maid:GiveTask(self.ProductPurchased)
|
|
53
|
+
|
|
54
|
+
self.AssetPurchased = Signal.new() -- :Fire(assetId)
|
|
55
|
+
self._maid:GiveTask(self.AssetPurchased)
|
|
56
|
+
|
|
57
|
+
self.BundlePurchased = Signal.new() -- :Fire(bundleId)
|
|
58
|
+
self._maid:GiveTask(self.BundlePurchased)
|
|
63
59
|
end
|
|
64
60
|
|
|
65
61
|
--[=[
|
|
66
|
-
|
|
67
|
-
@param passIdOrKey string | number
|
|
68
|
-
@return Promise<boolean>
|
|
62
|
+
Starts the service. Should be done via [ServiceBag]
|
|
69
63
|
]=]
|
|
70
|
-
function GameProductServiceClient:
|
|
71
|
-
|
|
64
|
+
function GameProductServiceClient:Start()
|
|
65
|
+
self._maid:GiveTask(RxBinderUtils.observeBoundClassBrio(self._binders.PlayerProductManager, Players.LocalPlayer):Subscribe(function(brio)
|
|
66
|
+
if brio:IsDead() then
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
local maid = brio:ToMaid()
|
|
71
|
+
local playerProductManager = brio:GetValue()
|
|
72
|
+
local playerMrketeer = playerProductManager:GetMarketeer()
|
|
73
|
+
|
|
74
|
+
local function exposeSignal(signal, assetType)
|
|
75
|
+
maid:GiveTask(playerMrketeer:GetAssetTrackerOrError(assetType).Purchased:Connect(function(...)
|
|
76
|
+
signal:Fire(...)
|
|
77
|
+
end))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
exposeSignal(self.GamePassPurchased, GameConfigAssetTypes.PASS)
|
|
81
|
+
exposeSignal(self.ProductPurchased, GameConfigAssetTypes.PRODUCT)
|
|
82
|
+
exposeSignal(self.AssetPurchased, GameConfigAssetTypes.ASSET)
|
|
83
|
+
exposeSignal(self.BundlePurchased, GameConfigAssetTypes.BUNDLE)
|
|
84
|
+
end))
|
|
85
|
+
end
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
76
|
-
end
|
|
87
|
+
--[=[
|
|
88
|
+
Returns true if item has been purchased this session
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
@param player Player
|
|
91
|
+
@param assetType GameConfigAssetType
|
|
92
|
+
@param idOrKey string | number
|
|
93
|
+
@return boolean
|
|
94
|
+
]=]
|
|
95
|
+
function GameProductServiceClient:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
|
|
96
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
97
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
98
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
81
99
|
|
|
82
|
-
return self:
|
|
100
|
+
return self._helper:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
|
|
83
101
|
end
|
|
84
102
|
|
|
85
103
|
--[=[
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@
|
|
104
|
+
Prompts the user to purchase the asset, and returns true if purchased
|
|
105
|
+
|
|
106
|
+
@param player Player
|
|
107
|
+
@param assetType GameConfigAssetType
|
|
108
|
+
@param idOrKey string | number
|
|
109
|
+
@return Promise<boolean>
|
|
89
110
|
]=]
|
|
90
|
-
function
|
|
91
|
-
assert(
|
|
111
|
+
function GameProductServiceClient:PromisePromptPurchase(player, assetType, idOrKey)
|
|
112
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
113
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
114
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
92
115
|
|
|
93
|
-
return self:ObservePlayerOwnsPass(Players.LocalPlayer, passIdOrKey)
|
|
94
|
-
end
|
|
95
116
|
|
|
96
|
-
|
|
97
|
-
return self._binders.PlayerProductManager
|
|
117
|
+
return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
|
|
98
118
|
end
|
|
99
119
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
assert(self._serviceBag, "Not initialized")
|
|
120
|
+
--[=[
|
|
121
|
+
Returns true if item has been purchased this session
|
|
103
122
|
|
|
104
|
-
|
|
123
|
+
@param player Player
|
|
124
|
+
@param assetType GameConfigAssetType
|
|
125
|
+
@param idOrKey string | number
|
|
126
|
+
@return Promise<boolean>
|
|
127
|
+
]=]
|
|
128
|
+
function GameProductServiceClient:PromisePlayerOwnership(player, assetType, idOrKey)
|
|
129
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
130
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
131
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
132
|
+
|
|
133
|
+
return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
|
|
105
134
|
end
|
|
106
135
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
assert(self._serviceBag, "Not initialized")
|
|
136
|
+
--[=[
|
|
137
|
+
Observes if the player owns this cloud asset or not
|
|
110
138
|
|
|
111
|
-
|
|
139
|
+
@param player Player
|
|
140
|
+
@param assetType GameConfigAssetType
|
|
141
|
+
@param idOrKey string | number
|
|
142
|
+
@return Observable<boolean>
|
|
143
|
+
]=]
|
|
144
|
+
function GameProductServiceClient:ObservePlayerOwnership(player, assetType, idOrKey)
|
|
145
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
146
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
147
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
148
|
+
|
|
149
|
+
return self._helper:ObservePlayerOwnership(player, assetType, idOrKey)
|
|
112
150
|
end
|
|
113
151
|
|
|
114
|
-
|
|
152
|
+
--[=[
|
|
153
|
+
Flags the propmt is open
|
|
154
|
+
]=]
|
|
155
|
+
function GameProductServiceClient:FlagPromptOpen()
|
|
115
156
|
assert(self ~= GameProductServiceClient, "Use serviceBag")
|
|
116
157
|
assert(self._serviceBag, "Not initialized")
|
|
117
|
-
assert(type(productIdOrKey) == "number" or type(productIdOrKey) == "string", "productIdOrKey")
|
|
118
|
-
|
|
119
|
-
local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
120
|
-
if not productId then
|
|
121
|
-
warn(("No asset with key %q"):format(tostring(productIdOrKey)))
|
|
122
|
-
return false
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
if self._purchasedDevProductsThisSession[productIdOrKey] then
|
|
126
|
-
return true
|
|
127
|
-
end
|
|
128
158
|
|
|
129
|
-
|
|
159
|
+
self._promptOpenFlag = true
|
|
130
160
|
end
|
|
131
161
|
|
|
132
|
-
function GameProductServiceClient:PromisePurchasedOrPrompt(passIdOrKey)
|
|
133
|
-
local gamepassId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
134
|
-
if not gamepassId then
|
|
135
|
-
return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
136
|
-
end
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
--[=[
|
|
164
|
+
Returns true if the prompt is open
|
|
165
|
+
@return boolean
|
|
166
|
+
]=]
|
|
167
|
+
function GameProductServiceClient:GuessIfPromptOpenFromFlags()
|
|
168
|
+
assert(self ~= GameProductServiceClient, "Use serviceBag")
|
|
169
|
+
assert(self._serviceBag, "Not initialized")
|
|
143
170
|
|
|
144
|
-
|
|
145
|
-
end)
|
|
171
|
+
return self._promptOpenFlag
|
|
146
172
|
end
|
|
147
173
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
assert(productIdOrKey, "Bad productIdOrKey")
|
|
151
|
-
|
|
152
|
-
local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
153
|
-
if not productId then
|
|
154
|
-
return Promise.rejected(("No asset with key %q"):format(tostring(productIdOrKey)))
|
|
155
|
-
end
|
|
174
|
+
--[=[
|
|
175
|
+
Promises to either check a gamepass or a product to see if it's purchased.
|
|
156
176
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
@param gamePassIdOrKey string | number
|
|
178
|
+
@param productIdOrKey string | number
|
|
179
|
+
@return boolean
|
|
180
|
+
]=]
|
|
181
|
+
function GameProductServiceClient:PromiseGamePassOrProductUnlockOrPrompt(gamePassIdOrKey, productIdOrKey)
|
|
182
|
+
assert(type(gamePassIdOrKey) == "number" or type(gamePassIdOrKey) == "string", "Bad gamePassIdOrKey")
|
|
183
|
+
assert(type(productIdOrKey) == "number" or type(productIdOrKey) == "string", "Bad productIdOrKey")
|
|
161
184
|
|
|
162
|
-
if self:
|
|
185
|
+
if self:HasPurchasedThisSession(Players.LocalPlayer, GameConfigAssetTypes.PRODUCT, productIdOrKey) then
|
|
163
186
|
return Promise.resolved(true)
|
|
164
187
|
end
|
|
165
188
|
|
|
166
|
-
return self:
|
|
167
|
-
:Then(function(
|
|
168
|
-
if
|
|
169
|
-
return
|
|
189
|
+
return self:PromisePlayerOwnership(Players.LocalPlayer, GameConfigAssetTypes.PASS, productIdOrKey)
|
|
190
|
+
:Then(function(owns)
|
|
191
|
+
if owns then
|
|
192
|
+
return true
|
|
193
|
+
else
|
|
194
|
+
return self:PromisePromptPurchase(Players.LocalPlayer, GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
170
195
|
end
|
|
171
|
-
|
|
172
|
-
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, productId)
|
|
173
|
-
return owned
|
|
174
196
|
end)
|
|
175
197
|
end
|
|
176
198
|
|
|
@@ -8,102 +8,97 @@ local require = require(script.Parent.loader).load(script)
|
|
|
8
8
|
local MarketplaceService = game:GetService("MarketplaceService")
|
|
9
9
|
local Players = game:GetService("Players")
|
|
10
10
|
|
|
11
|
-
local
|
|
11
|
+
local BaseObject = require("BaseObject")
|
|
12
12
|
local PlayerProductManagerConstants = require("PlayerProductManagerConstants")
|
|
13
13
|
local GameConfigServiceClient = require("GameConfigServiceClient")
|
|
14
|
-
local
|
|
14
|
+
local GameConfigAssetTypes = require("GameConfigAssetTypes")
|
|
15
|
+
local PlayerMarketeer = require("PlayerMarketeer")
|
|
15
16
|
|
|
16
|
-
local PlayerProductManagerClient = setmetatable({},
|
|
17
|
+
local PlayerProductManagerClient = setmetatable({}, BaseObject)
|
|
17
18
|
PlayerProductManagerClient.ClassName = "PlayerProductManagerClient"
|
|
18
19
|
PlayerProductManagerClient.__index = PlayerProductManagerClient
|
|
19
20
|
|
|
20
21
|
require("PromiseRemoteEventMixin"):Add(PlayerProductManagerClient, PlayerProductManagerConstants.REMOTE_EVENT_NAME)
|
|
21
22
|
|
|
22
23
|
function PlayerProductManagerClient.new(obj, serviceBag)
|
|
23
|
-
local self = setmetatable(
|
|
24
|
+
local self = setmetatable(BaseObject.new(obj), PlayerProductManagerClient)
|
|
24
25
|
|
|
25
26
|
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
26
27
|
self._gameConfigServiceClient = self._serviceBag:GetService(GameConfigServiceClient)
|
|
27
28
|
|
|
28
|
-
|
|
29
29
|
if self._obj == Players.LocalPlayer then
|
|
30
|
-
self.
|
|
30
|
+
self._marketeer = PlayerMarketeer.new(self._obj, self._gameConfigServiceClient:GetConfigPicker())
|
|
31
|
+
self._maid:GiveTask(self._marketeer)
|
|
31
32
|
|
|
32
|
-
self:
|
|
33
|
-
self:_setupRemoteEventLocal(remoteEvent)
|
|
34
|
-
end)
|
|
33
|
+
self:_connectMarketplace()
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
-- Configure remote events
|
|
36
|
+
self:_replicateRemoteEventType(GameConfigAssetTypes.ASSET)
|
|
37
|
+
self:_replicateRemoteEventType(GameConfigAssetTypes.BUNDLE)
|
|
38
|
+
self:_replicateRemoteEventType(GameConfigAssetTypes.PASS)
|
|
39
|
+
self:_replicateRemoteEventType(GameConfigAssetTypes.PRODUCT)
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
return self
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
45
|
+
--[=[
|
|
46
|
+
@return PlayerMarketeer
|
|
47
|
+
]=]
|
|
48
|
+
function PlayerProductManagerClient:GetMarketeer()
|
|
49
|
+
return self._marketeer
|
|
50
|
+
end
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
local
|
|
52
|
+
function PlayerProductManagerClient:_replicateRemoteEventType(assetType)
|
|
53
|
+
local tracker = self._marketeer:GetAssetTrackerOrError(assetType)
|
|
56
54
|
|
|
57
|
-
self.
|
|
55
|
+
self._maid:GiveTask(tracker.PromptFinished:Connect(function(assetId, isPurchased)
|
|
56
|
+
self:PromiseRemoteEvent():Then(function(remoteEvent)
|
|
57
|
+
remoteEvent:FireServer(PlayerProductManagerConstants.NOTIFY_PROMPT_FINISHED, assetType, assetId, isPurchased)
|
|
58
|
+
end)
|
|
59
|
+
end))
|
|
60
|
+
end
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
--[=[
|
|
63
|
+
Gets the current player
|
|
64
|
+
@return Player
|
|
65
|
+
]=]
|
|
66
|
+
function PlayerProductManagerClient:GetPlayer()
|
|
67
|
+
return self._obj
|
|
60
68
|
end
|
|
61
69
|
|
|
62
|
-
function PlayerProductManagerClient:
|
|
63
|
-
--
|
|
70
|
+
function PlayerProductManagerClient:_connectMarketplace()
|
|
71
|
+
-- Assets
|
|
64
72
|
self._maid:GiveTask(MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
|
|
65
73
|
if player == self._obj then
|
|
66
|
-
|
|
74
|
+
local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.ASSET)
|
|
75
|
+
tracker:HandlePurchaseEvent(assetId, isPurchased)
|
|
67
76
|
end
|
|
68
77
|
end))
|
|
69
78
|
|
|
70
79
|
-- Products
|
|
71
|
-
self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId,
|
|
80
|
+
self._maid:GiveTask(MarketplaceService.PromptProductPurchaseFinished:Connect(function(userId, productId, isPurchased)
|
|
72
81
|
if self._obj.UserId == userId then
|
|
73
|
-
|
|
82
|
+
local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PRODUCT)
|
|
83
|
+
tracker:HandlePurchaseEvent(productId, isPurchased)
|
|
74
84
|
end
|
|
75
85
|
end))
|
|
76
86
|
|
|
77
|
-
--
|
|
87
|
+
-- Game passes
|
|
78
88
|
self._maid:GiveTask(MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, isPurchased)
|
|
79
89
|
if player == self._obj then
|
|
80
|
-
|
|
90
|
+
local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.PASS)
|
|
91
|
+
tracker:HandlePurchaseEvent(gamePassId, isPurchased)
|
|
81
92
|
end
|
|
82
93
|
end))
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
-- For overrides
|
|
86
|
-
function PlayerProductManagerClient:GetConfigPicker()
|
|
87
|
-
return self._gameConfigServiceClient:GetConfigPicker()
|
|
88
|
-
end
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
local promise = self._pendingPassPromises[gamePassId]
|
|
96
|
-
if promise then
|
|
97
|
-
if isPurchased then
|
|
98
|
-
-- TODO: verify this on the server here
|
|
99
|
-
-- Can we break cache?
|
|
100
|
-
self:SetPlayerOwnsPass(gamePassId, true)
|
|
95
|
+
-- Bundles
|
|
96
|
+
self._maid:GiveTask(MarketplaceService.PromptBundlePurchaseFinished:Connect(function(player, bundleId, isPurchased)
|
|
97
|
+
if player == self._obj then
|
|
98
|
+
local tracker = self._marketeer:GetAssetTrackerOrError(GameConfigAssetTypes.BUNDLE)
|
|
99
|
+
tracker:HandlePurchaseEvent(bundleId, isPurchased)
|
|
101
100
|
end
|
|
102
|
-
|
|
103
|
-
self._pendingPassPromises[gamePassId] = nil
|
|
104
|
-
promise:Resolve(isPurchased)
|
|
105
|
-
end
|
|
101
|
+
end))
|
|
106
102
|
end
|
|
107
103
|
|
|
108
|
-
|
|
109
104
|
return PlayerProductManagerClient
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
--[=[
|
|
2
|
+
This service provides an interface to purchase produces, assets, and other
|
|
3
|
+
marketplace items. This listens to events, handles requests between server and
|
|
4
|
+
client, and takes in both assetKeys from GameConfigService, as well as
|
|
5
|
+
assetIds.
|
|
6
|
+
|
|
7
|
+
See [GameProductServiceClient] for the client equivalent. The API surface should be
|
|
8
|
+
effectively the same between the two.
|
|
9
|
+
|
|
10
|
+
@server
|
|
2
11
|
@class GameProductService
|
|
3
12
|
]=]
|
|
4
13
|
|
|
@@ -7,11 +16,17 @@ local require = require(script.Parent.loader).load(script)
|
|
|
7
16
|
local Players = game:GetService("Players")
|
|
8
17
|
local MarketplaceService = game:GetService("MarketplaceService")
|
|
9
18
|
|
|
10
|
-
local GameProductServiceBase = require("GameProductServiceBase")
|
|
11
19
|
local Maid = require("Maid")
|
|
20
|
+
local GameProductServiceHelper = require("GameProductServiceHelper")
|
|
21
|
+
local GameConfigAssetTypeUtils = require("GameConfigAssetTypeUtils")
|
|
12
22
|
|
|
13
|
-
local GameProductService =
|
|
23
|
+
local GameProductService = {}
|
|
24
|
+
GameProductService.ServiceName = "GameProductService"
|
|
14
25
|
|
|
26
|
+
--[=[
|
|
27
|
+
Initializes the service. Should be done via [ServiceBag]
|
|
28
|
+
@param serviceBag ServiceBag
|
|
29
|
+
]=]
|
|
15
30
|
function GameProductService:Init(serviceBag)
|
|
16
31
|
assert(not self._serviceBag, "Already initialized")
|
|
17
32
|
|
|
@@ -23,21 +38,93 @@ function GameProductService:Init(serviceBag)
|
|
|
23
38
|
|
|
24
39
|
-- Internal
|
|
25
40
|
self._binders = self._serviceBag:GetService(require("GameProductBindersServer"))
|
|
41
|
+
|
|
42
|
+
-- Configure
|
|
43
|
+
self._helper = GameProductServiceHelper.new(self._binders.PlayerProductManager)
|
|
44
|
+
self._maid:GiveTask(self._helper)
|
|
26
45
|
end
|
|
27
46
|
|
|
47
|
+
--[=[
|
|
48
|
+
Starts the service. Should be done via [ServiceBag]
|
|
49
|
+
]=]
|
|
28
50
|
function GameProductService:Start()
|
|
51
|
+
-- TODO: Avoid binding this unless explicitly asked to
|
|
52
|
+
-- TODO: Provide receipt processing API surface
|
|
53
|
+
|
|
29
54
|
MarketplaceService.ProcessReceipt = function(...)
|
|
30
55
|
return self:_processReceipt(...)
|
|
31
56
|
end
|
|
32
57
|
|
|
33
58
|
self._maid:GiveTask(function()
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
task.spawn(function()
|
|
60
|
+
-- This might be unsafe
|
|
61
|
+
MarketplaceService.ProcessReceipt = nil
|
|
62
|
+
end)
|
|
36
63
|
end)
|
|
37
64
|
end
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
--[=[
|
|
67
|
+
Returns true if item has been purchased this session
|
|
68
|
+
|
|
69
|
+
@param player Player
|
|
70
|
+
@param assetType GameConfigAssetType
|
|
71
|
+
@param idOrKey string | number
|
|
72
|
+
@return boolean
|
|
73
|
+
]=]
|
|
74
|
+
function GameProductService:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
|
|
75
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
76
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
77
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
78
|
+
|
|
79
|
+
return self._helper:HasPlayerPurchasedThisSession(player, assetType, idOrKey)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
--[=[
|
|
83
|
+
Prompts the user to purchase the asset, and returns true if purchased
|
|
84
|
+
|
|
85
|
+
@param player Player
|
|
86
|
+
@param assetType GameConfigAssetType
|
|
87
|
+
@param idOrKey string | number
|
|
88
|
+
@return boolean
|
|
89
|
+
]=]
|
|
90
|
+
function GameProductService:PromisePlayerPromptPurchase(player, assetType, idOrKey)
|
|
91
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
92
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
93
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
94
|
+
|
|
95
|
+
return self._helper:PromisePromptPurchase(player, assetType, idOrKey)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
--[=[
|
|
99
|
+
Returns true if item has been purchased this session
|
|
100
|
+
|
|
101
|
+
@param player Player
|
|
102
|
+
@param assetType GameConfigAssetType
|
|
103
|
+
@param idOrKey string | number
|
|
104
|
+
@return Promise<boolean>
|
|
105
|
+
]=]
|
|
106
|
+
function GameProductService:PromisePlayerOwnership(player, assetType, idOrKey)
|
|
107
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
108
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
109
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
110
|
+
|
|
111
|
+
return self._helper:PromisePlayerOwnership(player, assetType, idOrKey)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
--[=[
|
|
115
|
+
Observes if the player owns this cloud asset or not
|
|
116
|
+
|
|
117
|
+
@param player Player
|
|
118
|
+
@param assetType GameConfigAssetType
|
|
119
|
+
@param idOrKey string | number
|
|
120
|
+
@return Observable<boolean>
|
|
121
|
+
]=]
|
|
122
|
+
function GameProductService:ObservePlayerOwnership(player, assetType, idOrKey)
|
|
123
|
+
assert(typeof(player) == "Instance" and player:IsA("Player"), "Bad player")
|
|
124
|
+
assert(GameConfigAssetTypeUtils.isAssetType(assetType), "Bad assetType")
|
|
125
|
+
assert(type(idOrKey) == "number" or type(idOrKey) == "string", "Bad idOrKey")
|
|
126
|
+
|
|
127
|
+
return self._helper:ObservePlayerOwnership(player, assetType, idOrKey)
|
|
41
128
|
end
|
|
42
129
|
|
|
43
130
|
function GameProductService:_processReceipt(receiptInfo)
|