@quenty/gameproductservice 3.3.1 → 3.4.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 +11 -0
- package/package.json +15 -15
- package/src/Client/GameProductServiceClient.lua +5 -47
- package/src/Client/Manager/PlayerProductManagerClient.lua +45 -0
- package/src/Server/Manager/PlayerProductManager.lua +2 -2
- package/src/Shared/GameProductServiceBase.lua +76 -11
- package/src/Shared/Manager/PlayerProductManagerUtils.lua +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
# [3.4.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@3.3.1...@quenty/gameproductservice@3.4.0) (2022-07-31)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* API calls in GameProductService support passids or string keys to retrieve the passes. ([083170e](https://github.com/Quenty/NevermoreEngine/commit/083170e92bf2e71628edd17bf526d4b0936fe1a9))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [3.3.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/gameproductservice@3.3.0...@quenty/gameproductservice@3.3.1) (2022-07-19)
|
|
7
18
|
|
|
8
19
|
**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.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Generalized monetization system for handling products and purchases correctly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -27,22 +27,22 @@
|
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@quenty/attributeutils": "^6.
|
|
31
|
-
"@quenty/baseobject": "^5.
|
|
32
|
-
"@quenty/binder": "^6.
|
|
33
|
-
"@quenty/gameconfig": "^3.
|
|
30
|
+
"@quenty/attributeutils": "^6.2.0",
|
|
31
|
+
"@quenty/baseobject": "^5.1.0",
|
|
32
|
+
"@quenty/binder": "^6.3.0",
|
|
33
|
+
"@quenty/gameconfig": "^3.4.0",
|
|
34
34
|
"@quenty/loader": "^5.0.0",
|
|
35
|
-
"@quenty/maid": "^2.
|
|
36
|
-
"@quenty/marketplaceutils": "^5.
|
|
37
|
-
"@quenty/playerbinder": "^6.
|
|
38
|
-
"@quenty/promise": "^5.
|
|
39
|
-
"@quenty/remoting": "^5.
|
|
40
|
-
"@quenty/rx": "^5.
|
|
41
|
-
"@quenty/rxbinderutils": "^6.
|
|
42
|
-
"@quenty/servicebag": "^5.
|
|
35
|
+
"@quenty/maid": "^2.4.0",
|
|
36
|
+
"@quenty/marketplaceutils": "^5.1.0",
|
|
37
|
+
"@quenty/playerbinder": "^6.3.0",
|
|
38
|
+
"@quenty/promise": "^5.1.0",
|
|
39
|
+
"@quenty/remoting": "^5.2.0",
|
|
40
|
+
"@quenty/rx": "^5.2.0",
|
|
41
|
+
"@quenty/rxbinderutils": "^6.3.0",
|
|
42
|
+
"@quenty/servicebag": "^5.1.0",
|
|
43
43
|
"@quenty/signal": "^2.2.0",
|
|
44
|
-
"@quenty/statestack": "^6.
|
|
44
|
+
"@quenty/statestack": "^6.2.0",
|
|
45
45
|
"@quenty/table": "^3.1.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "e31b3a35aa475bb5699a24898a8639e107165b36"
|
|
48
48
|
}
|
|
@@ -12,8 +12,6 @@ local GameProductServiceBase = require("GameProductServiceBase")
|
|
|
12
12
|
local Signal = require("Signal")
|
|
13
13
|
local GameConfigAssetTypes = require("GameConfigAssetTypes")
|
|
14
14
|
local Promise = require("Promise")
|
|
15
|
-
local RxStateStackUtils = require("RxStateStackUtils")
|
|
16
|
-
local Rx = require("Rx")
|
|
17
15
|
|
|
18
16
|
local GameProductServiceClient = GameProductServiceBase.new()
|
|
19
17
|
|
|
@@ -72,7 +70,7 @@ end
|
|
|
72
70
|
function GameProductServiceClient:PromiseLocalPlayerOwnsPass(passIdOrKey)
|
|
73
71
|
assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
|
|
74
72
|
|
|
75
|
-
local passId = self:
|
|
73
|
+
local passId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
76
74
|
if not passId then
|
|
77
75
|
return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
78
76
|
end
|
|
@@ -92,29 +90,6 @@ end
|
|
|
92
90
|
function GameProductServiceBase:ObserveLocalPlayerOwnsPass(passIdOrKey)
|
|
93
91
|
assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
|
|
94
92
|
|
|
95
|
-
if type(passIdOrKey) == "string" then
|
|
96
|
-
local picker = self._gameConfigService:GetConfigPicker()
|
|
97
|
-
return picker:ObserveActiveAssetOfAssetIdBrio()
|
|
98
|
-
:Pipe({
|
|
99
|
-
RxStateStackUtils.topOfStack();
|
|
100
|
-
Rx.switchMap(function(asset)
|
|
101
|
-
if asset then
|
|
102
|
-
return asset:ObserveAssetId()
|
|
103
|
-
else
|
|
104
|
-
return Rx.of(nil)
|
|
105
|
-
end
|
|
106
|
-
end);
|
|
107
|
-
Rx.switchMap(function(assetId)
|
|
108
|
-
if assetId then
|
|
109
|
-
return self:ObservePlayerOwnsPass(Players.LocalPlayer, passIdOrKey)
|
|
110
|
-
else
|
|
111
|
-
warn(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
112
|
-
return Rx.of(false)
|
|
113
|
-
end
|
|
114
|
-
end)
|
|
115
|
-
})
|
|
116
|
-
end
|
|
117
|
-
|
|
118
93
|
return self:ObservePlayerOwnsPass(Players.LocalPlayer, passIdOrKey)
|
|
119
94
|
end
|
|
120
95
|
|
|
@@ -141,7 +116,7 @@ function GameProductServiceClient:HasPurchasedProductThisSession(productIdOrKey)
|
|
|
141
116
|
assert(self._serviceBag, "Not initialized")
|
|
142
117
|
assert(type(productIdOrKey) == "number" or type(productIdOrKey) == "string", "productIdOrKey")
|
|
143
118
|
|
|
144
|
-
local productId = self:
|
|
119
|
+
local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
145
120
|
if not productId then
|
|
146
121
|
warn(("No asset with key %q"):format(tostring(productIdOrKey)))
|
|
147
122
|
return false
|
|
@@ -155,7 +130,7 @@ function GameProductServiceClient:HasPurchasedProductThisSession(productIdOrKey)
|
|
|
155
130
|
end
|
|
156
131
|
|
|
157
132
|
function GameProductServiceClient:PromisePurchasedOrPrompt(passIdOrKey)
|
|
158
|
-
local gamepassId = self:
|
|
133
|
+
local gamepassId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
159
134
|
if not gamepassId then
|
|
160
135
|
return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
161
136
|
end
|
|
@@ -174,12 +149,12 @@ function GameProductServiceClient:PromiseGamepassOrProductUnlockOrPrompt(passIdO
|
|
|
174
149
|
assert(passIdOrKey, "Bad passIdOrKey")
|
|
175
150
|
assert(productIdOrKey, "Bad productIdOrKey")
|
|
176
151
|
|
|
177
|
-
local productId = self:
|
|
152
|
+
local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
178
153
|
if not productId then
|
|
179
154
|
return Promise.rejected(("No asset with key %q"):format(tostring(productIdOrKey)))
|
|
180
155
|
end
|
|
181
156
|
|
|
182
|
-
local gamepassId = self:
|
|
157
|
+
local gamepassId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
183
158
|
if not gamepassId then
|
|
184
159
|
return Promise.rejected(("No asset with key %q"):format(tostring(passIdOrKey)))
|
|
185
160
|
end
|
|
@@ -199,21 +174,4 @@ function GameProductServiceClient:PromiseGamepassOrProductUnlockOrPrompt(passIdO
|
|
|
199
174
|
end)
|
|
200
175
|
end
|
|
201
176
|
|
|
202
|
-
|
|
203
|
-
function GameProductServiceClient:_toAssetId(assetType, assetIdOrKey)
|
|
204
|
-
assert(type(assetIdOrKey) == "number" or type(assetIdOrKey) == "string", "Bad assetIdOrKey")
|
|
205
|
-
|
|
206
|
-
if type(assetIdOrKey) == "string" then
|
|
207
|
-
local picker = self._gameConfigService:GetConfigPicker()
|
|
208
|
-
local asset = picker:FindFirstActiveAssetOfKey(assetType, assetIdOrKey)
|
|
209
|
-
if asset then
|
|
210
|
-
return asset:GetAssetId()
|
|
211
|
-
else
|
|
212
|
-
return nil
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
return assetIdOrKey
|
|
217
|
-
end
|
|
218
|
-
|
|
219
177
|
return GameProductServiceClient
|
|
@@ -11,6 +11,7 @@ local Players = game:GetService("Players")
|
|
|
11
11
|
local PlayerProductManagerBase = require("PlayerProductManagerBase")
|
|
12
12
|
local PlayerProductManagerConstants = require("PlayerProductManagerConstants")
|
|
13
13
|
local GameConfigServiceClient = require("GameConfigServiceClient")
|
|
14
|
+
local Promise = require("Promise")
|
|
14
15
|
|
|
15
16
|
local PlayerProductManagerClient = setmetatable({}, PlayerProductManagerBase)
|
|
16
17
|
PlayerProductManagerClient.ClassName = "PlayerProductManagerClient"
|
|
@@ -24,15 +25,40 @@ function PlayerProductManagerClient.new(obj, serviceBag)
|
|
|
24
25
|
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
25
26
|
self._gameConfigServiceClient = self._serviceBag:GetService(GameConfigServiceClient)
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
if self._obj == Players.LocalPlayer then
|
|
30
|
+
self._pendingPassPromises = {}
|
|
31
|
+
|
|
28
32
|
self:PromiseRemoteEvent():Then(function(remoteEvent)
|
|
29
33
|
self:_setupRemoteEventLocal(remoteEvent)
|
|
30
34
|
end)
|
|
35
|
+
|
|
36
|
+
self._maid:GiveTask(MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamePassId, isPurchased)
|
|
37
|
+
if player == self._obj then
|
|
38
|
+
self:_handlePassFinished(player, gamePassId, isPurchased)
|
|
39
|
+
end
|
|
40
|
+
end))
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
return self
|
|
34
44
|
end
|
|
35
45
|
|
|
46
|
+
function PlayerProductManagerClient:PromptGamePassPurchase(gamePassId)
|
|
47
|
+
assert(type(gamePassId) == "number", "Bad gamePassId")
|
|
48
|
+
assert(self._obj == Players.LocalPlayer, "Can only prompt local player")
|
|
49
|
+
|
|
50
|
+
if self._pendingPassPromises[gamePassId] then
|
|
51
|
+
return self._maid:GivePromise(self._pendingPassPromises[gamePassId])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
MarketplaceService:PromptGamePassPurchase(self._obj, gamePassId)
|
|
55
|
+
local promise = Promise.new()
|
|
56
|
+
|
|
57
|
+
self._pendingPassPromises[gamePassId] = promise
|
|
58
|
+
|
|
59
|
+
return self._maid:GivePromise(promise)
|
|
60
|
+
end
|
|
61
|
+
|
|
36
62
|
function PlayerProductManagerClient:_setupRemoteEventLocal(remoteEvent)
|
|
37
63
|
-- Gear and other assets
|
|
38
64
|
self._maid:GiveTask(MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
|
|
@@ -61,4 +87,23 @@ function PlayerProductManagerClient:GetConfigPicker()
|
|
|
61
87
|
return self._gameConfigServiceClient:GetConfigPicker()
|
|
62
88
|
end
|
|
63
89
|
|
|
90
|
+
function PlayerProductManagerClient:_handlePassFinished(player, gamePassId, isPurchased)
|
|
91
|
+
assert(typeof(player) == "Instance", "Bad player")
|
|
92
|
+
assert(type(gamePassId) == "number", "Bad gamePassId")
|
|
93
|
+
assert(type(isPurchased) == "boolean", "Bad isPurchased")
|
|
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)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
self._pendingPassPromises[gamePassId] = nil
|
|
104
|
+
promise:Resolve(isPurchased)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
|
|
64
109
|
return PlayerProductManagerClient
|
|
@@ -119,8 +119,8 @@ function PlayerProductManager:_handlePassFinished(player, gamePassId, isPurchase
|
|
|
119
119
|
self:SetPlayerOwnsPass(gamePassId, true)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
promise:Resolve(isPurchased)
|
|
123
122
|
self._pendingPassPromises[gamePassId] = nil
|
|
123
|
+
promise:Resolve(isPurchased)
|
|
124
124
|
end
|
|
125
125
|
end
|
|
126
126
|
|
|
@@ -133,8 +133,8 @@ function PlayerProductManager:_handlePromptFinished(player, productId, isPurchas
|
|
|
133
133
|
if promise then
|
|
134
134
|
-- Success handled by receipt processing
|
|
135
135
|
if not isPurchased then
|
|
136
|
-
promise:Resolve(false)
|
|
137
136
|
self._pendingProductPromises[productId] = nil
|
|
137
|
+
promise:Resolve(false)
|
|
138
138
|
end
|
|
139
139
|
end
|
|
140
140
|
end
|
|
@@ -7,6 +7,9 @@ local require = require(script.Parent.loader).load(script)
|
|
|
7
7
|
local promiseBoundClass = require("promiseBoundClass")
|
|
8
8
|
local Rx = require("Rx")
|
|
9
9
|
local RxBinderUtils = require("RxBinderUtils")
|
|
10
|
+
local GameConfigAssetTypes = require("GameConfigAssetTypes")
|
|
11
|
+
local Promise = require("Promise")
|
|
12
|
+
local RxStateStackUtils = require("RxStateStackUtils")
|
|
10
13
|
|
|
11
14
|
local GameProductServiceBase = {}
|
|
12
15
|
GameProductServiceBase.ClassName = "GameProductServiceBase"
|
|
@@ -22,14 +25,45 @@ function GameProductServiceBase:GetPlayerProductManagerBinder()
|
|
|
22
25
|
error("Not implemented")
|
|
23
26
|
end
|
|
24
27
|
|
|
25
|
-
function GameProductServiceBase:ObservePlayerOwnsPass(player,
|
|
28
|
+
function GameProductServiceBase:ObservePlayerOwnsPass(player, passIdOrKey)
|
|
26
29
|
assert(typeof(player) == "Instance", "Bad player")
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
if type(passIdOrKey) == "string" then
|
|
32
|
+
local picker = self._gameConfigService:GetConfigPicker()
|
|
33
|
+
return picker:ObserveActiveAssetOfAssetTypeAndKeyBrio(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
34
|
+
:Pipe({
|
|
35
|
+
RxStateStackUtils.topOfStack();
|
|
36
|
+
Rx.switchMap(function(asset)
|
|
37
|
+
if asset then
|
|
38
|
+
return asset:ObserveAssetId()
|
|
39
|
+
else
|
|
40
|
+
return Rx.of(nil)
|
|
41
|
+
end
|
|
42
|
+
end);
|
|
43
|
+
Rx.switchMap(function(assetId)
|
|
44
|
+
if assetId then
|
|
45
|
+
return self:_observePlayerOwnsPassForId(player, assetId)
|
|
46
|
+
else
|
|
47
|
+
warn(("No pass with key %q"):format(tostring(passIdOrKey)))
|
|
48
|
+
return Rx.of(false)
|
|
49
|
+
end
|
|
50
|
+
end)
|
|
51
|
+
})
|
|
52
|
+
elseif type(passIdOrKey) == "number" then
|
|
53
|
+
return self:_observePlayerOwnsPassForId(player, passIdOrKey)
|
|
54
|
+
else
|
|
55
|
+
error("[GameProductServiceBase.ObservePlayerOwnsPass] - Bad passIdOrKey")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
function GameProductServiceBase:_observePlayerOwnsPassForId(player, passId)
|
|
60
|
+
assert(typeof(player) == "Instance", "Bad player")
|
|
61
|
+
assert(type(passId) == "number", "Bad passId")
|
|
28
62
|
|
|
29
63
|
return self:_observeManager(player):Pipe({
|
|
30
64
|
Rx.switchMap(function(manager)
|
|
31
65
|
if manager then
|
|
32
|
-
return manager:ObservePlayerOwnsPass(
|
|
66
|
+
return manager:ObservePlayerOwnsPass(passId)
|
|
33
67
|
else
|
|
34
68
|
return Rx.of(false)
|
|
35
69
|
end
|
|
@@ -37,29 +71,44 @@ function GameProductServiceBase:ObservePlayerOwnsPass(player, gamePassId)
|
|
|
37
71
|
})
|
|
38
72
|
end
|
|
39
73
|
|
|
40
|
-
function GameProductServiceBase:PromisePlayerOwnsPass(player,
|
|
74
|
+
function GameProductServiceBase:PromisePlayerOwnsPass(player, passIdOrKey)
|
|
41
75
|
assert(typeof(player) == "Instance", "Bad player")
|
|
42
|
-
assert(type(
|
|
76
|
+
assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
|
|
77
|
+
|
|
78
|
+
local passId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
79
|
+
if not passId then
|
|
80
|
+
return Promise.rejected(("No pass with key %q"):format(tostring(passIdOrKey)))
|
|
81
|
+
end
|
|
43
82
|
|
|
44
83
|
return self:_promiseManager(player)
|
|
45
84
|
:Then(function(manager)
|
|
46
|
-
return manager:PromisePlayerOwnsPass(
|
|
85
|
+
return manager:PromisePlayerOwnsPass(passId)
|
|
47
86
|
end)
|
|
48
87
|
end
|
|
49
88
|
|
|
50
|
-
function GameProductServiceBase:PromptGamePassPurchase(player,
|
|
89
|
+
function GameProductServiceBase:PromptGamePassPurchase(player, passIdOrKey)
|
|
51
90
|
assert(typeof(player) == "Instance", "Bad player")
|
|
52
|
-
assert(type(
|
|
91
|
+
assert(type(passIdOrKey) == "number" or type(passIdOrKey) == "string", "Bad passIdOrKey")
|
|
92
|
+
|
|
93
|
+
local passId = self:ToAssetId(GameConfigAssetTypes.PASS, passIdOrKey)
|
|
94
|
+
if not passId then
|
|
95
|
+
return Promise.rejected(("No pass with key %q"):format(tostring(passIdOrKey)))
|
|
96
|
+
end
|
|
53
97
|
|
|
54
98
|
return self:_promiseManager(player)
|
|
55
99
|
:Then(function(manager)
|
|
56
|
-
return manager:PromptGamePassPurchase(
|
|
100
|
+
return manager:PromptGamePassPurchase(passId)
|
|
57
101
|
end)
|
|
58
102
|
end
|
|
59
103
|
|
|
60
|
-
function GameProductServiceBase:PromisePromptPurchase(player,
|
|
104
|
+
function GameProductServiceBase:PromisePromptPurchase(player, productIdOrKey)
|
|
61
105
|
assert(typeof(player) == "Instance", "Bad player")
|
|
62
|
-
assert(type(
|
|
106
|
+
assert(type(productIdOrKey) == "number", "Bad productIdOrKey")
|
|
107
|
+
|
|
108
|
+
local productId = self:ToAssetId(GameConfigAssetTypes.PRODUCT, productIdOrKey)
|
|
109
|
+
if not productId then
|
|
110
|
+
return Promise.rejected(("No product with key %q"):format(tostring(productIdOrKey)))
|
|
111
|
+
end
|
|
63
112
|
|
|
64
113
|
return self:_promiseManager(player)
|
|
65
114
|
:Then(function(manager)
|
|
@@ -67,6 +116,21 @@ function GameProductServiceBase:PromisePromptPurchase(player, productId)
|
|
|
67
116
|
end)
|
|
68
117
|
end
|
|
69
118
|
|
|
119
|
+
function GameProductServiceBase:ToAssetId(assetType, assetIdOrKey)
|
|
120
|
+
assert(type(assetIdOrKey) == "number" or type(assetIdOrKey) == "string", "Bad assetIdOrKey")
|
|
121
|
+
|
|
122
|
+
if type(assetIdOrKey) == "string" then
|
|
123
|
+
local picker = self._gameConfigService:GetConfigPicker()
|
|
124
|
+
local asset = picker:FindFirstActiveAssetOfKey(assetType, assetIdOrKey)
|
|
125
|
+
if asset then
|
|
126
|
+
return asset:GetAssetId()
|
|
127
|
+
else
|
|
128
|
+
return nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
return assetIdOrKey
|
|
133
|
+
end
|
|
70
134
|
|
|
71
135
|
function GameProductServiceBase:_observeManager(player)
|
|
72
136
|
assert(typeof(player) == "Instance", "Bad player")
|
|
@@ -81,4 +145,5 @@ function GameProductServiceBase:_promiseManager(player)
|
|
|
81
145
|
end
|
|
82
146
|
|
|
83
147
|
|
|
148
|
+
|
|
84
149
|
return GameProductServiceBase
|